前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java多线程常用面试题

Java多线程常用面试题

作者头像
全栈程序员站长
发布2022-08-31 17:02:15
2680
发布2022-08-31 17:02:15
举报
文章被收录于专栏:全栈程序员必看

大家好,又见面了,我是你们的朋友全栈君。

一、什么是多线程?

线程是指程序在运行的过程中,能够执行程序代码的一个执行单元。

Java语言中,线程有五种状态:新建、就绪、运行、阻塞及死亡。


二、线程与进程的区别?

进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段、堆空间)及一些进程级的文件(列如:打开的文件),但是各个线程拥有自己的栈空间。在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行。


三、为什么要使用多线程?

【1】提高执行效率,减少程序的响应时间。因为单线程执行的过程只有一个有效的操作序列,如果某个操作很耗时(或等待网络响应),此时程序就不会响应鼠标和键盘等操作,如果使用多线程,就可以将耗时的线程分配到一个单独的线程上执行,从而使程序具备更好的交互性。 【2】与进程相比,线程的创建和切换开销更小。因开启一个新的进程需要分配独立的地址空间,建立许多数据结构来维护代码块等信息,而运行于同一个进程内的线程共享代码段、数据段、线程的启动和切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。 【3】目前市场上服务器配置大多数都是多CPU或多核计算机等,它们本身而言就具有执行多线程的能力,如果使用单个线程,就无法重复利用计算机资源,造成资源浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。 【4】利用多线程能简化程序程序的结构,是程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。


四、同步与异步有什么区别?

在多线程的环境中,通常会遇到数据共享问题,为了确保共享资源的正确性和安全性,就必须对共享数据进行同步处理(也就是锁机制)。对共享数据进行同步操作(增删改),就必须要获得每个线程对象的锁(this锁),这样可以保证同一时刻只有一个线程对其操作,其他线程要想对其操作需要排队等候并获取锁。当然在等候队列中优先级最高的线程才能获得该锁,从而进入共享代码区。 Java语言在同步机制中提供了语言级的支持,可以通过使用synchronize关键字来实现同步,但该方法是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并不是越多越好,要避免所谓的同步控制。实现同步的方法有两种:①同步方法(this锁)。②同步代码块(this锁或者自定义锁)当使用this锁时,就与同步方法共享同一锁,只有当①释放,②才可以使用。同时,同步代码块的范围也小于同步方法,建议使用,相比之下能够提高性能。


五、如何实现java多线程?

Java虚拟机允许应用程序并发地运行多个线程,在Java语言中实现多线程的方法有三种,其中前两种为常用方法:

【1】继承Thread类,重写run()方法 Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且启动线程的唯一方法就是通过Thread类的start()方法,start()方法是一个本地(native)方法,它将启动一个新的线程,并执行run()方法(执行的是自己重写了Thread类的run()方法),同时调用start()方法并不是执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行多线程代码由操作系统决定。

代码语言:javascript
复制
class MyThread extends Thread{//创建线程类
    public void run(){
       System.out.println("Thread Body");//线程的函数体
    }
}
 
public class Test{
   public static void main(String[] args){
     MyThread thread = new Thread();
     thread.run();//开启线程
   }
}

【2】实现Runnable接口,并实现该结构的run()方法 1)自定义实现Runnable接口,实现run()方法。 2)创建Thread对象,用实现Runnable接口的对象作为参数实例化该Thread对象。 3)调用Thread的start()方法。

代码语言:javascript
复制
class MyThread implements Runnable{
   pulic void run(){
      System.out.println("Thread Body");
   }
}
 
public class Test{
  public static void main(String[] args){
     MyThread myThread = new MyThread();
     Thread thread = new Thread(myThread);
     thread.start();//启动线程
  }
}

其实,不管是哪种方法,最终都是通过Thread类的API来控制线程。

【3】实现Callable接口,重写call()方法 Callable接口实际是属于Executor框架中的功能类,Callable结构与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要体现在如下三点: 1)Callable在任务结束后可以提供一个返回值,Runnable无法提供该功能。 2)Callable中的call()方法可以跑出异常,而Runnable中的run()不能跑出异常。 3)运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供能了检查计算是否完成的方法。由于线程输入异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监控目标线程来调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程会阻塞,直到目标线程的call()方法结束返回结果。

代码语言:javascript
复制
public class CallableAndFuture{
   //创建线程类
   public static class CallableTest implements Callable{
     public String call() throws Exception{
        return "Hello World!";
     }
   }
   public static void main(String[] args){
     ExecutorService threadPool = Executors.newSingleThreadExecutor();
     Future<String> future = threadPool.submit(new CallableTest());
     try{
          System.out.println("waiting thread to finish");
          System.out.println(future.get());
        }catch{Exception e}{
          e.printStackTrace
        }
   }
}

建议:当需要实现多线程时,一般推荐使用Runnable接口方式,因为Thread类定义了多种方法可以被派生类使用或重写,但是只有run()方法必须被重写,在run()方法中实现这个线程的主要功能,这当然也是实现Runnable接口所需的方法。再者,我们很多时候继承一个类是为了去加强和修改这个类才去继承的。因此,如果我们没有必要重写Thread类中的其他方法,那么通过继承Thread类和实现Runnable接口的效果是相同的,这样的话最好还是使用Runnable接口来创建线程。


六、run()方法与start()方法有什么区别?

通常,系统通过调用线程类的start()方法启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以别JVM调用执行,执行的过程中,JVM通过调用想成类的run()方法来完成实际的操作,当run()方法结束后,线程也就会终止。 如果直接调用线程类的run()方法,就会被当做一个普通函数调用,程序中仍然只有一个主程序,也就是说start()方法能够异步调用run()方法,但是直接调用run()方法却是同步的,也就无法达到多线程的目的。


七、多线程数据同步实现的方法有哪些?

当使用多线程访问同一数据时,非常容易出现线程安全问题,因此采用同步机制解决。Java提供了三种方法:

【1】synchronized关键字 在Java语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的synchronize代码时,需要先获取这个锁,然后再去执行相应的代码,执行结束后,释放锁。 synchronize关键字主要有两种用法(synchronize方法和synchronize代码块) 1)synchronized方法:在方法的声明前加synchronize关键字:

代码语言:javascript
复制
    public synchronize void test();

将需要对同步资源的操作放入test()方法中,就能保证此资源在同一时刻只能被一个线程调用,从而保证资源的安全性。然而当此方法体规模非常大时,会影响系统的效率。

2)synchronized块:既可以把任意的代码段声明为synchronized,也可以指定上锁的对象,有非常高的灵活性。

代码语言:javascript
复制
    synchronized(syncObject){
    //访问syncObject的代码块
    }

【2】wait()方法与notify()方法

当使用synchronized来修饰某个共享资源时,如果线程A1在执行synchronized代码,线程A2也要执行同一对象的统同一synchronize的代码,线程A2将要等到线程A1执行完后执行,这种情况可以使用wai()和notify()。必须是统一把锁,才生效。

代码语言:javascript
复制
 class NumberPrint implements Runnable{  
        private int number;  
        public byte res[];  
        public static int count = 5;  
        public NumberPrint(int number, byte a[]){  
            this.number = number;  
            res = a;  
        }  
        public void run(){  
            synchronized (res){  
                while(count-- > 0){  
                    try {  
                        res.notify();//唤醒等待res资源的线程,把锁交给线程(该同步锁执行完毕自动释放锁)  
                        System.out.println(" "+number);  
                        res.wait();//释放CPU控制权,释放res的锁,本线程阻塞,等待被唤醒。  
                        System.out.println("------线程"+Thread.currentThread().getName()+"获得锁,wait()后的代码继续运行:"+number);  
                    } catch (InterruptedException e) {  
                        // TODO Auto-generated catch block  
                        e.printStackTrace();  
                    }  
                }//end of while  
                return;  
            }//synchronized  
              
        }  
    }  
    public class WaitNotify {
        public static void main(String args[]){  
            final byte a[] = {0};//以该对象为共享资源  
            new Thread(new NumberPrint((1),a),"1").start();  
            new Thread(new NumberPrint((2),a),"2").start();  
        }  
    }  

输出结果:

代码语言:javascript
复制
 1  
 2  
------线程1获得锁,wait()后的代码继续运行:1  
 1  
------线程2获得锁,wait()后的代码继续运行:2  
 2  
------线程1获得锁,wait()后的代码继续运行:1  
 1  
------线程2获得锁,wait()后的代码继续运行:2 

【3】Lock JDK5新增加Lock接口以及它的一个实现类ReentrantLock(重入锁),也可以实现多线程的同步; 1)lock():以阻塞的方式获取锁,也就是说,如果获取到了锁,就会执行,其他线程需要等待,unlock()锁后别的线程才能执行,如果别的线程持有锁,当前线程等待,直到获取锁后返回。

代码语言:javascript
复制
public int consume(){
        int m = 0;
        try {
            lock.lock();
            while(ProdLine.size() == 0){
                System.out.println("队列是空的,请稍候");
                empty.await();
            }
            m = ProdLine.removeFirst();
            full.signal(); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            lock.unlock();
            return m;
        }
    }

2)tryLock()。以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回true,否则,返回false。 3)tryLock(long timeout,TimeUnit unit)。在给定的时间单元内,获取到了锁返回true,否则false。 4)lockInterruptibly().如果获取了锁,立即返回;如果没有锁,当前线程处于休眠状态,直到获取锁,或者当前线程被中断(会收到InterruptedException异常)。它与lock()方法最大的区别在于如果()方法获取不到锁,就会一直处于阻塞状态,且会忽略Interrupt()方法。


八、sleep()方法与wait()方法有什么区别?

sleep()是使线程暂停执行一段时间的方法。wait()也是一种使线程暂停执行的方法,直到被唤醒或等待时间超时。 区别:

1)原理不同:sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到时间一到,此线程会自动“苏醒”。 wait()方法是Object类的方法,用于线程间通讯,这个方法会使当前线程拥有该对象锁的进程等待,直到其他线程调用notify()方法(或notifyAll方法)时才“醒”来,不过开发人员可可以给它指定一个时间,自动“醒”来。与wait()方法配套的方法还有notify()和notifyAll()方法。 2)对锁的处理机制不同。由于sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通讯,因此,调用sleep()方法并不会释放锁。而wait()方法则不同,调用后会释放掉他所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。 3)使用区域不同,由于wait()的特殊意义,因此它必须放在同步控制方法或者同步代码块中使用,而sleep()则可以放在任何地方使用。 4)sleep()方法 必须捕获异常,而wait()、notify()、notifyAll()不需要捕获异常。在sleep的过程中,有可能被其他对象调用它的interrupt(),产生InterruptedException异常。 sleep不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用sleep()方法。而推荐使用wait()方法。


九、sleep()与yield()的区别?

1)sleep()给其他线程运行机会时,不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而yield()方法只会给相同优先级或更高优先级的线程以运行的机会。 2)sleep()方法会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程很可能在进入到可执行状态后马上又被执行。


十、终止线程的方法有哪些?

1)stop()方法,它会释放已经锁定的所有监视资源,如果当前任何一个受监视资源保护的对象处于一个不一致的状态(执行了一部分),其他线程线程将会获取到修改了的部分值,这个时候就可能导致程序执行结果的不确定性,并且这种问题很难被定位。 2)suspend()方法,容易发生死锁。因为调用suspend()方法不会释放锁,这就会导致此线程挂起。 鉴于以上两种方法的不安全性,Java语言已经不建议使用以上两种方法来终止线程了。 3)一般建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,既执行完run()方法,也就是说提供一种能够自动让run()方法结束的方式,在实际中,我们可以通过flag标志来控制循环是否执行,从而使线程离开run方法终止线程。

代码语言:javascript
复制
public class MyThread implements Runnable{
 private volatile Boolean flag;
 public void stop(){
     flag=false;
 }
 public void run(){
  while(flag);//do something
 }
}

上述通过stop()方法虽然可以终止线程,但同样也存在问题;当线程处于阻塞状态时(sleep()被调用或wait()方法被调用或当被I/O阻塞时),上面介绍的方法就不可用了。此时使用interrupt()方法来打破阻塞的情况,当interrupt()方法被调用时,会跑出interruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出。

代码语言:javascript
复制
public class MyThread{
  public static void main(String[] args){
    Thread thread = new Thread(new MyThread);
    public void run(){
       System.out.println("thread go to sleep");
       try{
            //用休眠来模拟线程被阻塞
            Thread.sleep(5000);
            System.out.println("thread finish");
          } catch (InterruptedException e){
            System.out.println("thread is interrupted!);
           }
     } 
   }
}
thread.start();
therad.interrupt();

程序运行结果:thread go to sleep thread is interrupted! 如果I/0停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态。或者通过出发异常,使用readLine()方法在等待网络上的一个信息,此时线程处于阻塞状态,让程序离开run()就出发close()方法来关闭流,这个时候就会跑出IOException异常,通过捕获此异常就可以离开run()。


十一、synchronized与Lock有什么异同?

Java语言中提供了两种锁机制的实现对某个共享资源的同步;synchronized和Lock。其中synchronized使用Object类对象本身的notify()、wait()、notifyAll()调度机制,而Lock使用condition包进行线程之间的调度,完成synchronized实现的所有功能 1)用法不一样。synchronized既可以加在方法上,也可以加在特定的代码块中,括号中表示需要的锁对象。而Lock需要显式的指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现,他有比synchronized更精确的线程语义。 2)性能不一样。在JDK5中增加了一个Lock接口的实现类ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义、还多了锁投票、定时锁、等候锁和中断锁。它们的性能在不同的情况下会有所不同;在资源竞争不激烈的情况下,synchronized的性能要优于RenntrantLock,但是资源竞争激烈的情况下,synchronized性能会下降的非常快,而ReentrantLock的性能基本保持不变。 3)锁机制不一样。synchronized获得锁和释放锁的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且自动解锁,而condition中的await()、signal()、signalAll()能够指定要释放的锁。不会因为异常而导致锁没有被释放从而引发死锁的问题。而Lock则需要开发人员手动释放,并且必须放在finally块中释放,否则会引起死锁问题。此外,Lock还提供了更强大的功能,他的tryLock()方法可以采用非阻塞的方式去获取锁。 虽然synchronized与Lock都可以实现多线程的同步,但是最好不要同时使用这两种同步机制给统一共享资源加锁(不起作用),因为ReentrantLock与synchronized所使用的机制不同,所以它们运行时独立的,相当于两个种类的锁,在使用的时候互不影响。

面试题

【1】当一个线程进入一个对象的synchronized()方法后,其他线程是否能够进入此对象的其他方法? 答案:其他线程可进入此对象的非synchronized修饰的方法。如果其他方法有synchronized修饰,都用的是同一对象锁,就不能访问。

【2】如果其他方法是静态方法,且被synchronized修饰,是否可以访问? 答案:可以的,因为static修饰的方法,它用的锁是当前类的字节码,而非静态方法使用的是this,因此可以调用。


十二、什么是守护线程?

Java提供了两种线程:守护线程和用户线程守护线程又被称为“服务进程”、“精灵线程”、“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分,通俗点讲,每一个守护线程都是JVM中非守护线程的“保姆”。典型例子就是“垃圾回收器”。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。 用户线程和守护线程几乎一样,唯一的不同就在于如果用户线程已经全部退出运行,只剩下守护线程运行,JVM也就退出了因为当所有非守护线程结束时,没有了守护者,守护线程就没有工作可做,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有的守护线程。也就是说,只要有任何非守护线程运行,程序就不会终止。 Java语言中,守护线程优先级都较低,它并非只有JVM内部提供,用户也可以自己设置守护线程,方法就是在调用线程的start()方法之前,设置setDaemon(true)方法,若将参数设置为false,则表示用户进程模式。需要注意的是,守护线程中产生的其它线程都是守护线程,用户线程也是如此。

线程类:

代码语言:javascript
复制
package com.sunhui.Thread;

public class ThreadDaemon extends Thread {
	@Override
	public void run() {
		for(int i = 0 ; i<100;i++) {
			System.out.println(getName()+":"+i);
		}
	}
}

守护线程测试类:

代码语言:javascript
复制
package com.sunhui.Thread;
/*
 * public final void setDaemon(boolean on):是否设置为守护进程。true:是;false:否
 */
public class ThreadDaemonTest {
	public static void main(String[] args) {
		ThreadDaemon td1 = new ThreadDaemon ();
		ThreadDaemon td2 = new ThreadDaemon ();
		td1.setName("关羽");
		td2.setName("张飞");

		//添加守护线程
		td1.setDaemon(true);
		td2.setDaemon(true);
	
		td1.start();
		td2.start();
		
		Thread.currentThread().setName("刘备");
		for(int i=0;i<5;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

输出结果:

代码语言:javascript
复制
刘备:0
关羽:0
张飞:0
关羽:1
刘备:1
关羽:2
张飞:1
关羽:3
关羽:4
关羽:5
刘备:2
关羽:6
张飞:2
张飞:3
关羽:7
刘备:3
关羽:8
张飞:4
关羽:9
刘备:4
关羽:10
关羽:11
关羽:12
关羽:13
关羽:14
关羽:15
关羽:16
关羽:17

解释:

关羽线程和张飞线程均设置为守护线程,刘备线程为用户进程。这三个线程均随机抢占CPU的使用权,当刘备抢占并且运行完毕之后,关羽和张飞这两个线程将在某一时间死亡,切记并不是立刻死亡,而是刘备线程执行完毕的一段时间后。


十三、join()方法的作用是什么?

在Java语言中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。简单点说就是将两个线程合并,并实现同步功能。具体而言,可以通过线程A的join()方法来等待线程A的结束,或者使用线程A的join(2000)方法来等待线程A的结束,但最多只等2s。示例如下:

代码语言:javascript
复制
class ThreadImp implements Runnable{
  public void run(){
    try{
       System.out.println("Begin ThreadImp");
       Thread.sleep(5000);
       System.out.println("End ThreadImp");
    }catch(InterruptedException e){
       e.printStackTrace();
    }
  }
}
代码语言:javascript
复制
public class JoinTest{
  public static void main(String[] args){
    Thread t = new Thread(new ThreadImp());
    t.start();
    try{
         t.join(1000);//主线程等待1s
         if(t.isAlive()){
            System.out.println("t has not finished");
         }else{
            System.out.println("t has finished");
         }
       System.out.println("joinFinish");
    }catch(InterruptedExcetion e){
       e.printStackTrace();
    }
  }
}

运行结果:Begin ThreadImp t has not finished joinFinish End ThreadImp


发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/143032.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月2,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是多线程?
  • 二、线程与进程的区别?
  • 三、为什么要使用多线程?
  • 四、同步与异步有什么区别?
  • 五、如何实现java多线程?
  • 六、run()方法与start()方法有什么区别?
  • 七、多线程数据同步实现的方法有哪些?
  • 八、sleep()方法与wait()方法有什么区别?
  • 九、sleep()与yield()的区别?
  • 十、终止线程的方法有哪些?
  • 十一、synchronized与Lock有什么异同?
  • 十二、什么是守护线程?
  • 十三、join()方法的作用是什么?
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档