Java并发编程核心:管程Monitor原理与实战全解析

🌈 开篇故事:电影院里的座位争夺战
想象一个火爆的电影首映场,100个座位却有1000人抢票。如果没有管理员协调,会发生什么?

  • 临界区:售票窗口(共享资源)
  • 竞态条件:多人同时抢同一座位导致系统崩溃
  • 管程(Monitor):电影院管理员,保证有序购票

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。
核心概念
临界区Critical Section:一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
例如,下面代码中的临界区

tatic int counter = 0;
static void increment( )
    //临界区
    counter++;
}
static void decrement( )
    //临界区
    counter--;
}

竞态条件Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

1. 共享问题

启用两个线程,一个线程对共享变量做n次自增,另一个线程对共享变量做n次自减,以上的结果可能是正数、负数、零。为什么呢?
因为Java中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析,例如对于i++而言(i 为静态变量),实际会产生如下的JVM字节码指令:

getstatic i // 获取静态变量i的值
iconst_ 1 //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i

而对应i–也是类似:

getstatic i //获取静态变量i的值
iconst_ 1 //准备常量1
isub //自减
putstatic //将修改后的值存入静态变量i

负数情况在这里插入图片描述

正数情况
在这里插入图片描述

2. synchronized解决方案

应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
1.阻塞式的解决方案: synchronized,Lock
2.非阻塞式的解决方案:原子变量

public class TeaTest {
    static int count = 0;
    static Object lock = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            for (int i = 0; i < 500; i++) {
                synchronized (lock){
                    ++count;
                }
            }
        }, "t1").start();

        new Thread(()->{
            for (int i = 0; i < 500; i++) {
                synchronized (lock){
                    --count;
                }
            }
        }, "t1").start();

        System.out.println("count="+count);
    }

本次课使用阻塞式的解决方案: synchronized,来解决上述问题,即俗称的[对象锁],它采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁],其它线程再想获取这个[对象锁]时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。synchronized实际上是使用对象锁保证了临界区代码的原子性

注意
虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后顺序不同、需要一个线程等待其它线程运行到某个点

synchronized语法

synchronized(对象){ // 线程1,线程2(blocked)
    临界区I
}

class Test{
    public synchronized void test( ) {//加在普通方法上,锁的是当前对象this
    }
}

等价于

class Test{
    public void test(){
        synchronized(this) {
        }
    }
}

class Test{
    public synchronized static void test() {//加在静态方法上,锁的是类对象 Test.class
    }
}

等价于

class Test
    public static void test( ) {
        synchronized (Test.class)
    }
  }

3 变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的;
  • 但局部变量引用的对象则未必
  • 如果该对象没有逃离方法的作用范围,它是线程安全的;
  • 如果该对象逃离方法的作用范围,需要考虑线程安全。

局部变量线程安全分析

public static void test1() {
    int i = 10;
    1++;
}

每个线程调用test1(方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下的类
    这里说它们是线程安全的,是指多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为,它们的每个方法是原子的,但注意它们多个方法的组合不是原子的,见后面分析
    在这里插入图片描述

不可变类线程安全性
String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。有同学或许有疑问,String有replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
并非直接修改原来子串,而是重新创建了新的字符串对象

案例:多窗口卖票问题 (售票方法加synchronized,锁定共享对象)

@Slf4j(topic = "TicketSicket")
public class TciketWindowTest {
    public Random random = new Random();
    public int randomCount(){
        return random.nextInt(5) + 1;
    }
    
    static class  TicketWindow{
        private int count;

        public TicketWindow(int count) {
            this.count = count;
        }

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

        public synchronized int sell(int t){
            if (t <= this.count){
                this.count -= t;
                return t;
            }
            return 0;
        }
    }
    
    @Test
    public void testSell() throws InterruptedException {
        TicketWindow window = new TicketWindow(1000);
        List<Integer> amountList = new Vector<>();
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(randomCount());
                    int sell = window.sell(randomCount());
                    amountList.add(sell);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
            threadList.add(thread);
        }

        for (Thread t: threadList) {
            t.join();
        }

        //统计卖出的票数和剩余的票数
        log.info("余票:{} ", window.getCount());
        log.info("售出票:{}", amountList.stream().mapToInt(i->i).sum());
    }

}

4 Monitor(锁) 概念

前置知识:
在这里插入图片描述
一个Java对象是在堆内存中,由对象头(header)、**实例数据(Instance Data)对齐填充(Padding)**三部分组成。

  1. Java对象头:由标记字(Mark Word)类指针(klass word)数组长度组成.。
  • mark word: 用于存储自身的运行数据。
  • Klass word:是指向该对象类元数据(方法区)的指针,JVM通过这个指针确定这个对象是哪个类的实例
  • 数组长度: 如果一个对象是一个数组,那么对象头还需要额外的空间来存储数组的长度。
  1. 实例数据:主要包括对象的各种成员变量(包括基本类型和引用类型),基本类型直接存储内容,引用类型则是存储指针,static类型的变量会放到类中,而不是放到实例数据里
  2. 对齐填充:主要作用是提高CPU内存的访问速度。

对象头(header)
在这里插入图片描述
对象头Mark Word主要用来表示对象的线程锁状态
在这里插入图片描述
先看锁标志位和偏向锁标记位:
最低2位,锁标志位(lock)是表示对象的线程锁状态,其中,正常和偏向锁时都是01,轻量级锁用00表示,重量级锁用10表示,标记了GC的用11表示。由于正常和偏向锁时都是01,因此低3位 偏向锁标记位(biased_lock) 用0或1表示是否时偏向的。
接着我们横着看,

  • 在正常不加锁时,mark word 由biased_lock、age、identity_hashcode组成,age是GC的年龄,最大15(4位),每从Survivor区复制一次,年龄增加1。identity_hashcode就是对象的哈希码,当对象处于加锁状态时,这个哈希码会移到monitor,(synchronized会在代码块前后插入monitor)。
  • 在偏向锁时,mark word 由biased_lock、age、epoch、thread组成。epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。thread:持有偏向锁的线程ID,如果该线程再次访问这个锁的代码块,可以直接访问。
  • 在轻量级锁时,mark word 由ptr_to_lock_record组成。ptr_to_lock_record:指向栈中锁记录的指针
  • 在重量级锁时,mark word 由ptr_to_heavyweight_monitor组成。ptr_to_heavyweight_monitor:指向对象监视器Monitor的指针

Monitor被翻译为监视器管程

  • 每个加synchronized的Java对象都可以关联一个Monitor对象,如果使用synchronized 给对象上锁(重量级)之后,该对象的对象头的Mark Word中就被设置指向Monitor对象的指针
  • Monitor结构如下
    在这里插入图片描述

Monitor由操作系统提供

  • 刚开始Monitor中Owner为null
  • 当Thread-2执行synchronized(obj) 就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一 个Owner
  • 在Thread-2 上锁的过程中,如果Thread-3,Thread-4, Thread-5 也来执行synchronized(obj), 就会进入EntryList BLOCKED
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
  • 图中WaitSet中的Thread-0, Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析
    注意:
  • synchronized必须是进入同一个对象的monitor才有上述的效果
  • 不加synchronized的对象不会关联监视器,不遵从以上规则

synchronized 锁升级
锁升级就是lock状态从正常无锁->偏向锁->轻量级锁->重量级锁的过程

  • 初期锁对象刚创建时,还没有任何线程来竞争,锁状态01,偏向锁标识位是0(无线程竞争它)。
  • 当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。
  • 当有两个线程开始竞争这个锁对象,情况发生变化了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
  • 如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。Monitor依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

synchronized锁分类-(类比自习室学习房门锁)

  • 房间门上- 防盗锁 - Monitor(重量级锁)
  • 房间使用时间基本是错开的。房间门上- 挂小南书包 ,每次进门先翻翻书包看课本是谁的,是自己的才可以进 - 轻量级锁
  • 一个线程使用的概率很大,房间门上-刻上小南大名,每次进门前若名字还在,说明没人打扰,可以进-偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达20阈值
  • 刻名现象太频繁,改为 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

synchronized优化原理

1.偏向锁

  • 轻量级锁在没有竞争时(就自己这个线程), 每次重入仍然需要执行CAS操作。
  • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,Java 6中引入了偏向锁来做进一步优化。
  • 当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的线程ID。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。
    在这里插入图片描述
偏向锁的获取:

1、首先获取锁对象头中的 Mark Word,判断当前对象是否处于可偏向状态(即当前没有对象获得偏向锁)。
2、如果是可偏向状态,则通过CAS原子操作,把当前线程的ID写入到 MarkWord,如果CAS成功,表示获得偏向锁成功,会将偏向锁标记设置为1,且将当前线程的ID写入Mark Word;如果CAS失败则说明当前有其他线程获得了偏向锁,同时也说明当前环境存在锁竞争,这时候就需要将已获得偏向锁的线程中的偏向锁撤销掉(具体参考下面偏向锁的撤销),并升级为轻量级锁。
3、如果当前线程是已偏向状态,需要检查Mark Word中的ThreadID是否和自己相等,如果相等则不需要再次获得锁,可以直接执行同步代码块,如果不相等,说明当前偏向的是其他线程,需要撤销偏向锁并升级到轻量级锁。

撤销偏向锁

偏向锁的撤销,需要等待全局安全点(即在这个时间点上没有正在执行的字节码),然后会暂停拥有偏向锁的线程,并检查持有偏向锁的线程是否活着,主要有以下两种情况:

  • 如果线程不处于活动状态,则将对象头设置成无锁状态。
  • 如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程(重偏向需要满足批量重偏向的条件),要么恢复到无锁或者标记对象不适合作为偏向锁。
    最后唤醒暂停的线程。

调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode
    在调用hashCode后使用偏向锁,记得去掉-Xx: -UseBiasedLocking
    2.撤销-其它线程使用对象
    当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
    3.撤销-调用wait/notify
偏向锁批量重偏向

一个线程创建了大量对象而且执行了同步操作后另一个线程又来将这些对象作为锁对象进行操作,并且达到阈值,此时就会发生偏向锁重偏向的操作(除了这种情况,其他情况只有有线程来竞争锁,则偏向锁状态就结束了)。
-XX:BiasedLockingBulkRebiasThreshold 为重偏向阈值JVM参数,默认20,可以通过**-XX:+PrintFlagsFinal**打印出默认参数,接下来我们通过一个示例来演示一下批量重偏向:

  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
  • 当撤销偏向锁阈值超过20次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

批量撤销

  • 当撤销偏向锁阈值超过40次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

@Benchmark
public void b() throws Exception {
    object o = new object();
    synchronized (o) {
        x++ ;
    }
  }

JIT(即时编译器)发现局部变量o并没有逃离锁的作用范围,会优化掉加锁的代码块
默认开启该优化,可加参数-XX: -EliminateLocks自行关闭

注意:
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101, 这时它的thread、epoch、 age 都为0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 可添加VM参数**-XX:-UseBiasedLocking** 禁用偏向锁;如果没有开启偏向锁,那么对象创建后,markword 值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值

2. 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized
  • 假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
 
 public void method1() throws Exception {
    synchronized (obj) {
        //同步块A
        method2();
    }
  }
  
  public void method2() throws Exception {
    synchronized (obj) {
        //同步块B
    }
  }
轻量级锁加锁

线程在执行同步代码块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

流程示意图

  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
    在这里插入图片描述
  2. 让锁记录中Object refrence指向锁对象,并尝试用cas替换Object中的 Mark Word,将Mark Word的值存入锁记录。
    在这里插入图片描述
  3. 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下
    在这里插入图片描述
  4. 如果cas失败,有两种情况
  • 如果是其他线程已经持有了改Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再加一条Lock Record作为重入的计数
    在这里插入图片描述
  1. 当退出synchronized代码块(解锁)时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入数减一
    在这里插入图片描述
    当退出synchronized代码块(解锁)时锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已升级成为重量级锁,进入重量级锁解锁流程
自旋锁

轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。

为什么要采用自旋等待呢?
因为绝大多数情况下线程获得锁和释放锁的过程都是非常短暂的,自旋一定次数之后极有可能碰到获得锁的线程释放锁,所以,轻量级锁适用于那些同步代码块执行很快的场景,这样,线程原地等待很短的时间就能够获得锁了。
注意:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是 10 次,可以使用**-XX:PreBlockSpin**参数来设置自旋锁等待的次数。

自适应自旋
JDK1.7 开始,引入了自适应自旋锁,修改自旋锁次数的JVM参数被取消,虚拟机不再支持由用户配置自旋锁次数,而是由虚拟机自动调整。自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

重量级锁

锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待唤醒了。每一个对象中都有一个Monitor监视器,而Monitor依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。而且当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。我们可以简单的理解为,在加重量级锁的时候会执行monitorenter指令,解锁时会执行monitorexit指令。

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • Java 7之后不能控制是否开启自旋功能
    自旋重试成功的情况
    在这里插入图片描述
    在这里插入图片描述

5 wait notify

为什么需要wait?

  • 协调线程:在某些情况下,一个线程可能需要等待另一个线程完成特定的操作或满足某个条件。使用 wait() 可以实现这种协调。
  • 避免忙等待:如果线程在等待某个条件时不释放锁,会导致其他线程无法执行,从而造成死锁或资源浪费。wait() 方法解决了这个问题,因为它会释放锁,让其他线程有机会运行。

5.1 原理之wait/notify

  • Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITING 线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争

5.2 API介绍

  • obj.wait() 让进入object监视器的线程到waitSet等待
  • obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
  • obj.notifyAll() 让object上正在waitSet等待的线程全部唤醒
    它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法

5.3 wait notify的正确姿势

sleep(long n)和wait(long n)的区别

  • sleep是Thread方法,而wait是Object的方法
  • sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
  • sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。
  • 它们状态都是TIMED_WAITING
synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}
    
//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

6. 同步模式之保护性暂停

定义:即Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点

  • 有一个结果需要从一个线程传递到另一个线程, 让他们关联同一个GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK中,join的实现、Future的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式
    在这里插入图片描述

7. 异步模式之生产者/消费者

在这里插入图片描述
定义

  • 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程 一一 对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK中各种阻塞队列,采用的就是这种模式
@Slf4j
public class Messagequeue {
    public static void main(String[] args) {
        MsgQueue msgQueue = new MsgQueue(2);
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(()->{
                msgQueue.put(new MsgQueue.Message(finalI, "值"+finalI));
            }, "生产者"+i).start();
        }

        new Thread(()->{
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                MsgQueue.Message take = msgQueue.take();
            }
        }, "消费者").start();
    }
}

// 消息队列类,java线程之间通信
@Slf4j
class MsgQueue{
    private LinkedList<Message> list = new LinkedList<Message>(); //消息的队列集合
    private Integer capcity; //队列容量

    public MsgQueue(Integer capcity) {
        this.capcity = capcity;
    }

    //取走消息
    public Message take(){
        synchronized (list){
            if (list.isEmpty()){
                try {
                    log.info("队列为空,消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = list.removeFirst();
            log.info("已消费消息: {}", message);
            list.notifyAll();
            return message;
        }
    }

    //存入消息
    public void put(Message msg){
        synchronized (list){
            if (list.size() == capcity){
                try {
                    log.info("队列已满,生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将消息加入队列尾部
            list.addLast(msg);
            log.info("已生产消息: {}", msg);
            list.notifyAll();
        }
    }

    static final class Message{
        private Integer id;
        private Object value;
        public Message(Integer id, Object value) {
            this.id = id;
            this.value = value;
        }

        public Integer getId() {
            return id;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "Message{" +
                    "id=" + id +
                    ", value=" + value +
                    '}';
        }
    }
}

8. park & unpark 基本使用

它们是LockSupport类中的方法

  • LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport 中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark (暂停线程对象)

使用案例

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        log.info("start....");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("park....");
        LockSupport.park();
        log.info("resume...");
    }, "t1");
    t.start();
    TimeUnit.SECONDS.sleep(2);
    log.info("unpark....");
    LockSupport.unpark(t);
}

输出

[t1] INFO main.juc.ThreadTest.TestParkUnPark - start....
[t1] INFO main.juc.ThreadTest.TestParkUnPark - park....
[main] INFO main.juc.ThreadTest.TestParkUnPark - unpark....
[t1] INFO main.juc.ThreadTest.TestParkUnPark - resume...

与Object的wait & notify相比

  • wait,notify和 notifyAll必须配合Object Monitor 一起使用,而unpark不必
  • park & unpark 是以线程为单位来[阻塞]和[唤醒]线程,而notify只能随机唤醒一 个等待线程,notifyAll是唤醒所有等待线程,就不那么[精确]
  • park & unpark可以先unpark,而wait & notify不能先notify

原理之park & unpark
每个线程都有自己的一个Parker对象,由三部分组成counter, _cond 和_mutex 打个比喻

  • 线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好比背包中的帐篷。 counter就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用park就是要看需不需要停下来歇息
  • 如果备用干粮耗尽,那么钻进帐篷歇息
  • 如果备用干粮充足,那么不需停留,继续前进
  • 调用unpark,就好比令干粮充足
  • 如果这时线程还在帐篷,就唤醒让他继续前进
  • 如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进
  • 因为背包空间有限,多次调用unpark仅会补充一份备用干粮

9. 重新理解线程状态转换

假设有线程Thread t
情况1 NEW ----> Runnable
调用 t.start()方法

情况2 Runnable ----> WAITING,t 线程用 synchronized(obj) 获取了对象锁后

  • obj.wait
    -obj.notify obj.notifyAll obj.interrupt

情况3 Runnable ----> TIMED_WAITING

  • wait(xx)
  • join(xx)
  • sleep(xx)
  • LocakSupport.parkNanos(xx)

情况4 Runnable ----> BLOCKED
竞争synchronized锁失败

情况5 Runnable ----> TERMINATED
当前所有方法执行完毕

死锁:哲学家就餐问题
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁。

定位死锁
检测死锁可以使用 jconsole 工具,或使用 jps 定位进程id,再用 jstack 定位死锁

避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

什么是中断机制?
首先一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。
所以,Thread.stop,Thread.suspend,Thread.resume都已经被废弃了。
其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了-种用于停止线程的协商机制一一中断,也即中断标识协商机制。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断-一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竞该做什么需要你自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false 表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

12. ReentryLock

与synchronized一样,都支持可重入;此外,还具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
    lock.lock();
    try {
        
    }finally {
        lock.unlock();
    }
}

可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j(topic="lockTest")
public class LockTest {
    private static final ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            log.info("enter main");
            method1();
        }finally {
            lock.unlock();
        }
    }

    public static void method1(){
        lock.lock();
        try {
            log.info("enter method1");
            method2();
        }finally {
            lock.unlock();
        }
    }

    public static void method2(){
        lock.lock();
        try {
            log.info("enter method2");
        }finally {
            lock.unlock();
        }
    }
}

可打断

锁超时

公平锁
ReentrantLock 默认是不公平的,可通过在构造方法中传入true创建公平锁

条件变量
synchronized中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待
ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized是那些不满足条件的线程都在一间休息室等消息
  • 而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
    使用流程
  • await 前需要获得锁
  • await 执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(或打断、或超时)取重新竞争lock锁
  • 竞争lock锁成功后,从await后继续执行
Condition condition = lock.newCondition();
conditiona.await(); //conditiona 进入休息室休息
conditionb.signal(); //conditionb的线程被唤醒

同步模式之顺序控制
案例一:先打印2后打印1
方法一:使用wait notify

@Slf4j
public class TestOrder {
     static Object lock = new Object();
     static boolean t2runned = false;//表示t2是否运行过
     
    public static void main(String[] args) {

        Thread t1 = new Thread(()-> {
            synchronized (lock) {
                if (!t2runned){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("1");
                }
            }
        }, "t1");

        Thread t2 = new Thread(()-> {
            synchronized (lock){
                log.info("2");
                t2runned = true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();
    }

}

方法二:使用ReentrantLock await() signal()

方法三:使用park()、unpark() 【更简单】

public static void main(String[] args) {
    Thread t1 = new Thread(()-> {
        LockSupport.park();
        log.info("1");
    }, "t1");

    Thread t2 = new Thread(()-> {
        log.info("2");
        LockSupport.unpark(t1);
    }, "t2");
    t1.start();
    t2.start();
}

案例二:线程1输出a 5次,线程2输出 b 5次,线程 3 输出c 5次。现在要求输出abcabcabcabcabc怎么实现
方法一:wait notify版本

public class TestOrder2 {
    public static void main(String[] args) {
        WaitNotify wn = new WaitNotify(1, 5);
        new Thread(()->{
          wn.print("a", 1, 2);
        },"t1").start();
        new Thread(()->{
            wn.print("b", 2, 3);
        },"t1").start();
        new Thread(()->{
            wn.print("c", 3, 1);
        },"t1").start();
    }
}

  /*
    输出内容    等待标记    下一个标记
    a           1           2
    b           2           3
    c           3           1
   */
    class WaitNotify{
        private  int flag; //等待标记
        private int loopNumber; //循环次数

      public WaitNotify(int flag, int loopNumber) {
          this.flag = flag;
          this.loopNumber = loopNumber;
      }

      public void print(String str, int waitflag, int nextflag){
         synchronized (this){
             for (int i = 0; i < loopNumber; i++) {
                 while (waitflag != flag){
                     try {
                         this.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 System.out.print(str);
                 this.flag = nextflag;
                 this.notifyAll();
             }
         }
      }

}

方法二:ReentrantLock版本

public class TestOrder3 {
    static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        AWaitSignal as = new AWaitSignal(5);
        Condition a = as.newCondition();
        Condition b = as.newCondition();
        Condition c = as.newCondition();
        new Thread(()->{
            as.print("a", a, b);
        }, "t1").start();
        new Thread(()->{
            as.print("b", b, c);
        }, "t1").start();
        new Thread(()->{
            as.print("c", c, a);
        }, "t1").start();

        Thread.sleep(1000);
        as.lock();
        try {
            System.out.println("开始.....");
            a.signal();
        }finally {
            as.unlock();
        }
    }
}

class AWaitSignal extends ReentrantLock{
    private  int loopNumber;

    public AWaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    // 参数1 打印内容  参数2:进入哪一间休闲室休息  参数3:下一间休息室
    public void print(String s, Condition cur, Condition wait){
        lock();
        try {
            for (int i = 0; i < loopNumber; i++) {
                cur.await();
                System.out.print(s);
                wait.signal(); // 唤醒下一个线程
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            unlock();
        }
    }
}

方法三:park、unpark

public class TestOrder4 {
    static Thread t1;
    static Thread t2;
    static Thread t3;

    public static void main(String[] args) {
        ParkUnPark park = new ParkUnPark(5);

        t1 = new Thread(() -> {
            park.print("a", t2);
        });
        t2 = new Thread(() -> {
            park.print("b", t3);
        });
        t3 = new Thread(() -> {
            park.print("c", t1);
        });
        t1.start();
        t2.start();
        t3.start();
        LockSupport.unpark(t1);
    }
}

class ParkUnPark{
    private int loopNumber;

    public ParkUnPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String s, Thread next){
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(s);
            LockSupport.unpark(next);
        }
    }
}

参考:
黑马程序员视频笔记
原文链接:https://blog.youkuaiyun.com/zwx900102/article/details/106305107

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值