不得不说的Java线程中的那些事

生命啊,他璀灿如歌,代码啊,他bug贼多!

本文主要对以前学过的内容的回顾。主要有以下内容:

  • 线程的创建方式
  • 线程的状态
  • Thread类和Object类
  • synchronizedvolatile
  • 双重检查单例模式
  • 生产者消费者的实现

线程的创建方式

线程的创建方式的这个问题的答案,网上回答各种各样,不信的话现在打开百度搜索一下,就能看到了。如何在这么多的信息中找到正确的答案?那首先考虑的就是官方答案咯,Java的官方文档给出了如下信息:

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread.

The other way to create a thread is to declare a class that implements the Runnable interface.

即线程的创建方式:有两种,分别是继承Thread类和实现Runnable接口。

public class CreateThreadWay {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread());
        Thread thread1 = new Thread(new MyThread2());
        thread.start();
        thread1.start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("方式一: 通过继承的方式创建线程");
    }
}
class MyThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("方式2:通过实现Runnable#run()创建线程");
    }
}
复制代码

运行结果如下:

create_thread.png

这就是创建线程的两种方式,是不是很简单?但有一下几点需要注意:

其一:上面的代码都创建了一个相应实现类的实例传递给Thread的构造器方法,所以稍微看一下Thread类构造器方法

public Thread(Runnable target) {
    this(null, target, "Thread-" + nextThreadNum(), 0);
}
复制代码

可以看见,需要传入一个Runnable接口的实现类对象,但是方式一明明传入是一个Thread实例,他怎么没出现问题勒?那是因为Thread类本身就实现了Runnable接口,Thread本身就是Runnable的形状了,惊不惊喜,意不意外?

public class Thread implements Runnable
复制代码

其二:不管是方式一还是方式二,我们都是重写了run方法,但是在创建线程的时候明明调用的是start()方法,但run方法的逻辑却得到了执行!这是为何?

源码是最好的答案, 在Thread#start()方法上有这么一段注释:

Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread. The result is that two threads are running concurrently: the current thread (which returns from the call to the start method) and the other thread (which executes its run method). It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.

也就是说调用了start()方法后,Java 虚拟机会主动调用线程的当前线程的run方法,而run方法的源码如下:

public void run() {
    if (target != null) {
        target.run();
    }
}
复制代码

这个target 是不是好生熟悉?你一定在哪里见过它是吧?target就是我们传入的Runnable实例。

其三:其他创建线程的方式如lambda表达式,线程池之类的都是基于上述两种的变种写法。因此本质上是一样的!

其四:Java是一种单继承语言,不支持多个父类,因此在创建线程方式的选择中,优先考虑实现接口的方式,继承也是一种资源!!

线程的状态

前面讨论了如何创建线程,接下来就应该谈谈线程状态转换。在Java中,Java的设计者规定了线程有如下几种状态

  • NEW: 线程创建但没有调用start()方法

  • RUNNABLE:可运行的。处于该状态只是表示线程处于可运行的状态,而不是处于运行状态。

    • A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processo
  • BLOCKED:线程处于阻塞状态,进入该状态只有两种方式,一是线程在进入synchronized修饰的带块时,没有获取到monitor, 第二是线程获取到monitor之后,在同步代码块中调用了object.wait()方法之后,再次进入同步代码块。

    • Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait.
  • WAITING: 线程进入等待状态 Object.wait()、Thread.join()

  • TIME_WAITING: 计时等待线程调用Thread.sleep()、Object.wait(timeout)、等方法时进入

  • TERMINATED:线程运行结束

下面的代码打印了线程的各个状态:

public class ThreadStateTest {
​
    public static Object lock = new Object();
    public static Object lock2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadState());
        Thread thread2 = new Thread(new ThreadState2());
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]"); // NEW
        thread2.start();
        thread.start();
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]");
        Thread.sleep(1000);
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]");
        Thread.sleep(3000);
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]");
        Thread.sleep(3000);
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]");
        synchronized (lock2){
            System.out.println("main-thread will release the lock2 monitor");
            lock2.notifyAll();
        }
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]");
        Thread.sleep(1000);
        System.out.println("main-thread print [thread state = " + thread.getState()+" ]");
    }
    static class ThreadState implements Runnable {
        @Override
        public void run() {
            System.out.println("ThreadState 线程得到运行 runnable");
            try {
                Thread.sleep(3000);
                showBlockState();
                synchronized (lock2) {
                    System.out.println("ThreadState 调用 wait() 释放锁资源");
                    lock2.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public void showBlockState() {
            synchronized (lock) {
                System.out.println("ThreadState get the monitor lock");
            }
        }
    }
    static class ThreadState2 implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
​
            }
        }
    }
}
​
复制代码

运行结果如下

thread_state.png

解释一下上面的代码

  • 线程创建后没调用start()方法,线程处于NEW状态
  • 在线程执行start()后,状态变为RUNNABLEe
  • 由于t1会sleep(3000),导致lock1t2获取且占据五秒后释放
  • 在主线程sleep(1000)后,此时t1sleep时间还没有结束故处于time_waiting状态
  • 在主线程sleep(3000)后,t1sleep结束,但t2还有1s才会释放monitor (5-1-3=1s),此时获取不到monitor,因此处于BLOCKED状态
  • 在主线程第二个sleep(3000)期间,t2会在这3s执行到第1s后,释放掉monitor,t1在这期间依次获得lock1、lock2并在获得lock2后执行lock2.wait释放掉monitor,导致线程进入waiting状态。
  • 主线程在t1释放掉lock2后得到lock2,执行lock2.notifyAll()唤醒t1,t1开始去争夺lock2,但不一定成功,因此如果没有得到lock2则处于BLOCKED状态
  • 主线程sleep(1000)此时t1已经得到lock2并执行完后面的逻辑

整个过程如下图所示

thread_state_drawio.png

上面的代码演示了线程的状态是如何变化,接下来用更加完善的图来展示线程的状态之间的相互转换

transfer_six_thread_state_drawio.png

其中需要注意的是线程调用了start之后,从new--->terminate这一过程是不可逆的。

其次上面涉及到的关键字synchronized和各个api将马上予以说明

Object类和Thread类

JDK中提供了对线程进行控制的API,本文主要涉及到Object、Thread中的方法,其他的在后面的文章演示使用。

Object类中提供了如下方法对线程的控制

  • wait()/wait(timeout):使线程释放掉获取的monitor,当不带参数时,线程进入waiting状态,处于waiting状态时,没获取到monitor将处于BLOCKED状态,当使用带有timeout的方法时,线程将处于time_wating状态,等timeout结束后重新去争夺monitor,如果没拿到还是会处于BLOCKED状态。拿到之后处于RUNNABLE状态
  • notify/notifyAll:前者随机唤醒一个等待当前monitor的线程,后者唤醒全部等待当前monitor的线程。

注意:wait()、notify()、notifyAll()并不能随便调用,需要拿到当前monitor之后才能调用,即需要处于synchronized修饰的代码块。如在演示线程状态变化的代码中的lock2,

Thread类中也提供了很多的方法进行控制,比如设置线程的相关属性的API,如线程名称,线程是否为守护线程等,但本文内容主要包含下列API。

  • threadInstance.interrupt(): 通知实例线程停止运行,将中断标志位设为true
  • threadInstance.isInterruptd():判断当前线程是否停止,停止返回true,反之false
  • Thread.interrupted() : 这是一个静态方法,返回当前线程是否停止,并清除当前线程中断标志位状态,置为false
  • Thread.sleep(timeout): 这也是一个静态方法,让前线程休眠指定的时间,时间过后将会继续运行,不会释放锁资源。在休眠期间被interrupt掉,将会抛出一个InterruptedException异常,在抛出异常后会将中断标志置为false
  • threadInstance.join()/join(timeout):前者表示无线等待,后者只等待timeout时间后,便执行
  • threadInstance.yeild(): 当前线程让出cpu,让cpu去执行其他线程,但不一定会成功,因为当前线程还会继续资源的争夺。

Java的设计者,在设计线程停止运行这一方面,舍弃了原先的stop()方法,该方法会强制线程停止,过于粗暴,在1.2标记为过时。取而代之的是采用interrupt()通知线程该停止运行了,设置中断标志为true,让线程自己决定自己该何时停止。

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () ->{
        System.out.println("runnable 即将使用 Thread.sleep() ");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            System.out.println("in exception: "+Thread.currentThread().getName() + " interrupt1 =  " + Thread.interrupted());
            Thread.currentThread().interrupt();
            System.out.println("in exception: "+Thread.currentThread().getName() + " interrupt2 =  " + Thread.interrupted());
            System.out.println("in exception: "+Thread.currentThread().getName() + " interrupt3 =  " + Thread.interrupted());
            Thread.currentThread().interrupt();
            for (int i = 0; i < 5; i++) {
                System.out.print(i);
            }
            System.out.println();
        }
    };
    Thread thread = new Thread(runnable,"runnable");
    thread.start();
    System.out.println("1. in main: "+thread.isInterrupted());
    thread.interrupt();
    System.out.println("2. in main: "+thread.isInterrupted());
    thread.join();
    System.out.println("3. in main: " + thread.interrupted());
}
复制代码

首先先对上面的代码进行简要说明

  • 首先创建了一个名称为runnable的线程
  • 然后调用thread.interrupt通知线程中断
  • 然后进行了一些打印

上面的代码的运行结果如下图

thread_interrupted.png

首先需要对上面的运行结果进行说明

  • 3.in mian打印在输出01234使用在主线程中调用了thread.join,该方法是让主线程阻塞,直到thread运行完毕后,在继续运行
  • 2. in main【图中第一个红色框处】打印为true是因为调用了thread.interrpt()
  • in exception:interrupt1打印false是因为sleep方法响应了InterruptedException清除掉了中断标志,
  • in exception:interrupt2打印true是因为Thread.currentThread().interrupt()重新设置了标志位
  • in exception:interrupt3打印false使用为Thread.interrupted()返回标志位后,清除了标志位
  • 回到3.in main的打印这里按道理应该是true,因为在runnable线程中的catch块中设置了Thread.currentThread().interrupt(), 然而这里打为false 这是因为interrupted()和调用他对象无关,只和当前运行的线程有关。即 System.out.println("3. in main: " + thread.interrupted());返回的是主线程的中断标志,所以是false

synchronized关键字

在演示线程状态转换的代码中和线程状态转换的图上,都有synchronized关键字的身影,接下来就对该关键字进行说明。

在对synchronized进行说明之前,先来一点小菜:

Java内存模型

Java虚拟机是对物理的PC的抽象,Java内存是对真实电脑内存模型的抽象,不同于真实的电脑内存模型,拥有多级高速缓存,Java内存模型仅设计为主内存加工作内存。

Java中数据都存在于主内存中,工作内存中的数据都是通过load和save操作从主内存中加载数据,或者把工作内存中的数据同步到主内存中,

Java线程对变量的所有操作都必须在工作内存中。

JMM.png

Java内存模型的几个必须了解的概念:

  • 原子性:指一个操作一旦开始就不可中断。即时是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
  • 可见性:指一个线程对一个变量的值进行修改后,其他线程是否可知
  • 有序性:程序在执行时,程序指令有可能发生重排序,即程序执行的顺序不一定和书写顺序相同

原子性和可见性比较好理解,有序性不那么好理解,在看完下面这个例子之后,在进行说明。

普通变量在线程和线程之间是不可见的。Java线程不能和主内存直接打交道,因此就会出现工作内存中的数据没及时得到更新而导致的线程安全问题。

static int count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new PrintTask());
    Thread thread2 = new Thread(new PrintTask());
    thread.start();
    thread2.start();
    thread.join();
    thread2.join();
    System.out.println(count);
}
x
static class PrintTask implements Runnable {
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            count = count + 1;
        }
    }
}
复制代码

上面的代码按照预想的情况count 值为20000;然而实际上均小于20000;

thread_count.png

这是因为两个线程计算的结果被相互覆盖,具体过程如下图所示,此时i的正确结果应该是a+2

err_count.png

线程在得到cpu的调度时,并不会保证他在将i+1的结果刷新之后再去调度其他线程,这是因为在Java中count = count + 1他并不是一个原子操作,没有保证原子性,即使写成count++,这也不是原子操作。那么如何才能得到正确的结果呢,这是就体现了synchronized关键字的作用。

synchronized的用法

  • 指定加锁对象[monitor],在进入同步代码块之前,必须获得指定的对象的锁
  • 作用于实例方法,相当于对当前实例加锁,静茹同步代码块需要获取当前实例的锁
  • 作用于静态方法,相当于对当前类加锁,在进入当前类之前需要获得当前类的锁

synchronized保证了原子性,可见性,以及有序性,但是没有禁止指令重排序的。

synchronized保证了原子性,此时将代码修改为如下即可:只有当count在累加了10000次之后,才释放掉lock让其他线程获取并继续累加。

synchronized (lock){
    for (int j = 0; j < 10000; j++) {
        count = count + 1;
    }
}
复制代码

提到了sychronized关键字,就不得不提他的轻量级实现volatile关键字,在这之前需要先讲述一下指令重排.

发生指令重排的前提:

保证串行语义的一致性,即发生指令重排不会使语义逻辑发生错误。但是没有义务保证多线程语义的一致性

为什么要发生指令重排:

是为了提高代码运行的效率

以病人看病为例说明

resort.png

左边是没有重排序的情况下,一个医生看一个病人需要花费两个小时的时间,下一个病人才能够得到治疗,而进行重排序之后下一个病人在20分钟后就可以得到治疗,看病效率得到了极大的提高。

将一个病人看病的过程当作一个指令的执行过程,则在没有进行指令重排序的情况下第二条指令需要等到第一条指令才能得到运行,进行重排序后,只需要第一条指令完成"挂号"后,就可以得到运行,CPU得到了极大的压榨。CPU:你了不起! 你清高!! 你1080P!

需要说明的是:不是所有指令都可以进行重排序,以下指令则不能重排序

  • 程序顺序性原则:一个线程内保证语义的串行性
  • volatile原则:volatile变量的写先于读发生,这就保证了可见性。[先读再写]
  • 锁规则:解锁操作发生在随后的加锁前,
  • 传递性:a先于b,b先于c,那么a必然先于c
  • 线程的start()方法先于他的每一个动作。
  • 线程的所有操作先于线程的终结Thread::join()
  • 线程的中断先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法

明白了上述的内容,volatile关键字就比较好理解了。volatile关键字保证了内存的可见性,禁止了指令的重排序,没有保证原子性。这就是为什么有时候被volatile修饰的变量还需要synchronized原因了。如双重检查实现单例模式.

private static volatile Object instance;
​
public static Object getInstance() {
    if (instance == null){
        synchronized (DoubleCheckSingleton.class){
            if (instance == null){
                instance = new Object();
            }
        }
    }
    return instance;
}
复制代码

为什么这么写是因为创建对象过程不是原则性的,它分为三步:

  1. 在堆上分配内存
  2. 注入属性
  3. 将对象的引用指向内存分配的地址

如果不使用volatile关键字则23步可能被排序为132, 这样返回的对象是有问题的,属性缺失。

如果只使用volatile关键字可能有并发风险,前后得到的对象不是同一个!

double_check.png

生产者和消费者的实现

了解了Thread、Object中的API,就可以手动实现一个生产者和消费者,首先需要明确的一点是,生产者产生生产数据,存放到仓库,消费者需要从仓库中取出数据去消费。

produce_consumer.png

仓库代码如下,负责向仓库添加逻辑和消费逻辑,存的数据为一个Interger对象

class Storage {
​
    public static final Object lockC = new Object();
    LinkedList<Integer> storage = new LinkedList<>();
    public void take(){
        synchronized (lockC){
            if (storage.isEmpty()){
                lockC.notify();
                try {
                    System.out.println("仓库为空,请生产");
                    lockC.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                System.out.println(Thread.currentThread().getName() + "消费:" + storage.poll() + " 当前还剩下:" + storage.size() +" 个");
                lockC.notify();
            }
        }
    }
    public void put(Integer integer){
        synchronized (lockC){
            if (storage.size() == 20) {
                lockC.notify();
                try {
                    System.out.println("仓库已到达最大生产容量,请消费");
                    lockC.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                storage.add(integer);
                System.out.println(Thread.currentThread().getName() + "生产:" + integer + " 当前仓库还有" + storage.size() +" 个");
                lockC.notify();
            }
        }
​
    }
​
}
复制代码

生产者和消费者线程如下

class Produce implements Runnable{
    private Storage storage;
    public Produce(Storage storage) {
        this.storage = storage;
    }
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            storage.put((int)(Math.random() * 100));
        }
    }
}
class Consumer implements  Runnable{
    private Storage storage;
    public Consumer(Storage storage ) {
        this.storage = storage;
    }
    @Override
    public void run() {
        while (true){
            storage.take();
        }
    }
}
​
public static void main(String[] args) {
    Storage storage = new Storage();
    Thread threadP1 = new Thread(new Produce(storage),"1号生产线");
    Thread threadC1 = new Thread(new Consumer(storage),"消费线1");
    threadP1.start();
    threadC1.start();
}
复制代码

因为消费者消费速度大于生产者的生产速度,所以运行结果如下图所示,总能出现仓库为空的情况。

produce_consumer_code.png

各位看官老爷,如果觉得内容还可以,就点个赞鼓励鼓励。

参考资料

  • 深入理解Java虚拟机
  • Java高并发程序设计
  • Java并发编程实战
  • Java官网

猜你喜欢

转载自juejin.im/post/7216914115259514935