Java并发编程基础篇1-线程八大核心1(线程的创建、启动、中断、生命周期)

263 阅读16分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

0. 概念

0.1 面试题:为什么多线程极其重要???

  • 硬件方面:摩尔定律失效,

    • 摩尔定律:它是由英特尔创始人之一Gordon Moore(戈登·摩尔)提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。可是从2003年开始CPU主频已经不再翻倍,而是采用多核而不是更快的主频。
    • 摩尔定律失效。在主频不再提高且核数在不断增加的情况下,要想让程序更快就要用到并行或并发编程。
  • 软件方面:高并发系统,异步+回调等生产需求。

0.2 Java中的线程理解

  • Java线程理解以及openjdk中的实现

    • private native start0();
    • Java语言本身底层就是C++语言
    • OpenJKD源码网址:openjdk.java.net
  • 更加底层的C++源码解读

    • openjdk8\jdk\src\share\native\java\long 下的 thread.c

      • java线程是通过start的方法启动执行的,主要内容在native方法start0中,Openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c中的start0其实就是JVM_StartThread。此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。
    • openjdk8\hotspot\src\share\vm\prims 下的jvm.cpp

      image.png
      image.png

      • openjdk8\hotspot\src\share\vm\runtime 下的thread.cpp

        image.png

0.3 进程和线程

进程:是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源。

线程:在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程,⼀个进程会有1个或多个线程的。

管程Monitor(监视器),也就是我们平时所说的锁。Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码

JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象

Object o = new Object();

new Thread(() -> {
    synchronized (o)
    {

    }
},"t1").start();

Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。

在JVM第三版书中的介绍如下: 在这里插入图片描述

1. 核心1:实现多线程的正确姿势

1.1 实现多线程的方法到底是几种?

Oracle官网的文档是如何写的?

  • 方法一:实现Runnable接口
  • 方法二:继承Thread类

方法一:实现Runnable接口并传入Thread类

/**
 * 描述:     用Runnable方式创建线程
 */
public class RunnableStyle implements Runnable{

    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("用Runnable方法实现线程");
    }
}

方法二:继承Thread类然后重写run()

/**
 * 描述:     用Thread方式实现线程
 */
public class ThreadStyle extends Thread{

    @Override
    public void run() {
        System.out.println("用Thread类实现线程");
    }

    public static void main(String[] args) {
        new ThreadStyle().start();
    }
}

我们来看Thread类的run方法,如下:

/* What will be run. */
private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

方法一和方法二在实现多线程的本质上,并没有区别,都是最终调用了start()方法来新建 线程。这两个方法的最主要区别在于run()方法的内容来源:

  • 方法一:最终调用target.run();
  • 方法二:run()整个都被重写

1.2 同时用两种方式会怎么样?

/**
 * 描述:     同时使用Runnable和Thread两种实现线程的方式
 */
public class BothRunnableThread {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我来自Runnable");
            }
        }){
            @Override
            public void run() {
                System.out.println("我来自Thread");
            }
        }.start();
    }
}

执行结果为:

我来自Thread

原因:由于实现Runnable接口并传入Thread类,然后调用Thread类的run方法,但是我们又继承Tread类实现了run方法,所以Runnable传入的无效。

1.3 典型错误观点分析

1.3.1 线程池创建线程

线程池创建线程也算是一种新建线程的方法(本质也是通过Thread的方式)。如下:

/**
 * 描述:     线程池创建线程的方法
 */
public class ThreadPool5 {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Task() {
            });
        }
    }
}

class Task implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

但是我们跟进源码,其实也是通过传入Runnable构建Thread类,源码为DefaultThreadFactory工厂类如下:

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

1.3.2 Callable和FutureTask创建线程

无返回值是实现runnable接口,有返回值是实现callable接口,所以callable是新的实现线程的方式,这也是一种典型的错误观点。本质实现了Runnable接口

Callable接口的类图如下: image.png

FutureTask接口的类图如下: image.png

实质上,这两种也是通过实现Runnable并传入Thread类

1.3.3 定时器

TimerTask implements Runnable

/**
 * 描述:     定时器创建线程
 */
public class DemoTimmerTask {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }, 1000, 1000);
    }
}

1.3.4 匿名内部类

/**
 * 描述:     匿名内部类的方式
 */
public class AnonymousInnerClassDemo {

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
}

1.3.5 Lambda表达式

/**
 * 描述:     lambda表达式创建线程
 */
public class Lambda {

    public static void main(String[] args) {
        new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
    }
}

1.3.6 典型错误观点总结

多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗

1.4 总结

总结:最精准的描述

实现多线程的方法通常我们可以分为两类,Oracle也是这么说的

  • 准确的讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有两种方式
    • 方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类
    • 方法二:重写Thread的run方法(继承Thread类)

1.5 常见面试题

1.5.1 实现多线程的方法有几种?

答题思路,有以下5点:

  1. 从不同的角度看,会有不同的答案。

  2. 典型答案是两种,分别是实现Runnable接口继承Thread类,然后具体展开说;

  3. 但是,我们看原理,其实Thread类实现了Runnable接口,并且看Thread类的run方法,会发现其实那两种本质都是一样的,run方法的代码如下:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    

    方法一和方法二,也就是实现Runnable接口并传入Thread类继承Thread类然后重写run(),在实现多线程的本质上,并没有区别,都是最终调用了start()方法来新建线程。这两个方法的最主要区别在于run()方法的内容来源:

    • 方法一:最终调用target.run();
    • 方法二:run()整个都被重写
  4. 然后具体展开说其他方式; 还有其他的实现线程的方法,例如线程池等,它们也能新建线程,但是细看源码,从没有逃出过本质,也就是实现Runnable接口和继承Thread类。

  5. 结论:我们只能通过新建Thread类这一种方式来创建线程,但是类里面的run方法有两种方式来实现,第一种是重写run方法,第二种实现Runnable接口的run方法,然后再把该runnable实例传给Thread类。除此之外,从表面上看线程池、定时器等工具类也可以创建线程,但是它们的本质都逃不出刚才所说的范围。

以上这种描述比直接回答一种、两种、多种都更准确

1.5.2 实现Runnable接口 和继承Thread类,那种方法更好?

方法1(实现Runnable接口)更好

方法2是不推荐的,理由如下:

  • 从代码架构角度,具体的任务(run方法)应该和“创建和运行线程的机制(Thread类)”解耦。
  • 使用继承Thread的方式的话,那么每次想新建一个任务,只能新建一个独立的线程,而这样做的损耗会比较大。如果使用Runnable和线程池,就可以大大减小这样的损耗。
  • 继承Thread类以后,由于Java语言不支持双继承,这样就无法再继承其他的类,限制了可扩展性。 通常我们优先选择方法1。
两种方法的本质对比

方法一和方法二,也就是“实现Runnable接口并传入Thread类”和“继承Thread类然后重写run()”在实现 多线程的本质上,并没有区别,都是最终调用了start()方法来新建线程。这两个方法的最主要区别在 于run()方法的内容来源:

  • 方法一:最终调用target.run():
  • 方法二:run()整个都被重写

2. 核心2:启动多线程(start和run的区别)

2.1 启动多线程的正确和错误方式

2.1.1 start()方法

调用两次start()方法,报错,如下:

public class CanStartThreadTwice {
    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
        thread.start();
    }
}
//输出结果
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.startthread.CanStartThreadTwice.main(CanStartThreadTwice.java:16)

来看一下Thread.start()源码:

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

start()执行流程:

  • 检查线程状态,只有NEW状态下的线程才能继续,否则会抛出IllegalThreadStateException(在运行中或者已结束的线程,都不能再次启动)
  • 被加入线程组
  • 调用start()方法启动线程 注意点:
  • start方法是被synchronized修饰的方法,可以保证线程安全。
  • 由JVM创建的main方法线程和system组线程,并不会通过start来启动。

2.1.2 run()方法

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

2种情况

  • runnable对象target.run():本质跟普通方法一样的执行方式
  • 通过start()来间接地调用run()
public class StartAndRunMethod {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        //直接runnable.run
        runnable.run();
        //通过start方法来间接执行run
        new Thread(runnable).start();
    }
}

//输出结果
main
Thread-0

如直接调用run(),那么run()只是一个普通的方法而已,和线程的生命周期没有任何关系。

2.2 常见面试题

2.2.1 一个线程调用2次 start()方法会出现什么情况?

分析如 2.1.1 ;
从源码可以看出,start的时候会先检查线程状态,只有NEW状态下的线程才能继续,否则会抛出 IllegalThreadStateException.

2.2.2 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?

start()才是真正启动一个线程,而如果直接调用run(),那么run只是一个普通的方法而已,和线程的生 命周期没有任何关系。

3. 核心3:线程停止中断之最佳实践

常用Thread类的方法如下:

public static boolean interrupted测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false。
public boolean isInterrupted()测试线程是否已经中断。线程的中断状态 不受该方法的影响。
public void interrupt()中断线程。

3.1 原理介绍

使用interrupt来通知,而不是强制

interrupt是中断,A线程通知B线程去中断,而B线程是具有主动权的。B线程何时停止是B线程自己决定的,可能根据当前业务逻辑完成情况,所以说是通知,而不是强制

3.2 停止线程的最佳实践

通常线程会在什么情况下停止?

  • run方法的所有代码都执行完毕了
  • 有异常出现且没有捕获
  • 正确的停止方法:interrupt

3.2.1 普通情况下停止线程

thread.interrupt()通知,同时在thread的run方法中对interrupt状态进行响应

/**
 * run方法内没有sleep和wait方法时,停止线程
 */
public class RightWayStopThreadWithoutSleep implements Runnable {
    @Override
    public void run() {
        int num = 0;
        //run方法里面需要做相关的相应判断逻辑,如果不加!Thread.currentThread().isInterrupted(),等效是没有接受到interrupt通知,也就不会做停止的逻辑
        while (!Thread.currentThread().isInterrupted() && num < Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000的倍数");
            }
            num++;
        }
        System.out.println("任务运行结束了");
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(1000);
        //使用interrupt去通知中断
        thread.interrupt();
    }
}

3.2.2 线程被阻塞的情况(sleep)

public class RightWayStopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        System.out.println(num + "是100的倍数");
                    }
                    num++;
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}
//输出结果
0100的倍数
100100的倍数
200100的倍数
300100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadWithSleep.lambda$main$0(RightWayStopThreadWithSleep.java:21)
	at java.lang.Thread.run(Thread.java:748)

3.2.3 如果线程在每次迭代(for/while)后都阻塞

/**
 *如果在执行过程中,每次循环都会调用sleep和wait等方法,那么不需要每次迭代都检查是否已中断
 */
public class RightWayStopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                while (num <= 10000) {
                    if (num % 100 == 0) {
                        System.out.println(num + "是100的倍数");
                    }
                    num++;
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
//输出结果
0100的倍数
100100的倍数
200100的倍数
300100的倍数
400100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:20)
	at java.lang.Thread.run(Thread.java:748)

3.2.4、while内try/catch的问题

public class CanInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <=10000 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
//输出结果
0100的倍数
100100的倍数
200100的倍数
300100的倍数
400100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.CanInterrupt.lambda$main$0(CanInterrupt.java:20)
	at java.lang.Thread.run(Thread.java:748)
500100的倍数
600100的倍数
...

为什么加了Thread.currentThread().isInterrupted()判断后,线程没有停止,仍然继续执行?

其实是因为sleep响应中断后,会把interrupt状态清除,所以中断信号无效

3.2.5、实际开发中的2种最佳实践

  • 优先选择:传递中断(将中断向上抛出,由顶层run方法来处理中断)
/**
 * 最佳实践:catch了InterruptException之后的优先选择:在方法签名中抛出异常
 * 那么在run()就会强制try/catch
 */
public class RightWayStopThreadInProd implements Runnable {
    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                System.out.println("保存日志逻辑");
                e.printStackTrace();
            }
        }
    }
 
    private void throwInMethod() throws InterruptedException {
        //在方法签名中抛出异常
        Thread.sleep(2000);
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
//输出结果
go
保存日志逻辑
go //中断后,interrupt标志位被清除,所以会不断循环打印go
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:26)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:17)
	at java.lang.Thread.run(Thread.java:748)
go
...
  • 不想或无法传递:恢复中断
/**
 *  最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,
 * 以便于在后续的执行中,依然能够检查到刚才发生了中断
 * 回到刚才RightWayStopThreadInProd补上中断,让它跳出
 */
public class RightWayStopThreadInProd2 implements Runnable {
    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted,程序运行结束");
                break;
            }
            reInterrupt();
        }
    }
 
    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            //在这里恢复中断状态
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
//输出结果
java.lang.InterruptedException: sleep interrupted
Interrupted,程序运行结束
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd2.reInterrupt(RightWayStopThreadInProd2.java:26)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd2.run(RightWayStopThreadInProd2.java:20)
	at java.lang.Thread.run(Thread.java:748)
  • 不应屏蔽中断

3.2.6、响应中断的方法总结列表

  • Object.wait()/wait(long)/wait(long,int)
  • Thread.sleep(long)/Thread.sleep(long,int)
  • Thread.join()/join(long)/join(long,int)
  • java.util.concurrent.BlockingQueue.take()/put(E)
  • java.util.concurrent.locks.Lock.lockInterrruptibly()
  • java.util.concurrent.CountDownLatch.await()
  • java.util.concurrent.CyclicBarrier.await()
  • java.util.concurrent.Exchanger.exchange(V)
  • java.nio.channels.InterruptibleChannel相关方法
  • java.nio.channels.Selector的相关方法

3.3 停止线程的错误方法

3.3.1 被弃用的stop、suspend和resume方法

  • stop()来停止线程,会导致线程运行一半突然停止
/**
 *  错误的停止方法:用stop()来停止线程,会导致线程运行一半突然停止,
 * 没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)
 */
public class StopThread implements Runnable {
    @Override
    public void run() {
        //模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
        for (int i = 0; i < 5; i++) {
            System.out.println("连队" + i + "开始领取武器");
            for (int j = 0; j < 10; j++) {
                System.out.println(j);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("连队" + i + "已经领取完毕");
        }
    }
 
    public static void main(String[] args) {
        Thread thread = new Thread(new StopThread());
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.stop();
    }
}
//输出结果
连队0开始领取武器
0
1
2
3
4
5
6
7
8
9
连队0已经领取完毕
连队1开始领取武器
0
1
2
3
4
5
6
  • suspend将线程挂起,运行->阻塞;调用后并不释放所占用的锁(不释放锁,可能会导致死锁)
  • resume将线程解挂,阻塞->就绪(不释放锁,可能会导致死锁)

3.3.2 用volatile设置boolean标记位

3.3.2.1 看上去可行

/**
 *  演示用volatile的局限:part1看似可行
 */
public class WrongWayVolatile implements Runnable {
    private volatile boolean canceled = false;
 
    @Override
    public void run() {
        int num = 0;
        try {
            while (num < 10000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;
 
    }
}
//输出结果
0100的倍数
100100的倍数
200100的倍数
...

3.3.2.2 错误之处

  • 代码演示
/**
 * 演示用volatile的局限part2
 * 陷入阻塞时,volatile是无法停止线程的
 * 此例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
 */
public class WrongWayVolatileCantStop {
    public static void main(String[] args) throws InterruptedException {
        //生产者产生数据
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);
 
        //消费者消费数据
        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");
 
        //一旦消费不需要更多数据了,我们应该让生产者也停下来,但实际情况是没有停下来
        producer.canceled = true;
    }
}
 
/**
 * 生产者
 */
class Producer implements Runnable {
    public volatile boolean canceled = false;
    BlockingQueue storage;
 
    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }
 
    @Override
    public void run() {
        int num = 0;
        try {
            while (num < 10000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}
 
/**
 * 消费者
 */
class Consumer {
    BlockingQueue storage;
 
    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }
 
    public boolean needMoreNums() {
        return Math.random() > 0.95 ? false : true;
    }
}
 
//输出结果
0100的倍数,被放到仓库中了。
100100的倍数,被放到仓库中了。
200100的倍数,被放到仓库中了。
300100的倍数,被放到仓库中了。
400100的倍数,被放到仓库中了。
500100的倍数,被放到仓库中了。
600100的倍数,被放到仓库中了。
700100的倍数,被放到仓库中了。
800100的倍数,被放到仓库中了。
900100的倍数,被放到仓库中了。
1000100的倍数,被放到仓库中了。
0被消费了
100被消费了
1100100的倍数,被放到仓库中了。
200被消费了
1200100的倍数,被放到仓库中了。
1300100的倍数,被放到仓库中了。
300被消费了
1400100的倍数,被放到仓库中了。
400被消费了
1500100的倍数,被放到仓库中了。
500被消费了
600被消费了
1600100的倍数,被放到仓库中了。
700被消费了
1700100的倍数,被放到仓库中了。
1800100的倍数,被放到仓库中了。
800被消费了
900被消费了
1900100的倍数,被放到仓库中了。
1000被消费了
2000100的倍数,被放到仓库中了。
消费者不需要更多数据了。
//线程没有停止
  • 错误原因:线程阻塞在storage.put(num),无法继续执行
  • 修正方式

3.3.2.3 修正方案

/**
 * 使用interrupt方式正确处理生产者、消费者
 */
public class WrongWayVolatileFixed {
    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        //生产者产生数据
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);
 
        //消费者消费数据
        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");
        producerThread.interrupt();
    }
 
    /**
     * 生产者
     */
    class Producer implements Runnable {
        BlockingQueue storage;
 
        public Producer(BlockingQueue storage) {
            this.storage = storage;
        }
 
        @Override
        public void run() {
            int num = 0;
            try {
                while (num < 10000 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍数,被放到仓库中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者结束运行");
            }
        }
    }
 
    /**
     * 消费者
     */
    class Consumer {
        BlockingQueue storage;
 
        public Consumer(BlockingQueue storage) {
            this.storage = storage;
        }
 
        public boolean needMoreNums() {
            return Math.random() > 0.95 ? false : true;
        }
    }
}
//输出结果
...
3800100的倍数,被放到仓库中了。
2900被消费了
3900100的倍数,被放到仓库中了。
4000100的倍数,被放到仓库中了。
3000被消费了
java.lang.InterruptedException
消费者不需要更多数据了。
生产者结束运行
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
	at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.volatiledemo.WrongWayVolatileFixed$Producer.run(WrongWayVolatileFixed.java:51)
	at java.lang.Thread.run(Thread.java:748)

3.4 重要函数的源码解析

3.4.1 interrupt方法

判断是否已被中断相关方法

  • static boolean interrupted():判断是否中断,同时清除中断状态
 public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
private native boolean isInterrupted(boolean ClearInterrupted);
  • boolean isInterrupted() 返回线程是否中断,不清除中断状态

  • Thread.interrupted()的目的对象:静态interrupted只跟当前线程有关,与对象无关

3.5 常见面试问题

3.5.1 如何停止线程

  • 原理:用interrupt来请求,好处是可以保证数据安全,应该把主动权交给被中断的线程

  • 三方配合:想停止线程,要请求方、被停止方、子方法被调用方相互配合才行:

    • 作为被停止方:每次循环中或者适时检查中断信号,并且在可能抛出InterrupedException的地方处理该中断信号;
    • 请求方:发出中断信号;
    • 子方法调用方(被线程调用的方法的作者)要注意:优先在方法层面抛出InterrupedException,或者检查到中断信号时,再次设置中断状态;
  • 最后再说错误的方法:stop/suspend已废弃,volatile的boolean无法处理长时间阻塞的情况

3.5.2 如何处理不可中断的阻塞

(例如抢锁时ReentrantLock.lock()或者Socket I/O时无法响应中断,那应该怎么让该线程停止呢?)

  • 如果线程阻塞是由于调用了wait(),sleep()或join()方法,你可以中断线程,通过抛出InterruptedException异常来唤醒该线程。

  • 但是对于不能响应InterruptedException的阻塞,很遗憾,并没有一个通用的解决方案。

  • 但是我们可以利用特定的其它的可以响应中断的方法,比如ReentrantLock.lockInterruptibly(),比如关闭套接字使线程立即返回等方法来达到目的。

  • 答案有很多种,因为有很多原因会造成线程阻塞,所以针对不同情况,唤起的方法也不同。

  • 总结就是说如果不支持响应中断,就要用特定方法来唤起,没有万能药。

  • 针对特定的场景,用特定的方法来处理

4. 核心4:图解线程生命周期

4.1 6种状态

  • New:新建还未执行(start()
  • Runnable(可运行的):调用了start()方法后,就会变为Runnable状态
  • Blocked:进入synchronized修饰的区域,同时锁被其他线程拿走
  • Waiting:只能手工唤醒
  • Timed Waiting:计时等待。等到固定time时间后,就可以被唤醒;或者通过手工唤醒2种方式都可以
  • Terminated:程序正常执行完毕;或者出现没有被捕获的异常,中止了run方法

4.2 状态间的转化图示

image.png

4.3 代码示例

  • NEW、RUNNABLE、Terminated
/**
 * 展示线程的NEW、RUNNABLE、Terminated状态。即使是正在运行,也是Runnable状态,而不是Running
 */
public class NewRunnableTerminated implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(i);
        }
    }
 
    public static void main(String[] args) {
        Thread thread = new Thread(new NewRunnableTerminated());
        //打印出NEW的状态
        System.out.println(thread.getState());
        thread.start();
        System.out.println(thread.getState());
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印出RUNNABLE的状态,即使是正在运行,也是RUNNABLE,而不是RUNNING
        System.out.println(thread.getState());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印出TERMINATED状态
        System.out.println(thread.getState());
    }
}
//输出结果
NEW
RUNNABLE
0
1
2
...
501
RUNNABLE
502
503
...
999
TERMINATED
  • Blocked,Waiting,Timed Waiting
/**
 *  展示Blocked,Waiting,TimedWaiting
 */
public class BlockedWaitingTimedWaiting implements Runnable {
    public static void main(String[] args) {
        BlockedWaitingTimedWaiting runnable = new BlockedWaitingTimedWaiting();
        Thread thread1 = new Thread(runnable);
        thread1.start();
        Thread thread2 = new Thread(runnable);
        thread2.start();
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印出Timed_Waiting状态,因为正在执行Thread.sleep(1000);
        System.out.println(thread1.getState());
        //打印出BLOCKED状态,因为thread2想拿得到sync()的锁却拿不到
        System.out.println(thread2.getState());
        try {
            Thread.sleep(1300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印出WAITING状态,因为执行了wait()
        System.out.println(thread1.getState());
    }
 
    @Override
    public void run() {
        syn();
    }
 
    private synchronized void syn() {
        try {
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//输出结果
TIMED_WAITING
BLOCKED
WAITING

4.4 阻塞状态是什么?

  • 一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待)都称为阻塞状态
  • 不仅仅是Blocked

4.5 常见面试问题

4.5.1 线程有哪几种状态?生命周期是什么?

如 4.2 所示。

4.5.2 特殊情况

  • 如果发生异常,可以直接跳到终止TERMINATED状态,不必再遵循路径,比如可以从WAITING直接到TERMINATED
  • Object.wait()刚被唤醒时,通常不能立刻抢到monitor锁,那就会从WAITING先进入BLOCKED状态,抢到锁后再转换到RUNNABLE状态

参考资料

Java并发编程知识体系
Java多线程编程核心技术
Java并发编程的艺术
Java并发实现原理 JDK源码剖析