C/C++教程

JUC并发编程与源码分析(2)

本文主要是介绍JUC并发编程与源码分析(2),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

JUC并发编程与源码分析

  • 一、Java的锁
    • 1.1 乐观锁和悲观锁
      • 1.1.1 悲观锁
      • 1.1.2 乐观锁
    • 1.2 通过8种情况演示锁运行案例,看看我们到底锁的是什么
      • 1.2.1 JVM中对应的锁在哪里?
      • 1.2.2 synchronized有三种应用方式
        • 1.2.2.1 JDK源码(notify方法)说明举例
        • 1.2.2.2 8种锁的案例实际体现在3个地方
      • 1.2.3 从字节码角度分析synchronized实现
        • synchronized同步代码块
          • 一定是一个enter两个exit吗?
        • synchronized普通同步方法
        • synchronized静态同步方法
      • 1.2.4 反编译synchronized锁的是什么
        • 1.2.4.1 什么是管程monitor
          • 为什么任何一个对象都可以成为锁?
    • 1.3 公平锁和非公平锁
      • 从ReentrantLock卖票编码演示公平和非公平现象
      • 何为公平锁/非公平锁?
        • 源码
        • 面试题
        • 预埋伏AQS
    • 1.4 可重入锁(又名递归锁)
      • "可重入锁"解释
      • 可重入锁种类
        • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
          • Synchronized的重入的实现机理
        • 显式锁(即Lock)也有ReentrantLock这样的可重入锁。
    • 1.5 死锁及排查
      • 1.5.1 产生死锁原因
      • 1.5.2 死锁case
      • 1.5.3 如何排查死锁
  • 二、LockSupport与线程中断
    • 2.1 线程中断机制
      • 2.1.1 如何停止、中断一个线程?
      • 2.1.2 什么是中断?
      • 2.1.3 中断相关API方法
        • 2.1.3.1 源码分析
          • 实例方法interrupt()
          • 实例方法isInterrupted()
          • 静态方法interrupted()
        • 2.1.3.2 静态方法interrupted()与实例方法isInterrupted()对比
      • 2.1.4 如何使用中断标志停止线程?
        • 2.1.4.1 方法
          • 通过一个volatile变量实现
          • 通过AtomicBoolean实现
          • 通过Thread类自带的中断api方法实现
        • 2.1.4.2 当前线程的中断标识为true,是不是就立刻停止?
          • 后手案例
          • 小总结
      • 总结
    • 2.2 LockSupport是什么?
    • 2.3 线程等待唤醒机制
      • 2.3.1 3种让线程等待和唤醒的方法
      • 2.3.2 Object类中的wait和notify方法
      • 2.3.3 Condition接口中的await后signal方法实现线程的等待和唤醒
      • 2.3.4 Object和Condition使用的限制条件
      • 2.3.5 LockSupport类中的park等待和unpark唤醒
        • 2.3.5.1 是什么?
        • 2.3.5.2 主要方法
          • API
          • 阻塞park() /park(Object blocker)
          • 唤醒unpark(Thread thread)
        • 2.3.5.3 代码
          • 正常+无锁块要求
          • 之前错误的先唤醒后等待,LockSupport照样支持

一、Java的锁

1.1 乐观锁和悲观锁

1.1.1 悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现类都是悲观锁

  • 适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 显式的锁定之后再操作同步资源

1.1.2 乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

  • 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
  • 乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
  • 乐观锁一般有两种实现方式:
    • 采用版本号机制
    • CAS(Compare-and-Swap,即比较并替换)算法实现

1.2 通过8种情况演示锁运行案例,看看我们到底锁的是什么

class Phone {//资源类
    public static synchronized void sendEmail() {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

/**
 * @auther zzyy
 * @create 2021-03-03 15:00
   题目:谈谈你对多线程锁的理解。

 * 线程   操作  资源类  8锁案例说明
 * 1    标准访问有ab两个线程,请问先打印邮件还是短信
 * 2    sendEmail方法暂停3秒钟,请问先打印邮件还是短信
 * 3    新增一个普通的hello方法,请问先打印邮件还是hello
 * 4    有两部手机,请问先打印邮件还是短信
 * 5    两个静态同步方法,同1部手机,请问先打印邮件还是短信
 * 6    两个静态同步方法, 2部手机,请问先打印邮件还是短信
 * 7    1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
 * 8    1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
 *
 * *  1-2
 *  *  *  一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
 *  *  *  其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
 *  *  *  锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
 *  3-4
 *  *  加个普通方法后发现和同步锁无关,hello
 *  *  换成两个对象后,不是同一把锁了,情况立刻变化。
 *
 *   *
 *  *  5-6 都换成静态同步方法后,情况又变化
 *  *  三种 synchronized 锁的内容有一些差别:
 *  * 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
 *  * 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
 *  * 对于同步方法块,锁的是 synchronized 括号内的对象
 *
 *   *  7-8
 *  *    当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
 *  *  *
 *  *  *  所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
 *  *  *  也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
 *  *  *
 *  *  *  所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
 *  *  *  具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
 *  *  *  但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
 *
 */
public class Lock8Demo {
    public static void main(String[] args) {//一切程序的入口,主线程
        Phone phone = new Phone();//资源类1
        Phone phone2 = new Phone();//资源类2

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        },"b").start();

    }
}

/**
 *
 * ============================================
 *  1-2
 *  *  一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
 *  *  其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
 *  *  锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
 *
 *  3-4
 *  *  加个普通方法后发现和同步锁无关
 *  *  换成两个对象后,不是同一把锁了,情况立刻变化。
 *
 *  5-6 都换成静态同步方法后,情况又变化
 *  三种 synchronized 锁的内容有一些差别:
 * 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
 * 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
 * 对于同步方法块,锁的是 synchronized 括号内的对象
 *
 *  7-8
 *    当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
 *  *
 *  *  所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
 *  *  也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
 *  *
 *  *  所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
 *  *  具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
 *  *  但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
 **/

在这里插入图片描述

1.2.1 JVM中对应的锁在哪里?

在这里插入图片描述

1.2.2 synchronized有三种应用方式

1.2.2.1 JDK源码(notify方法)说明举例

在这里插入图片描述

1.2.2.2 8种锁的案例实际体现在3个地方

  • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
  • 作用于代码块,对括号里配置的对象加锁。
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

1.2.3 从字节码角度分析synchronized实现

  • javap -c *.class文件反编译
  • javap -v *.class文件反编译(输出附加信息(包括行号,本地变量表,反汇编等详细信息))

synchronized同步代码块

public class LockByteCodeDemo {
    final Object object = new Object();



    public void m1() {
        synchronized (object){
            System.out.println("----------hello sync");
            //throw new RuntimeException("----ex");
        }
    }

    /*public synchronized void m2()
    {

    }*/

    public static synchronized void m2() {

    }
}

反编译
在这里插入图片描述

一定是一个enter两个exit吗?

不一定
在这里插入图片描述
在这里插入图片描述

synchronized普通同步方法

  • 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。
    如果设置了,执行线程会将先持有monitor然后再执行方法,
    最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

在这里插入图片描述
在这里插入图片描述

synchronized静态同步方法

  • ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

在这里插入图片描述
在这里插入图片描述

1.2.4 反编译synchronized锁的是什么

1.2.4.1 什么是管程monitor

管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
在这里插入图片描述

为什么任何一个对象都可以成为锁?
  • 因为所有对象都继承于Object
  • 在HotSpot虚拟机中,monitor采用ObjectMonitor实现
  • 每个对象天生都带着一个对象监视器

C++源码

  • ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
  • objectMonitor.hpp
    在这里插入图片描述
    在这里插入图片描述

1.3 公平锁和非公平锁

从ReentrantLock卖票编码演示公平和非公平现象

class Ticket {
    private int number = 50;

    //private Lock lock = new ReentrantLock(true); //默认用的是非公平锁,分配的平均一点,=--》公平一点
    private Lock lock = new ReentrantLock(false); //默认用的是非公平锁,分配的平均一点,=--》公平一点
    public void sale() {
        lock.lock();
        try {
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);
            }
        }finally {
            lock.unlock();
        }
    }



    /*Object objectLock = new Object();

    public void sale() {
        synchronized (objectLock)
        {
            if(number > 0)
            {
                System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);
            }
        }
    }*/
}

public class SaleTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() -> { for (int i = 1; i <=55; i++) {
            ticket.sale();
        }
        },"a").start();
        new Thread(() -> { for (int i = 1; i <=55; i++) {
            ticket.sale();
        }
        },"b").start();
        new Thread(() -> { for (int i = 1; i <=55; i++) {
            ticket.sale();
        }
        },"c").start();
        new Thread(() -> { for (int i = 1; i <=55; i++) {
            ticket.sale();
        }
        },"d").start();
        new Thread(() -> { for (int i = 1; i <=55; i++) {
            ticket.sale();
        }
        },"e").start();
    }
}

非公平锁:
在这里插入图片描述
在这里插入图片描述
公平锁:
在这里插入图片描述
在这里插入图片描述

何为公平锁/非公平锁?

⽣活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平

源码

  • 按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;
  • 先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以
    在这里插入图片描述
    在这里插入图片描述

面试题

  1. 为什么会有公平锁/非公平锁的设计为什么默认非公平?
  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间

  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销

  1. 使用公平锁会有什么问题?
  • 公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”
  1. 什么时候用公平?什么时候用非公平?
  • 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

预埋伏AQS

在这里插入图片描述
在这里插入图片描述

1.4 可重入锁(又名递归锁)

  • 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

"可重入锁"解释

可: 可以
重: 再次
入: 进入
锁: 同步锁。
进入什么:

  • 进入同步域(即同步代码块/方法或显式锁锁定的代码)

一句话:

  • 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
  • 自己可以获取自己的内部锁

可重入锁种类

隐式锁(即synchronized关键字使用的锁)默认是可重入锁

同步代码块:

public class ReEntryLockDemo{
    public static void main(String[] args) {
        final Object objectLockA = new Object();

        new Thread(() -> {
            synchronized (objectLockA) {
                System.out.println("-----外层调用");
                synchronized (objectLockA) {
                    System.out.println("-----中层调用");
                    synchronized (objectLockA) {
                        System.out.println("-----内层调用");
                    }
                }
            }
        },"a").start();
    }
}
 

同步方法:

/**
 * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远				       
 * 可以得到锁的
 */
public class ReEntryLockDemo{
    public synchronized void m1() {
        System.out.println("-----m1");
        m2();
    }
    public synchronized void m2() {
        System.out.println("-----m2");
        m3();
    }
    public synchronized void m3() {
        System.out.println("-----m3");
    }

    public static void main(String[] args) {
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

        reEntryLockDemo.m1();
    }
}
Synchronized的重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁(即Lock)也有ReentrantLock这样的可重入锁。

/**
 * 指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用并且不发生死锁,这样的锁就叫做可重入锁。
 * 简单的来说就是:
 * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class ReEntryLockDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"\t"+"-----外层");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+"\t"+"-----内层");
                }finally {
                    //lock.unlock();
                }
            }finally {
                lock.unlock();
            }
        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("------22222");
            }finally {
                lock.unlock();
            }
        },"t2").start();
    }
}

1.5 死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
在这里插入图片描述

1.5.1 产生死锁原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

1.5.2 死锁case

public class DeadLockDemo {
    static Object lockA = new Object();
    static Object lockB = new Object();


    public static void main(String[] args) {

        Thread a = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
                }
            }
        }, "a");
        a.start();

        new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁");

                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

                synchronized (lockA)
                {
                    System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功");
                }
            }
        },"b").start();


    }
}

1.5.3 如何排查死锁

纯命令:

  • jps -l
  • jstack 进程编号
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

图形化:

  • jconsole
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

二、LockSupport与线程中断

2.1 线程中断机制

2.1.1 如何停止、中断一个线程?

在这里插入图片描述

2.1.2 什么是中断?

首先
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。

其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的机制——中断。

中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。

每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

2.1.3 中断相关API方法

在这里插入图片描述
在这里插入图片描述

2.1.3.1 源码分析

实例方法interrupt()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实例方法isInterrupted()

在这里插入图片描述

静态方法interrupted()
public class InterruptDemo
{

    public static void main(String[] args) throws InterruptedException
    {
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println("111111");
        Thread.currentThread().interrupt();
        System.out.println("222222");
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
    }
}

在这里插入图片描述

2.1.3.2 静态方法interrupted()与实例方法isInterrupted()对比

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
方法的注释也清晰的表达了“中断状态将会根据传入的ClearInterrupted参数值确定是否重置”。

所以,
静态方法interrupted将 会清除中断状态(传入的参数ClearInterrupted为true),

实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)。

2.1.4 如何使用中断标志停止线程?

  • 在需要中断的线程中不断监听中断状态
    一旦发生中断,就执行相应的中断处理业务逻辑。

2.1.4.1 方法

通过一个volatile变量实现
public class InterruptDemo
{
private static volatile boolean isStop = false;

public static void main(String[] args)
{
    new Thread(() -> {
        while(true)
        {
            if(isStop)
            {
                System.out.println(Thread.currentThread().getName()+"线程------isStop = true,自己退出了");
                break;
            }
            System.out.println("-------hello interrupt");
        }
    },"t1").start();

    //暂停几秒钟线程
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    isStop = true;
}

}
 
通过AtomicBoolean实现
public class StopThreadDemo
{
    private final static AtomicBoolean atomicBoolean = new AtomicBoolean(true);

    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            while(atomicBoolean.get())
            {
                try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("-----hello");
            }
        }, "t1");
        t1.start();

        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

        atomicBoolean.set(false);
    }
}
 
通过Thread类自带的中断api方法实现
public class InterruptDemo
{
    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            while(true)
            {
                if(Thread.currentThread().isInterrupted())
                {
                    System.out.println("-----t1 线程被中断了,break,程序结束");
                    break;
                }
                System.out.println("-----hello");
            }
        }, "t1");
        t1.start();

        System.out.println("**************"+t1.isInterrupted());
        //暂停5毫秒
        try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        t1.interrupt();
        System.out.println("**************"+t1.isInterrupted());
    }
}
 

2.1.4.2 当前线程的中断标识为true,是不是就立刻停止?

>具体来说,当对一个线程,调用 interrupt() 时:

① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。
被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,
那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

后手案例

在这里插入图片描述

结论:
在这里插入图片描述

小总结
  • 中断只是一种协同机制,修改中断标识位仅此而已,不是立刻stop打断

总结

线程中断相关的方法:

interrupt()方法是一个实例方法
它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。

sInterrupted()方法也是一个实例方法i
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志

Thread类的静态方法interrupted()
返回当前线程的中断状态(boolean类型)且将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false

2.2 LockSupport是什么?

在这里插入图片描述
在这里插入图片描述
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

下面这句话,后面详细说
LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程

2.3 线程等待唤醒机制

2.3.1 3种让线程等待和唤醒的方法

  • 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  • 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  • 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

2.3.2 Object类中的wait和notify方法

  • wait和notify方法必须要在同步块或者方法里面,且成对出现使用
  • 先wait后notify才OK

2.3.3 Condition接口中的await后signal方法实现线程的等待和唤醒

  • Condtion中的线程等待和唤醒方法之前,需要先获取锁
  • 一定要先await后signal,不要反了

2.3.4 Object和Condition使用的限制条件

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

2.3.5 LockSupport类中的park等待和unpark唤醒

2.3.5.1 是什么?

  • 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

在这里插入图片描述
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
permit只有两个值1和零,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。

2.3.5.2 主要方法

API

在这里插入图片描述

阻塞park() /park(Object blocker)
  • 阻塞当前线程/阻塞传入的具体线程

调用LockSupport.park()时
在这里插入图片描述
permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,
然后会将permit再次设置为零并返回。

唤醒unpark(Thread thread)
  • 唤醒处于阻塞状态的指定线程

在这里插入图片描述
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。

2.3.5.3 代码

正常+无锁块要求
public class LockSupportDemo3
{
    public static void main(String[] args)
    {
        //正常使用+不需要锁块
Thread t1 = new Thread(() -> {
    System.out.println(Thread.currentThread().getName()+" "+"1111111111111");
    LockSupport.park();
    System.out.println(Thread.currentThread().getName()+" "+"2222222222222------end被唤醒");
},"t1");
t1.start();

//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"   -----LockSupport.unparrk() invoked over");

    }
}
 
之前错误的先唤醒后等待,LockSupport照样支持
public class T1
{
    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---被叫醒");
        },"t1");
        t1.start();

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---unpark over");
    }
}
 
 

在这里插入图片描述

这篇关于JUC并发编程与源码分析(2)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!