面试:Java 多线程

1. 线程、进程概念和区别

线程 4 种状态:运行、就绪、挂起、结束。

进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间,进程与线程的对比关系如图4-15所示。

在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行,那么为什么要使用多线程呢?其实,多线程的使用为程序研发带来了巨大的便利,具体而言,有以下几个方面的内容:

  1. 多线程可以减少程序响应时间

  2. 与进程相比,线程的创建和切换开销更小;数据共享效率高

  3. 多线程充分利用多核 CPU,提高 CPU 使用率

  4. 使用多线程能简化程序结构,便于理解和维护:一个复杂的进程可以分为多个线程来执行

2. 同步和异步有什么区别?

要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证在同一时刻只有个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的线程退出临界区时,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。

Java 语言在同步机制中提供了语言级的支持,可以通过使用 synchronized关键字来实现同步,但该方法并非“万金油”,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并非越多越好,要尽量避免无谓的同步控制。实现同步的方式有两种:一种是利用同步代码块来实现同步;另一种是利用同步方法来实现同步。

异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此,在进行输人输出处理时,不必关心其他线程的状态或行为,也不必等到输入输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能够提高程序的效率。

3. 如何实现 Java 多线程?

3.1 继承 Thread 类,重写 run() 方法

Thread 本质上也是实现了 Runnable 接口的一个实例,代表一个线程的实例。并且启动线程的唯一方法是通过 Thread类的 start()方法,它是一个本地(native)方法,将启动一个新线程,并执行 run()方法(Thread 中的 run()方法是一个空方法)。

这种方式通过自定义直接 extend Thread,并重写 run()方法,就可以启动新线程并执行自定义的 run()方法。需要注意的是调用 start()后并不是立即执行多线程代码,而是使得线程变为 可运行状态( Runnable ),什么时候运行多线程代码是由才做系统决定的。


class MyThread extends Thread{

public void run(){

System.out.println("Thread Body");

}

}



public class Test{

public static void main(String[] args){

MyThread thread = new MyThread();

thread.start();

}

}

3.2 实现 Runnable 接口,并实现该接口的 run()方法

  1. 自定义类并实现 Runabble 接口,实现 run()方法

  2. 创建 Thread 对象,用实现 Runnable 接口的对象作为参数,实例化该 Thread对象

  3. 调用 Threadstart()方法


class MyThread implements Runnable{

public void run(){

System.out.println("Thread Body");

}

}



public class Test{

public static void main(String[] args){

MyThread thread = new MyThread();

Thread t = new Thread(thread);

t.start();

}

}

不管是通继承 Thrad 类还是通过 Runnable 接口来实现多线程的方法,最终还是通过 Thread 的对象 API 来控制线程。

通常还是用实现 Runnable 接口的方式。

3.3 实现 Callable 接口,重写 call() 方法

Callable 接口实际是属于 Executor框架中的功能类, Callable接口与 Runnable 接口的功能类似,但提供了比 Runnable 更强大的功能,主要表现为以下 3 点:

  1. Callable 可以在任务结束后提供一个返回值Runnable无法提供这个功能。

  2. Callable 中的call()方法可以抛出异常,而 Runnablerun()方法不能抛出异常。

  3. 运行 Callable 可以拿到一个 Future 对象, Future 对象表示异步计算的结果它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用 Future 来监视目标线程调用 call() 方法的情况,当调用Futureget()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。


import java.util.concurrent.*;



public class CallableAndFuture{

// 创建线程类

public static class CallableTest implements Callable<String>{

public String call() throws Exception{

return "Hello World";

}

}



public static void main(String[] args){

// 一个单线程池 ExecutorService

ExecutorService threadPool = Executors.newSingleThreadExecutor();

// 启动线程

// 向线程池 ExecutorService 提交 Callable 对象,返回 Future 对象

Future<String> future = threadPool.submit(new CallableTest());

try{

System.out.println("waiting thread to finish");

// future.get() 获取线程结果(阻塞直到获取成功)

System.out.println(future.get());

}

catch (Exception e) {

e.printStackTrace();

}



}



}



// waiting thread to finish

// Hello World

4. run() 方法和 start()方法的区别

通常,系统通过调用线程类的 start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被 JVM 来调度执行。在调度过程中,JVM 通过调用线程类的run()方法来完成实际的操作,当run() 方法结束后,此线程就会终止。

如果直接调用线程类的run() 方法,这会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程,也就是说, start()方法能够异步地调用run() 方法,但是直接调用run() 方法却是同步的,因此也就无法达到多线程的目的。

5. 多线程同步的实现方法

5.1 synchronized 关键字

在 Java 语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有当一个线程调用对象的一段 synchronized 代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后,释放锁。

synchronized关键字主要有两种用法( synchronized方法synchronized),此外该关键字还可以作用于静态方法、类或某个实例,但这都对程序的效率有很大的影响。

5.1.1 synchronized 方法


public synchronized void multiThreadAccess();

只要把多个线程对类需要被同步的资源的操作放到 multiThreadAccess() 方法中,就能保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而,当一个方法的方法体规模非常大时,把该方法声明为 synchronized 会大大影响程序的执行效率。

为了提高程序的效率,Java 提供了 synchronized 块。

5.1.2 synchronized


synchronized(syncObject){

// 访问 syncObject 的代码

}

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

5.2 wait()方法与 notify()方法

当使用 synchronized 来修饰某个共享资源时,如果线程 A1 在执行 synchronized 代码,另外一个线程 A2也要同时执行同一对象的同一 synchronized 代码时,线程 A2 将要等到线程 A1 执行完成后,才能继续执行。在这种情况下可以使用wait() 方法和 notify() 方法。

synchronized 代码被执行期间,线程可以调用对象的 wait()方法,释放对象锁,进入等待状态,并且可以调用 notify() 方法或者 notifyAll() 方法通知 正在等待的其他线程notify()方法仅唤醒一个线程(等待队列中的一个线程)并允许它获得锁,而 notifyAll()唤醒所有等待这个对象的线程并允许它们去获得锁(只是让它们去竞争这个锁)。


public class Producer extends Thread {



List<Message> msgList = new ArrayList<>();



@Override public void run() {

try {

while (true) {

Thread.sleep(3000);

Message msg = new Message();

synchronized(msgList) {

msgList.add(msg);

msgList.notify(); //这里只能是notify而不能是notifyAll,否则remove(0)会报java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

}

}

} catch (Exception e) {

e.printStackTrace();

}

}



public Message waitMsg() {

synchronized(msgList) {

if(msgList.size() == 0) {

try {

msgList.wait();

} catch(InterruptedException e) {

e.printStackTrace();

}

}

return msgList.remove(0);

}

}

}



public class Consumer extends Thread {



private Producer producer;



public Consumer(String name, Producer producer) {

super(name);

this.producer = producer;

}



@Override public void run() {

while (true) {

Message msg = producer.waitMsg();

System.out.println("Consumer " + getName() + " get a msg");

}

}



public static void main(String[] args) {

Producer p = new Producer();

p.start();

new Consumer("Consumer1", p).start();

new Consumer("Consumer2", p).start();

new Consumer("Consumer3", p).start();

}

}

5.3 Lock

JDK5 新增了 Lock 几口以及它的一个实现类 ReentrantLock(重入锁),Lock 也可以用来实现多线程的同步,它提供了如下一些方法来实现线程的同步:

lock():以阻塞的方式获取锁,也就是说,如果获取到了则立即返回,否则线程等到,直到获取锁后返回

tryLock():以非阻塞的方式获取锁,只是尝试性地获取,获取到返回 true,获取不到返回 false

tryLock(long timeout, TimeUnit unit):上面函数基础上,指定超时时间,以及时间单元

lockInterruptibly():如果获取了锁立即返回,否则当前线程处于休眠状态,直到获得锁, 或者当前线程被其它线程打断(会收到 InterruptedException,它与 lock()最大的区别在于 lock()获取不到锁会一直阻塞,而且会忽略 interrupt()方法。

如果把lock.lockInterruptibly() 替换为 lock.lock(),编译器将会提示lock.lock() catch代码块无效,这是因为lock.lock不会抛出异常,由此可见lock()方法会忽略 interrupt()引发的异常。

6. sleep()方法与 wait()方法的区别

sleep()是使线程暂停执行一段时间的方法,wait()也是一种使线程暂停执行的方法,例如,当线程交互时,如果线程对一个同步对象 x 发出一个wait()调用请求,那么该线程会暂停执行,被调对象进入等待状态,直到被唤醒或等待时间超时。

具体而言,两者的区别如下:

  1. 原理不同。 sleep()方法是 Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程(并非释放锁),等到计时时间一到,此线程会自动“苏醒”,例如,当线程执行报时功能时每一秒钟打印出一个时间,那么此时就需要在打印方法前面加上一个 sleep()方法,以便让自己每隔 1s 执行一次,该过程如同闹钟一样。而wait()方法是 Object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用 notify()notifyAll()方法时才“醒”来,不过开发人员也可以给它指定一个时间,自动“醒”来。wait()方法配套的方法还有 notify()notifyAll() 方法。

  2. 对锁的处理机制不同。由于sleep()方法的主要作用是让线程暂停执行一段时间,时间到则自动恢复,不涉及线程间的通信,因此,调用 sleep()方法并不会释放锁。wait()方法则不同,当调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他 synchronized数据可被别的线程使用。举个简单例子,如果何昊拿遥控器的期间,可以用自己的 sleep()方法每隔10min调一次频道,而在这10min里,遥控器还在他的手上。

  3. 使用区域不同。由于wait()方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而 sleep()方法则可以放在任何地方使用。

sleep()方法必须捕获异常,而wait()notify()以及 notifyall()不需要捕获异常。在 sleep的过程中,有可能被其他对象调用它的 interrupt(),产生 InterruptedException异常。

sleep于不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用 sleep()方法,而推荐使用wait()方法。

yield() 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。用了yield()方法后,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行);而 sleep() 方法是变为阻塞状态

7. 终止线程的方法

在 Java 语言中,可以使用stop()方法与 suspend()方法来终止线程的执行。

当用 Thread.stop()来终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他线程将会“看”到这个不一致的状态,这可能会导致程序执行的不确定性,并且这种问题很难被定位。**

调用 suspend()方法容易发生死锁(死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进)。由于调用 suspend()方法不会释放锁,这就会导致一个问题:如果用一个 suspend挂起一个有锁的线程,那么在锁恢复之前将不会被释放。如果调用 suspend()方法,线程将试图取得相同的锁,程序就会发生死锁例如,线程A已经获取到了互斥资源M的锁,此时线程A通过 suspend()方法挂起线程A的执行,接着线程B也去访问互斥资源M,这时候就造成了死锁。

鉴于以上两种方法的不安全性,Java语言已经不建议使用以上两种方法来终止线程了。

那么,如何才能终止线程呢?一般建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,即执行完run()方法,也就是说,如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run()方法的执行。在实现时,可以通过设置一个flag标志来控制循环是否执行,通过这种方法来让线程离开run()方法从而终止线程。下例给出了结束线程的方法:

上例中,通过调用 MyThreadstop()方法虽然能够终止线程,但同样也存在问题:当线程处于非运行状态时(当 sleep()方法被调用或当wait()方法被调用或当被IO阻塞时),上面介绍的方法就不可用了。此时可以使用 interrupt()来打破阻塞的情况。当 interrupt()方法被调用时,会抛出 InterruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出,具体实现方式如下:

运行结果为:


thread go to sleep

thread is interupted!

如果程序因为 IO 而停滞,进入非运行状态,基本上要等到 IO 完成才能离开这个状态,在这种情况下,无法使用 interrupt()方法来使程序离开run()方法。这就需要使用一个替代的方法,基本思路也是触发一个异常,而这个异常与所使用的IO相关,例如,如果使用readLine()方法在等待网络上的一个信息,此时线程处于阻塞状态。让程序离开run()方法就是使用 close()方法来关闭流,在这种情况下会引发 IOException异常,run()方法可以通过捕获这个异常来安全地结束线程。

8. synchronizedLock 有什么异同?

这是 Java 提供的两种锁机制,synchronized使用 Object对象本身的 notifywaitnotifyAll机制,而 Lock 可以使用 Condition 进行线程之间的调度,完成 synchronized实现的所有功能。

两者的区别:

虽然两者都可以实现多线程同步,但是最好不要同时使用两者。因为 ReentrantLocksynchronized所使用的机制不同,所以它们的运行也是独立的,相当于两种类型的锁,在使用时互不影响。


import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;



class SyncTest{

private int value = 0;

// 可重入锁

Lock lock = new ReentrantLock();



// synchronized 对 value 加 1

public synchronized void addValueSync(){

this.value++;

System.out.println(Thread.currentThread().getName() + ":" + value);

}



// Lock 对 value 加 1

public void addValueLock(){

try{

lock.lock();

value++;

System.out.println(Thread.currentThread().getName() + ":" + value);

}

finally{

lock.unlock(); // 一定要再finally中释放锁

}

}

}



public class Test{

public static void main (String[] args) {

final SyncTest st = new SyncTest();

// 测试 synchronized

Thread t1 = new Thread(

new Runnable(){

public void run(){

for(int i = 0; i < 5; i++){

st.addValueSync();

try{

Thread.sleep(20); // 20 毫秒

}

catch(InterruptedException e){

e.printStackTrace();

}

}

}

}

);

// 测试 Lock

Thread t2 = new Thread(

new Runnable(){

public void run(){

for(int i = 0; i < 5; i++){

st.addValueLock();

try{

Thread.sleep(20); // 20 毫秒

}

catch(InterruptedException e){

e.printStackTrace();

}

}

}

}

);

t1.start();

t2.start();

}

}

运行结果:


Thread-1:2

Thread-0:1

Thread-1:4

Thread-0:3

Thread-0:5

Thread-1:6

Thread-0:7

Thread-1:8

Thread-0:9

Thread-1:10

每次运行结果都可能不同,而且 value 的值不是连续从1加到10(这就是因为两种锁采用了不同的机制,也就是 Lock ),所以不能同时使用两种锁。

这句话解释一下:对于静态方法,即使是 synchronized,比如 public synchronized static void foo(),它的锁并不是对象了,而是类的字节码

或者如果这个 synchronized 方法内部调用了 wait(),即释放改对象资源,那么其他某个 synchronized方法也可以被调用,否则不行。

9. 什么是守护线程?

两种线程:守护线程、用户线程。守护线程又称为“服务线程”、”后台线程“,是在后台提供一种通用服务的线程,不是不可或缺的,相当于是非守护线程的保姆。

用户线程和守护线程几乎一样,唯一的不同之处就在于如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM 也就退出了。因为当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有守护线程。也就是说,只要有任何非守护线程还在运行,程序就不会终止。

Java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程,例如,将一个用户线程设置为守护线程的方法就是在调用start()方法启动线程之前调用对象的 setDaemon(ture)方法,若将以上参数设置为 false,则表示的是用户进程模式。需要注意的是,当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此,示例如下


class ThreadDemo extends Thread{

public void run(){

System.out.println(Thread.currentThread().getName() + ":begin");

try{

Thread.sleep(1000);

}

catch(InterruptedException e){

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + ":end");

}

}



public class Test{

public static void main (String[] args) {

System.out.println("test3:begin");

Thread t1 = new ThreadDemo();

t1.setDaemon(true);

t1.start();

System.out.println("test3:end");



}

}





// 实际上只输出 test3,守护线程根本没输出,书上还输出了守护线程的 begin

10. join()方法的作用

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


public class JoinTest{

public static void main (String[] args) {

Thread t = new Thread(new ThreadImp());

t.start();

try{

// 主线程(这个t.join的调用者)等待线程 t 结束,只等待 1s ,然后执行后面的代码

t.join(1000);

if(t.isAlive()){

System.out.println("t has not finished");

}

else{

System.out.println("t has finished");

}

System.out.println("join finish");

}

catch (InterruptedException e) {

e.printStackTrace();

}

}

}



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();

}

}

}

输出:


Begin ThreadImp

t has not finished

join finish

End ThreadImp