多线程基础知识

本节内容:

线程的状态

wait/notify/notifyAll/sleep方法的介绍

如何正确停止线程

有哪些实现生产者消费者的方法

<span id="jump1">线程的状态/span>

线程一共有六种状态,分别是New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(等待)、Timed WaitIng(计时等待)、Terminated(终结)

状态流转图

image

NEW(新建)

当我们new一个新线程的时候,如果还未调用start()方法,则该线程的状态就是NEW,而一旦调用了start()方法,它就会从NEW变成Runnable

Runnable(可运行)

java中的可运行状态分为两种,一种是可运行,一种是运行中,如果当前线程调用了start()方法之后,还未获取CPU时间片,此时该线程处于可运行状态,等待被分配CPU资源,如果获得CPU资源后,该线程就是运行状态。

Blocked(阻塞)

java中的阻塞也分三种状态:Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待),这三种状态统称为阻塞状态。

  • Blocked状态(被阻塞):从结合图中可以看出从Runnable状态进入Blocked状态只有进入synchronized保护的代码时,没有获取到锁monitor锁,就会处于Blocked状态

  • Time Waiting(计时等待):Time Waiting和Waiting状态的区别是有没有时间的限制,一下情况会进入Time Waiting:

  • 设置了时间参数的Thread.sleep(long millis)

  • 设置了时间参数的Object.wait(long timeout)

  • 设置了时间参数的Thread.join(long millis)

  • 设置了时间参数的LockSupport.parkNanos(long millis)和LockSupport.parkUntil(long deadline)

  • Waiting状态(等待):线程进入Waiting状态有三种情况,分别是:
  • 没有设置Timeout的Object.wait()方法

  • 没有设置Timeout的Thread.join()方法

  • LockSupport.park()方法

Blocked状态仅仅针对synchronized monitor锁,如果获取的锁是ReentrantLock等锁时,线程没有抢到锁就会进入Waiting状态,因为本质上它执行的是LockSupport.park()方法,所以会进入Waiting方法,同样Object.wait()、Thread.join()也会让线程进入waiting状态。Blocked和Waiting不同的是blocked等待其他线程释放monitor锁,而Waiting则是等待某个条件,类似join线程执行完毕或者notify()\notifyAll()。

上图中可以看出处于Waiting、Time Waiting的线程调用notify()或者notifyAll()方法后,并不会进入Runnable状态而是进入Blocked状态,因为唤醒处于Waiting、Time Waiting状态的线程的线程在调用notify()或者notifyAll()时候,必须持有该monitor锁,所以处于Waiting、Time Waiting状态的线程被唤醒后,就会进入Blocked状态,直到执行了notify()\notifyAll()的线程释放了锁,被唤醒的线程才可以去抢夺这把锁,如果抢到了就从Blocked状态转换到Runnable状态

****Terminated(终结)****

进入这个状态的线程分两种情况:

  1. run()方法执行完毕,正常退出

  2. 发生异常,终止了run()方法。

<span id="jump2">wait/notify/notifyAll方法的使用</span>

首先wait方法必须在sychronized保护的同步代码中使用,在wait方法的源码注释中就有说:

在使用wait方法是必须把wait方法写在synchronized保护的while代码中,并且始终判断执行条件是否满足,如果满足就继续往下执行,不满足就执行wait方法,而且执行wait方法前,必须先持有对象的synchronized锁.

上面主要是两点:

  1. wait方法要在synchronized同步代码中调用.

  2. wait方法应该总是被调用在一个循环中

我们先分析第一点,结合以下场景分析为什么要这么设计

public class TestDemo {
private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);

public void add(String data){
        storage.add(data);
        notify();
    }

public String remove() throws InterruptedException {
//wait不用synchronized关键字保护,直接调用,
while (storage.isEmpty()){
            wait();
        }
return storage.remove();
    }
}

上述代码是一个简单的基于ArrayBlockingQueue实现的生产者、消费者模式,生产者调用add(String data)方法向storage中添加数据,消费者调用remove()方法从storage中消费数据.

代码中我们可以看到如果wait方法的调用没有用synchronized保护起来,那么就可能发生一下场景情况:

  1. 消费者线程调用remove()方法判断storage是否为空,如果是就调用wait方法,消费者线程进入等待,但是这就可能发生消费者线程调用完storage.isEmpty()方法后就被调度器暂停了,然后还没来得及执行wait方法.

  2. 此时生产者线程开始运行,开始执行了add(data)方法,成功的添加了data数据并且执行了notify()方法,但是因为之前的消费者还没有执行wait方法,所以此时没有线程被唤醒.

  3. 生产者执行完毕后,刚才被调度器暂停的消费者再回来执行wait方法,并且进入了等待,此时storage中已经有数据了.

以上的情况就是线程不安全的,因为wait方法的调用错过了notify方法的唤醒,导致应该被唤醒的线程无法收到notify方法的唤醒.

正是因为wait方法的调用没有被synchronized关键字保护,所以他和while判断不是原子操作,所以就会出现线程安全问题.

我们把以上代码改成如下,就实现了线程安全

public class TestDemo {
private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);

public void add(String data){
synchronized (this){
            storage.add(data);
            notify();
        }
    }

public String remove() throws InterruptedException {
synchronized (this){
while (storage.isEmpty()){
                wait();
            }
return storage.remove();
        }
    }
}

我们再来分析第二点wait方法应该总是被调用在一个循环中?

之所以将wait方法放到循环中是为了防止线程“虚假唤醒“(spurious wakeup),线程可能在没有被notify/notyfiAll,也没有被中断或者超时的情况下被唤醒,虽然这种概率发生非常小,但是为了保证发生虚假唤醒的正确性,所以需要采用循环结构,这样即便线程被虚假唤醒了,也会再次检查while的条件是否满足,不满足就调用wait方法等待.

为什么wait/notify/notifyAll被定义在Object类中

java中每个对象都是一个内置锁,都持有一把称为monitor监视器的锁,这就要求在对象头中有一个用来保存锁信息的位置.这个锁是对象级别的而非线程级别的,wait/notify/notifyAll也都是锁级别的操作,它们的锁属于对象,所以把它们定义在Object中最合适.

wait/notify和sleep方法的异同

相同点:

  1. 它们都可以让线程阻塞

  2. 它们都可以响应interrupt中断:在等待过程中如果收到中断信号,都可以进行响应并抛出InterruptedException异常

不同点:

  1. wait方法必须在synchronized同步代码中调用,sleep方法没有这个要求

  2. 调用sleep不会释放monitor锁,调用wait方法就释放monitor锁

  3. sleep要求等待一段时间后会自动恢复,但是wait方法没有设置超时时间的话会一直等待,直到被中断或者被唤醒,否则不能主动恢复

  4. wait/notify是Object方法,sleep是Thread的方法

<span id="jump3">如何正确停止线程</span>

正确的停止线程方式是通过使用interrupt方法,interrupt方法仅仅起到了通知需要被中断的线程的作用,被中断的线程有完全的自主权,它可以立刻停止,也可以执行一段时间再停止,或者压根不停止.这是因为java希望程序之间能互相通知、协作的完成任务.

interrupt()方法的使用

public class InterruptDemo implements Runnable{

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptDemo());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }

    @Override
    public void run() {
        int i =0;
        while (!Thread.currentThread().isInterrupted() && i<1000){
            System.out.println(i++);
        }
    }
}

上图中通过循环打印0~999,但是实际运行并不会打印到999,因为在线程打印到999之前,我们对线程调用了interrupt方法使其中断了,然后根据while中的判断条件,方法提前终止,运行结果如下:

image

其中如果是通过sleep、wait方法使线程陷入休眠,处于休眠期间的线程如果被中断是可以感受到中断信号的,并且会抛出一个InterruptException异常,同时清除中断信号,将中断标记位设置为false.

<span id="jump3">有哪些实现生产者消费者的方法</span>

生产者消费者模式是程序设计中常见的一种设计模式,我们通过下图来理解生产者消费者模式:

使用BolckingQueue实现生产者消费者模式

通过利用阻塞队列ArrayBlockingQueue实现一个简单的生产者消费者模式,创建两个线程用来生产对象,两个线程用来消费对象,如果ArrayBlockingQueue满了,那么生产者就会阻塞,如果ArrayBlockingQueue为空,那么消费者线程就会阻塞.线程的阻塞和唤醒都是通过ArrayBlockingQueue来完成的.

public void MyBlockingQueue1(){
        BlockingQueue<Object> queue=new ArrayBlockingQueue<>(10);
        Runnable producer = () ->{
            while (true){
                try {
                    queue.put(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(producer).start();
        new Thread(producer).start();

        Runnable consumer = () ->{
            while (true){
                try {
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(consumer).start();
        new Thread(consumer).start();
    }

使用Condition实现生产者消费者模式

如下代码其实也是类似ArrayBlockingQueue内部的实现原理.

如下代码所示,定义了一个队列容量是16的的queue,用来存放数据,定义一个ReentrantLock类型的锁,并在Lock锁的基础上创建了两个Condition,一个是notEmpty一个是notFull,分别代表队列没有空和没有满的条件,然后就是put和take方法.

put方法中,因为是多线程访问环境,所以先上锁,然后在while条件中判断queue中是否已经满了,如果满了,则调用notFull的await()方法阻塞生产者并释放Lock锁,如果没有满则往队列中放入数据,并且调用notEmpty.singleAll()方法唤醒所有的消费者线程,最后在finally中释放锁.

同理take方法和put方法类似,同样是先上锁,在判断while条件是否满足,然后执行对应的操作,最后在finally中释放锁.

public class MyBlockingQueue2 {
    private Queue queue;
    private int max;
    private ReentrantLock lock=new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull =lock.newCondition();

    public MyBlockingQueue2(int size){
        this.max =size;
        queue = new LinkedList();
    }

    public void put(Object o) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == max) {
                notFull.await();
            }
            queue.add(o);
            //唤醒所有的消费者
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException{
        lock.lock();
        try {
        //这里不能改用if判断,因为生产者唤醒了所有的消费者,
        //消费者唤醒后,必须在进行一次条件判断
            while (queue.size() == 0) {
                notEmpty.await();
            }
            Object remove = queue.remove();
            //唤醒所有的生产者
            notFull.signalAll();
            return remove;
        }finally {
            lock.unlock();
        }
    }
}

使用wait/notify实现生产者消费者模式

如下代码所示,利用wait/notify实现生产者消费者模式主要是在put和take方法上加了synchronized锁,并且在各自的while方法中进行条件判断


public class MyBlockingQueue3 {
    private int max;
    private Queue<Object> queue;

    public MyBlockingQueue3(int size){
        this.max =size;
        this.queue=new LinkedList<>();
    }

    public synchronized void put(Object o) throws InterruptedException {
        while(queue.size() == max){
            wait();
        }
        queue.add(o);
        notifyAll();
    }

    public synchronized Object take() throws InterruptedException {
        while (queue.size() == 0){
            wait();
        }
        Object remove = queue.remove();
        notifyAll();
        return remove;
    }
}

以上就是三种实现生产者消费者模式的方式,第一种比较简单直接利用ArrayBlockingQueue内部的特征完成生产者消费者模式的实现场景,第二种是第一种背后的实现原理,第三种利用synchronzied实现.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容

  • 一、线程基础 1.1线程的实现方法 (1)继承Thread 类(2)实现Runnable接口:Thread t1=...
    黑色叉腰魔头阅读 247评论 0 1
  • 创建、启动、控制、多线程同步、线程池 进程和线程 进程:是处于运行过程的程序,有一定的独立功能,是系统进行资源分配...
    长远勿见阅读 301评论 0 0
  • 什么是线程池?为什么要使用线程池? 将线程池化,需要运行任务时就从里面拿出来一个,不需要了就放回去,不需要每次都n...
    闫回阅读 396评论 0 1
  • 线程概述 线程与进程 进程  每个运行中的任务(通常是程序)就是一个进程。当一个程序进入内存运行时,即变成了一个进...
    闽越布衣阅读 1,009评论 1 7
  • 一、线程基本概念 1. 线程的五种状态 新建状态(new): 线程对象被创建后,就进入了新建状态。例如,Threa...
    Lynn_R01612x2阅读 435评论 0 1