【JUC】(2)共享问题解决与synchronized对象锁分析!全程干货!!快快收藏!!

简介: 本章内容将深入JUC研究并行并发问题;涵盖内容:锁、轻量锁、对象锁、Monitor监控、等待与唤醒...如果你也想在Java上更进一步!那么本篇文章将是你必看文章之一!

1. 共享问题与对象锁分析

1.1 共享带来的问题

当不使用锁的场景下,很容易出现这样的问题。

  1. 当用户A修改数据库中的x值变量为1,那么同时用户B向数据库索要x的值
  2. 而这时候x还没有被用户A改为1,所以数据库这边在处理用户B的时候返回的还是x=0;
  3. 当数据返回了用户B的值,才完成用户A的请求(将x修改为1)

1.1.1 从Java上来体现共享问题

static int x = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i <5000; i++) {
            x++;
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i <5000; i++) {
            x--;
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.info(String.valueOf(x));
}

有可能是0,但大多数情况下是其他数字

1.1.2 共享问题的分析

以上的结果可能是整数、负数、0

在Java中对静态变量的自增、自减操作并不是原子操作。

  • i++的字节码文件
getstatic i // 获取静态变量 i的值
iconst_1    // 准备常量1
iadd      // 自增
putstatic i // 将修改后的值存入静态变量i中
  • i–的字节码文件
getstatic i // 获取静态变量 i的值
iconst_1    // 准备常量1
iadd      // 自增
putstatic i // 将修改后的值存入静态变量i中

如果是单线程以上8行代码是顺序执行(不交错)那就不会出现这种问题

1.1.3 临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
  • 多个线程读共享资源其实也没问题
  • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int x=0;
static void add(){
    // 临界区
    x++;
}
static void sub(){
    // 临界区
    x--;
}

1.1.4 竞态条件 Race Condition

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

简单来说就是执行顺序冲突,线程1还没有完成更改值的操作,线程2就去读取,结果拿到的就是修改前的旧数据

1.2 synchronized(对象锁)解决方案

为了避免临界区的竞态条件发送,有多种手段可以达到目的

  • 阻塞式的解决方案:synchronized、Lock
  • 非阻塞式的解决方案:原子变量

使用synchronized注意的点:它采用互斥的方式让同一时刻最多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

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

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

运行流程

package com.renex.c3;
import lombok.extern.slf4j.Slf4j;
/**
 * 对象锁
 */
@Slf4j(topic = "scz")
public class Scz {
    static int x=0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    x++;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    x--;
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info("{}",x);// [main] INFO scz - 0
    }
}

1.2.1 面向对象的改进

把需要保护的共享变量放入一个类中

package com.renex.c3;
import lombok.extern.slf4j.Slf4j;
/**
 * 对象锁
 */
@Slf4j(topic = "scz")
public class Scz2 {
    public static void main(String[] args) throws InterruptedException {
        SyncClass syncClass = new SyncClass();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                syncClass.add();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                syncClass.del();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info("{}",syncClass.get());// [main] INFO scz - 0
    }
}
class SyncClass{
    private int x=0;
    public void add(){
        synchronized (this){
            x++;
        }
    }
    public void del(){
        synchronized (this){
            x--;
        }
    }
    public int get(){
        synchronized (this){
            return x;
        }
    }
}

将需要解决临界问题的变量放入一个类中。在该类中专门处理这些共享变量。

而我们仅需调用该类中做出处理的方法即可。

1.2.2 语法点

  • 其一:
class test1{
    public synchronized void test(){
        
    }
}
等价于:
class test1{
    public void test(){
        synchronized(this){
            
        }
    }
}
  • 其二:
class test{
    public synchronized static void test(){
        
    }
}
等价于:
class test{
    public static void test(){
        synchronized(Test.class){
            
        }
    }
}

1.2.3 ”线程八锁“

考察synchronized锁住的是哪个对象

  • 其一:锁住Demo对象;
    情况:可能是先1,也可能是先2
@Slf4j(topic = "test1")
public class Test1 {
    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo::a).start();
        new Thread(demo::b).start();
    }
}
@Slf4j(topic = "demo")
class Demo{
    public synchronized void a(){
        log.info("1");
    }
    public synchronized void b(){
        log.info("2");
    }
}
////////////////////////////////////////////////////
[Thread-0] INFO demo - 1
[Thread-1] INFO demo - 2
  • 其二:锁住Demo对象
    情况:1s后先1后2;先2 1s后1
package com.renex.c3;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "test1")
public class Test1 {
    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(()->{
            try {
                demo.a();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).start();
        new Thread(demo::b).start();
    }
}
@Slf4j(topic = "demo")
class Demo{
    public synchronized void a() throws Exception {
        Thread.sleep(1);
        log.info("1");
    }
    public synchronized void b(){
        log.info("2");
    }
}
////////////////////////////////////////////////////////
[Thread-0] INFO demo - 1
[Thread-1] INFO demo - 2

当synchronized不装饰在静态方法上,那么它锁住的是当前这个类对象的实例

如果是装饰在静态变量上,那么它锁住的是这个类对象

1.3 变量的线程安全分析

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

如果它们没有共享,则线程安全

如果它们被共享了,根据它们的状态是否能够改变,分为

  1. 如果只有读操作,则线程安全
  2. 如果有读写操作,则这段代码是临界区,需要考虑线程安全

1.3.2 局部变量是否线程安全

局部变量是线程安全的

但局部变量引用的对象则未必安全

  1. 如果该对象没有逃离方法的作用范围,它是线程安全的
  2. 如果该对象逃离方法的作用范围,需要考虑线程安全

1.3.3 局部变量线程安全分析

1.3.3.1 方法内

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

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

1.3.3.2 成员变量

@Slf4j(topic = "PS1C")
public class PS1C {
    public static void main(String[] args) {
        ThreadUnsafe unsafe = new ThreadUnsafe();
        for (int i = 0; i < 200; i++) {
            new Thread(()->{
                unsafe.test1(2); // 有可能会出问题
            },"Thread"+(i+1)).start();
        }
    }
}
class ThreadUnsafe{
    // 直接保存在堆中
    List<String> list = new ArrayList<String>();
    
    public void test1(int num){
        for (int i = 0; i < num; i++) {
            // 临界区
            add(list);
            sub(list);
            // 临界区
        }
    }
    private void add(List<String> list){
        list.add("1");
    }
    private void sub(List<String> list){
        list.remove(0);
    }
}
////////////////// 可能报错///////////////////
Exception in thread "Thread107" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
  at java.util.ArrayList.rangeCheck(ArrayList.java:659)
  at java.util.ArrayList.remove(ArrayList.java:498)
  at com.renex.c3.ThreadUnsafe.sub(PS1C.java:42)
  at com.renex.c3.ThreadUnsafe.test1(PS1C.java:35)
  at com.renex.c3.PS1C.lambda$main$0(PS1C.java:18)
  at java.lang.Thread.run(Thread.java:750)

由于list对象是直接存储在了堆区中,并不是栈帧,所以这属于一个共享变量。当发现调用时就会发生竞态条件

1.3.3.3 继承

当存在有继承关系时,如果重写了方法,那么就不再安全了

class ThreadUnsafe1{
    List<String> list = new ArrayList<String>();
    public void test1(int num){
        for (int i = 0; i < num; i++) {
            add(list);
            sub(list);
        }
    }
    public void add(List<String> list){
        list.add("1");
    }
    public void sub(List<String> list){
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadUnsafe1{
    public void sub(List<String> list){
        // 重写方法,并创建了一个新的线程
        new Thread(()->{
            list.remove(0);
        }).start();
    }
}
/////////////////////////////////////////
Exception in thread "Thread-379" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
  at java.util.ArrayList.rangeCheck(ArrayList.java:659)
  at java.util.ArrayList.remove(ArrayList.java:498)
  at com.renex.c3.ThreadSafeSubClass.lambda$sub$0(PS2C.java:48)
  at java.lang.Thread.run(Thread.java:750)

若是将父类的方法改为private,那么还依旧是安全的。因为无法重写私有方法

另,如果在方法上加final关键字,其实也是安全的

class ThreadUnsafe1 {
    List<String> list = new ArrayList<String>();
    public final void test1(int num) {
        for (int i = 0; i < num; i++) {
            add(list);
            sub(list);
        }
    }
    private void add(List<String> list) {
        list.add("1");
    }
    private void sub(List<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadUnsafe1 {
    public void sub(List<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

从这个例子中可以看出private或final提供【安全】的意义所在

1.3.4 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下的所有类

说它们是线程安全的多指的是:

  • 多个线程调用它们同一个实例的某一个方法时,是线程安全的。
  • 也可以理解为:
  1. 它们的每个方法是原子的
  2. 但它们多个方法的组合不是原子的

1.3.4.1 线程安全类方法的组合

Hashtable table = new Hashtable();
if(table.get("key")==null){
    table.put("key",value);
}

这里就可以看get和put方法。它们两个方法单独拎出来是线程安全的。

但是它们结合到一块用就不是了。因为table.get(“key”)之后可能就会有其他的线程来修改数据,这时候就不符合if内的表达式判断了。

1.3.4.2 不可变类线程安全性

String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

String有replace、substring等方法可以改变值,这样又怎么能保证线程安全呢?

源码:

public String substring(int beginIndex, int endIndex) {
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
        : new String(value, beginIndex, subLen);
}

可以看到,substring方法其实并没有改变字符串的值,而是创建了一个新的字符串返回,这就避免了共享值被改变的情况;包括replace方法也是类似

public String replace(char oldChar, char newChar) {
      // 判断老字符串是否等于新字符串
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 创建一个新字符串对象返回
                return new String(buf, true);
            }
        }
        return this;
    }

1.3.4.3 案例

分析代码,查看哪里有线程不安全的地方

package com.renex.c3;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Random;
import java.util.Vector;
/**
 * 卖票
 */
@Slf4j(topic = "sell")
public class SellTicket {
    public static void main(String[] args) throws InterruptedException {
        int rand = new Random().nextInt(5)+1;
        TicketWindow window = new TicketWindow(1000);
        /// 接收所有线程的集合
        ArrayList<Thread> threadArrayList = new ArrayList<>();
        Vector<Integer> amounts = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                int amount = window.sell(rand);
                amounts.add(amount);
            }, "t" + i);
            thread.start();
            threadArrayList.add(thread);
        }
        for (Thread thread : threadArrayList) {
            thread.join();
        }
        log.info("剩余票数:{}",window.getCount());
        log.info("卖出的票数:{}",amounts.stream().mapToInt(i->i).sum());
    }
}
class TicketWindow{
    private int count;
    public TicketWindow(int num) {
        this.count = num;
    }
    public int getCount(){
        return this.count;
    }
    public int sell(int amout){
        // 判断剩余票数是否大于要卖出的票数
        if (this.count >=amout){
            this.count -= amout;
            return amout;
        }else {
            return 0;
        }
    }
}
//////////////////////////////////////
[main] INFO sell - 剩余票数:0
[main] INFO sell - 卖出的票数:1002

首先分析main方法:

  1. int rand = new Random().nextInt(5)+1;代码只是返回一个数字来使用,所以是安全的
  2. TicketWindow window = new TicketWindow(1000);使用了类的构造方法先按下不表
  3. ArrayList<Thread> threadArrayList = new ArrayList<>();Vector<Integer> amounts = new Vector<>();这两行定义了接收的列表;但是在main方法中使用的,所以它两还是在main线程中的栈帧内。而且,也并没有组合使用方法,因此不会出现安全问题。
Thread thread = new Thread(() -> {
    int amount = window.sell(rand);
    amounts.add(amount);
}, "t" + i);
  1. 主要来看window.sell(rand)这段,它这里使用了sell方法;sell方法内部实际上就是一段临界区
    更值得注意的是window是一个线程外的变量,这也表明了是共享变量
private int count;
public int sell(int amout){
    // 判断剩余票数是否大于要卖出的票数
    if (this.count >=amout){
        this.count -= amout;
        return amout;
    }else {
        return 0;
    }
}
  1. 可以看到这里有判断,有赋值操作,在并发情况下就会出现所属,而且count变量是一个成员变量;
  2. 找到问题所在:TicketWindow类内部的sell方法
  3. 解决办法:
synchronized public int sell(int amout){
    // 判断剩余票数是否大于要卖出的票数
    if (this.count >=amout){
        this.count -= amout;
        return amout;
    }else {
        return 0;
    }
}
  1. 为sell方法添加上synchronized对象锁,锁的是谁?由于不是静态,那么它锁的就是window这个TicketWindow类的实例变量
    当上锁后
    int amount = window.sell(rand);每一个线程中的sell方法,都会检查window变量是否被加锁。如果被加锁,那么就阻塞切换上下文、等待获取锁;
  • 更改后
package com.renex.c3;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Random;
import java.util.Vector;
/**
 * 卖票
 */
@Slf4j(topic = "sell")
public class SellTicket {
    public static void main(String[] args) throws InterruptedException {
        int rand = new Random().nextInt(5)+1;
        // 对于线程thread来说,window变量就是一个共享变量
        TicketWindow window = new TicketWindow(1000);
        /// 接收所有线程的集合
        ArrayList<Thread> threadArrayList = new ArrayList<>();
        Vector<Integer> amounts = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                int amount = window.sell(rand);
                amounts.add(amount);
            }, "t" + i);
            thread.start();
            threadArrayList.add(thread);
        }
        // 必须等待所有线程都完成任务
        for (Thread thread : threadArrayList) {
            thread.join();
        }
        log.info("剩余票数:{}",window.getCount());
        log.info("卖出的票数:{}",amounts.stream().mapToInt(i->i).sum());
    }
}
class TicketWindow{
    private int count;
    public TicketWindow(int num) {
        this.count = num;
    }
    public int getCount(){
        return this.count;
    }
    synchronized public int sell(int amout){
        ////////////////////////// 临界区
        // 判断剩余票数是否大于要卖出的票数
        if (this.count >=amout){
            this.count -= amout;
            return amout;
        }else {
            return 0;
        }
        ////////////////////////// 临界区
    }
}
////////////////////////////////////////////////////
[main] INFO sell - 剩余票数:0
[main] INFO sell - 卖出的票数:1000

1.4 Monitor概念

1.4.1 Java 对象头

  • 普通对象
|--------------------------------------------------------------|
|                     Object Header (128 bits)                 |
|------------------------------------|-------------------------|
|        Mark Word (64 bits)         | Klass pointer (64 bits) |
|------------------------------------|-------------------------|

被压缩后

|--------------------------------------------------------------|
|                     Object Header (96 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (64 bits)         | Klass pointer (32 bits) |
|------------------------------------|-------------------------|
  • 数组对象
|---------------------------------------------------------------------------------|
|                                 Object Header (128 bits)                        |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(64bits)       | Klass pointer(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

Klass:被用于保存字节码文件(.class文件);这是一个C++对象,含有类的信息、虚方法表等

MarkWord:主要用来存储对象自身的运行时数据

内部存储的哈希码和我们看到的哈希码是不一样的

因为电脑的内存底层(寄存器)使用的是大端模式次序存放的;

内存中则以小端模式存放;CPU存取数成时,小端和大端之间的转换是通过硬件实现的,没有数据加载/存储的开销

mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。 为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

1.4.2 Monitor(锁)

Monitor 被翻译为监视器管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)后,该对象头的Mark Word中就被设置指向Monitor对象的指针

Monitor结构如下:

  1. 刚开始Monitor中Owner为null
  2. 当Thread-2执行synchronized(obj)就会奖Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  3. 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED阻塞等待
  4. Thread-2执行完代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
  5. 途中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程

synchronized必须是进入同一个对象的monitor才有上述的效果

不加synchronized的对象不会关联监视器,不遵从以上规则

1.5 synchronized原理进阶

Java HotSpot 虚拟机中,每个对象都有对象头(包含class指针和Mark Word)。

Mark Word平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容

1.5.1 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。而一旦出现并发情况,那么就会升级为重量级锁

轻量级锁对使用者是透明的,即语法仍然是synchronized

举个简单的例子:

学生A(t1)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没有竞争,继续上他的课。

如果这期间有其他学生B(t2)来了,会告知学生A(t1)有并发访问,线程A t1 随机升级为重量级锁,进入重量级锁的流程

而重量级锁就不是用课本占座这种小操作了。可以想象成在座位周围用一个铁栅栏上了锁围起来

static Object obj new Object();
public static void method1(){
    synchronized(obj){
        // 同步块A
        method2();
    }
}
public static void method2(){
    synchronized(obj){
    // 同步块B
    }
}

每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

线程A 对象Mark Word 线程B
访问同步块A,把Mark复制到线程A的锁记录 01(无锁)
CAS修改Mark为线程1锁记录地址 01(无锁)
成功(加锁) 00(轻量锁)线程1锁记录地址
执行同步块A 00(轻量锁)线程1锁记录地址
访问同步块B,把Mark复制到线程A的锁记录 00(轻量锁)线程1锁记录地址
CAS修改Mark为线程A锁记录地址 00(轻量锁)线程1锁记录地址
失败(发现是自己锁的) 00(轻量锁)线程1锁记录地址
锁重入 00(轻量锁)线程1锁记录地址
执行同步块B 00(轻量锁)线程1锁记录地址
同步块B执行完毕 00(轻量锁)线程1锁记录地址
同步块A执行完毕 00(轻量锁)线程1锁记录地址
成功(解锁) 01(无锁)
01(无锁) 访问同步块A,把Mark复制到线程2的
01(无锁) CAS 修改Mark为线程2锁记录地址
00(轻量级)线程2锁记录地址 成功(加锁)

1.5.2 锁膨胀

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

static Object obj = new Object();
public static void method1(){
    synchronized(obj){
        // 同步块
    }
}
线程A 对象Mark Word 线程B
访问同步块A,把Mark复制到线程A的锁记录 01(无锁)
CAS修改Mark为线程A锁记录地址 01(无锁)
成功(加锁) 00(轻量锁)线程1锁记录地址
执行同步块A 00(轻量锁)线程1锁记录地址
执行同步块A 00(轻量锁)线程1锁记录地址 访问同步块A,把Mark复制到线程2的
执行同步块A 00(轻量锁)线程1锁记录地址 CAS 修改Mark为线程2锁记录地址
执行同步块A 00(轻量锁)线程1锁记录地址 失败(发现其他线程已经占用锁)
执行同步块A 00(轻量锁)线程1锁记录地址 CAS修改Mark为重量级锁
执行同步块A 10(重量级)重置锁指针 阻塞中
执行完毕 10(重量级)重置锁指针 阻塞中
失败(解锁) 10(重量级)重置锁指针 阻塞中
释放重量级锁,唤起阻塞线程竞争 10(重量级)重置锁指针 阻塞中
10(重量级)重置锁指针 竞争重量锁
10(重量级)重置锁指针 成功(加锁)

  • 这时Thread-1加轻量级锁失败,进入锁膨胀流程
  • 即Object对象申请Monitor锁,让Object指向重量级锁地址
  • 然后自己进入Monitor的EntryList队列阻塞(BLOCKED)

  • 当Thread-0推出同步块解锁时,使用cas将MarkWord的值恢复给对象头(一定失败)。这时进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程

为什么一定失败?因为Object已经被申请了重量级锁流程了,唤醒了Monitor锁。

1.5.3 自旋重试

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

在java6以后,自旋锁时自适应的。比如对象刚刚的一次自旋操作成功过,那么任务这次自旋成功的可能性会比较高,就多自旋几次,反之就少自旋,甚至不自旋。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了更划算),熄火了相当于阻塞(等待时间长了更划算)
  • Java7之后不能控制是否开启自旋功能

1.5.3.1 自旋重试成功情况

线程1(cpu1上) 对象Mark 线程2(cpu2上)
10(重量锁)
访问同步块,获取monitor 10(重量锁)重置锁指针
成功(加锁) 10(重量锁)重置锁指针
执行同步块 10(重量锁)重置锁指针
执行同步块 10(重量锁)重置锁指针 访问同步块,获取monitor
执行同步块 10(重量锁)重置锁指针 自旋重试
执行完毕 10(重量锁)重置锁指针 自旋重试
成功(解锁) 10(重量锁) 自旋重试
10(重量锁)重置锁指针 成功(加锁)
10(重量锁)重置锁指针 执行同步块

1.5.3.2 自旋重试失败情况

线程1(cpu1上) 对象Mark 线程2(cpu2上)
10(重量锁)
访问同步块,获取monitor 10(重量锁)重置锁指针
成功(加锁) 10(重量锁)重置锁指针
执行同步块 10(重量锁)重置锁指针
执行同步块 10(重量锁)重置锁指针 访问同步块,获取monitor
执行同步块 10(重量锁)重置锁指针 自旋重试
执行同步块 10(重量锁)重置锁指针 自旋重试
10(重量锁)重置锁指针 阻塞

1.5.4 偏向锁

轻量级锁在没有竞争时,每次重入仍需要执行CAS操作。

java6中引入了偏向锁来做进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的,就表示没有竞争,不用重新CAS

在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能是非常不错的。

唯独就怕遭其他线程抢锁,因为需要撤销偏向(会STW)。重复争抢锁,就会导致性能下降

缺点:

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
  • 访问对象的hashCode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程t1的对象仍有机会重新偏向t2,重偏向会重置对象的Thread ID
  • 撤销偏向和重偏向都是批量进行的,以类为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用-XX:UseBiasedLocking禁用偏向锁

1.5.4.1 偏向状态

对象头MarkWord格式(64位)

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(128bits)                                              |
|-----------------------------------------------------------------------------------------------------------------|
|                                   Mark Word(64bits)               |  Klass Word(64bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
| unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:1|lock:2 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
| thread:54|      epoch:2       |unused:1|age:4|biase_lock:1|lock:2 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|                     ptr_to_lock_record:62                 |lock:2 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                    ptr_to_heavyweight_monitor:62          |lock:2 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                           |lock:2 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位位101,这时它的thread、epoch、age都为0
  • 偏向锁默认是延迟的,不会再程序启动时立即生效,如果想避免延迟,可以加VM参数
  • 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
@Slf4j(topic = "test1")
public class test1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        log.info(ClassLayout.parseInstance(dog).toPrintable());
    }
}
class Dog{
}
////////////////////////////////////////
[main] INFO test1 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
     /////// MarkWord
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     /////// MarkWord
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     /////// Klass
      8     4        (object header)                           e0 ae ca 92 (11100000 10101110 11001010 10010010) (-1832210720)
     12     4        (object header)                           bd 02 00 00 (10111101 00000010 00000000 00000000) (701)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

加锁后

@Slf4j(topic = "test1")
public class test1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        log.info(ClassLayout.parseInstance(dog).toPrintable());
        synchronized (dog){
            log.info(ClassLayout.parseInstance(dog).toPrintable());
        }
        log.info(ClassLayout.parseInstance(dog).toPrintable());
    }
}
class Dog{
}
///////////////////////////////////////////////////
[main] INFO test1 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           48 af d4 12 (01001000 10101111 11010100 00010010) (315928392)
     12     4        (object header)                           30 02 00 00 (00110000 00000010 00000000 00000000) (560)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
////////// 加锁的对象头信息打印结果
[main] INFO test1 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 f4 7f 4e (10001000 11110100 01111111 01001110) (1317008520)
      4     4        (object header)                           ff 00 00 00 (11111111 00000000 00000000 00000000) (255)
      8     4        (object header)                           48 af d4 12 (01001000 10101111 11010100 00010010) (315928392)
     12     4        (object header)                           30 02 00 00 (00110000 00000010 00000000 00000000) (560)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
///////////////// 锁释放后的对象头
[main] INFO test1 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           48 af d4 12 (01001000 10101111 11010100 00010010) (315928392)
     12     4        (object header)                           30 02 00 00 (00110000 00000010 00000000 00000000) (560)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

当关闭了偏向锁的延迟后(VM参数:)

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
[main] INFO test1 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 18 e2 f6 (00000101 00011000 11100010 11110110) (-152954875)
      4     4        (object header)                           d0 01 00 00 (11010000 00000001 00000000 00000000) (464)
      8     4        (object header)                           48 af ca a1 (01001000 10101111 11001010 10100001) (-1580552376)
     12     4        (object header)                           d0 01 00 00 (11010000 00000001 00000000 00000000) (464)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

我们只用看0 4 (object header) 05 18 e2 f6 (00000101 00011000 11100010 11110110) (-152954875)这一段的值:二进制码就可以了,可以看到变为了00000101正常状态是(00000001)

这就代表以及上锁了

1.5.4.2 撤销-调用对象的方法 hashCode

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

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode

在调用hashCode后使用偏向锁,记得去掉:-XX:+UseBiasedLocking

1.5.4.3 撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

package com.renex.c4;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
 * 锁偏向
 */
@Slf4j(topic = "test2")
public class test2 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Thread t1 = new Thread(() -> {
            log.info(ClassLayout.parseInstance(dog).toPrintable());
            synchronized (dog){
                log.info(ClassLayout.parseInstance(dog).toPrintable());
            }
            log.info(ClassLayout.parseInstance(dog).toPrintable());
            // 给本类加锁,如果dog锁被释放,唤醒t2线程
            synchronized (test2.class){
                test2.class.notify();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            // 让t2线程先阻塞,当本类锁被要求唤醒时才执行
            synchronized (test2.class){
                try {
                    test2.class.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            log.info(ClassLayout.parseInstance(dog).toPrintable());
            synchronized (dog){
                log.info(ClassLayout.parseInstance(dog).toPrintable());
            }
            log.info(ClassLayout.parseInstance(dog).toPrintable());
        }, "t2");
        t1.start();
        t2.start();
    }
}
///////////////////////////////////
[t1] INFO test2 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           d2 c7 00 f8 (11010010 11000111 00000000 11111000) (-134166574)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[t1] INFO test2 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 ea 72 (00000101 00001000 11101010 01110010) (1927940101)
      4     4        (object header)                           f1 01 00 00 (11110001 00000001 00000000 00000000) (497)
      8     4        (object header)                           d2 c7 00 f8 (11010010 11000111 00000000 11111000) (-134166574)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[t1] INFO test2 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 ea 72 (00000101 00001000 11101010 01110010) (1927940101)
      4     4        (object header)                           f1 01 00 00 (11110001 00000001 00000000 00000000) (497)
      8     4        (object header)                           d2 c7 00 f8 (11010010 11000111 00000000 11111000) (-134166574)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
////////////////////////////////////////////////////////////////////// t2 线程被唤醒执行
[t2] INFO test2 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 ea 72 (00000101 00001000 11101010 01110010) (1927940101)
      4     4        (object header)                           f1 01 00 00 (11110001 00000001 00000000 00000000) (497)
      8     4        (object header)                           d2 c7 00 f8 (11010010 11000111 00000000 11111000) (-134166574)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
///////////////////// 添加了轻量级锁:01110000;因为本来是偏向于t1线程的,这时候切换为t2,那么就升级为轻量级锁
[t2] INFO test2 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           70 f1 af 59 (01110000 11110001 10101111 01011001) (1504702832)
      4     4        (object header)                           ee 00 00 00 (11101110 00000000 00000000 00000000) (238)
      8     4        (object header)                           d2 c7 00 f8 (11010010 11000111 00000000 11111000) (-134166574)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
//////////////////// 偏向状态更改为不可偏向
[t2] INFO test2 - com.renex.c4.Dog object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           d2 c7 00 f8 (11010010 11000111 00000000 11111000) (-134166574)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0

1.5.4.4 撤销-调用wait/notify

因为只有重量级锁才有notify;当使用了重量级锁,自然偏向锁也就没有了

1.5.4.5 批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重写偏向T2,重偏向会重置对象的Thread ID

当撤销偏向锁阈值超过20次后,JVM会这样觉得偏向是否错误,于是会在给这些对象加锁时重写偏向至加锁线程

1.5.4.6 批量撤销

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

1.5.5 其他优化

1.5.5.1 减少上锁时间

同步代码块中尽量简短一点

1.5.5.2 减少锁的粒度

将一个锁拆分为多个锁提高并发度 例如:

  • ConcurrentHashMap
  • LongAdder 分为base和cells两部分。
  • 没有并发竞争时候或者cells数组正在初始化的时候,会使用CAS来累加值到base;
  • 有并发竞争时,则会初始化cells数组,数组有多少个cell,就允许有多少线程并行修改,最后将赎罪中每个cell累加,再加上base就是最终的值
  • LinkedBlockingQueue入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高

1.5.5.3 锁粗化

多次循环进入同步块不如同步块内多次循环

另外JVM可能会做如下优化,把多次append的加锁操作粗化一次(因为都是对同一个对象加锁,没比较重入多次)

new StringBuffer().append("a").append("b").append("c");

1.5.5.4 锁消除

JVM会进行代码的逃逸分析,例如某个加锁对象时方法内局部变量,不会被其他线程锁访问到,这时候就会被即时编译器忽略掉所有同步操作

1.5.5.5 读写分离

读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。

不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步:

  • 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
  • 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制。
  • 系统将写请求交给主数据库处理,读请求交给从数据库处理

1.5.6 wait/notify

1.5.6.1 为什么需要wait?

  1. Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态;

因为如果BLCOKED队列阻塞太多线程,也会拖慢程序的运行效率

  1. BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  2. BLOCKED线程会在Owner线程释放锁时唤醒
  3. WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需要进入EntryList重新竞争

1.5.6.2 API介绍

  • obj.wait():让进入object监视器的线程到waitSet等待
  • obj.notify():在object上正在waitSet等待的线程中挑一个唤醒
  • obj.notifyAll():让object上正在waitSet等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法

package com.renex.c4;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
 * wait/notify
 */
@Slf4j(topic = "test2")
public class test3 {
    final static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (obj) {
                log.info("执行...");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("结束...");
            }
        },"t1").start();
        new Thread(()->{
            synchronized (obj) {
                log.info("2执行...");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("2结束...");
            }
        },"t2").start();
        // 主线程两秒后执行
        Thread.sleep(2);
        // 锁住obj对象
        synchronized (obj){
            log.info("唤醒");
            // 在监视obj锁的Monitor监视器的WAITING队列里随机挑一个的线程拉入EntryList重新抢锁唤醒
            obj.notify();
            // obj.notifyAll();//全部唤醒
        }
    }
}
///////////////////
[t1] INFO test2 - 执行...
[t2] INFO test2 - 2执行...
[main] INFO test2 - 唤醒
[t1] INFO test2 - 结束...
  • 关于wait()方法
    它实际提供了两个方法;可以设置最大等待时间
public final void wait() throws InterruptedException {
    wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
// 实际还是调用 wait(long timeout)
public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
            "nanosecond timeout value out of range");
    }
    if (nanos > 0) {
        timeout++;
    }
    wait(timeout);
}

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

  1. sleep是Thread方法
    wait是Object方法
  2. sleep不需要强制和synchronized配合使用
    wait需要和synchronized一起使用
  3. sleep在睡眠的同时,不会释放对象锁
    wait在等待时会释放对象锁
  4. 两者状态都是一样的TIME_WAITING
@Slf4j(topic = "test4")
public class test4 {
    final static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (obj){
                log.info("t1获得锁");
                try {
                    // sleep进入睡眠并不会将锁释放
//                    Thread.sleep(20000);
                    obj.wait();// 进入Wait Set等待前会释放锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
        
        Thread.sleep(10);// main线程先睡10毫秒;让t1线程先运行
        synchronized (obj){
            log.info("main获得锁");
        }
    }
}
///////////////////////////////
[Thread-0] INFO test4 - t1获得锁
[main] INFO test4 - main获得锁

1.5.6.4 案例1-装修工与主管

package com.renex.c4;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
@Slf4j(topic = "Room")
public class Room {
    static final Object lock = new Object();
    static Boolean success = false;
    /**
     * 主管需要签发证明
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                if (!success){
                    // 没有签发证明!
                    log.info("签发证明还没有到位!!");
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("签发证明在哪里?");
                    if (success){
                        log.info("签发证明已经拿到了!");
                    }
                }else {
                    log.info("签发证明已经审批下来了!!");
                }
            }
        },"主管").start();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                synchronized (lock){
                    if (success){
                        log.info("开工!干活!!!");
                    }else {
                        log.info("没有签发证明也能开工!干活!!!");
                    }
                }
            },"装修工"+i).start();
        }
        Thread.sleep(10);
        new Thread(()->{
            synchronized (lock){
                success = true;
                log.info("签发证明发放了!");
            }
        },"施工方").start();
    }
}
/////////////////////////////////
[主管] INFO Room - 签发证明还没有到位!!
[主管] INFO Room - 签发证明在哪里?
[施工方] INFO Room - 签发证明发放了!
[装修工4] INFO Room - 开工!干活!!!
[装修工3] INFO Room - 开工!干活!!!
[装修工1] INFO Room - 开工!干活!!!
[装修工2] INFO Room - 开工!干活!!!
[装修工0] INFO Room - 开工!干活!!!

这里我们可以看到主管线程由于使用sleep方法,因此它并没有释放掉对象锁。

所以当它睡眠20s后,依旧无法获得签发证明,因为 施工方线程 还在等待锁

并且由于没有释放锁,所以装修工同样也无法进行施工,而实际上装修工是可以提前施工的

这里换成wait()notify()方法就可以了

package com.renex.c4;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.util.Date;
@Slf4j(topic = "Room")
public class Room {
    static final Object lock = new Object();
    static Boolean success = false;
    /**
     * 主管需要签发证明
     * @param args
     */
    @Test
    public void test1() throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                if (!success){
                    // 没有签发证明!
                    log.info("签发证明还没有到位!!");
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("签发证明在哪里?");
                    if (success){
                        log.info("签发证明已经拿到了!");
                    }
                }else {
                    log.info("签发证明已经审批下来了!!");
                }
            }
        },"主管").start();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                synchronized (lock){
                    if (success){
                        log.info("开工!干活!!!");
                    }else {
                        log.info("没有签发证明也能开工!干活!!!");
                    }
                }
            },"装修工"+i).start();
        }
        Thread.sleep(10);
        new Thread(()->{
            synchronized (lock){
                success = true;
                log.info("签发证明发放了!");
            }
        },"施工方").start();
    }
}
/////////////////////////////////
[主管] INFO Room - 签发证明还没有到位!!
[施工方] INFO Room - ------------签发证明发放了!
[装修工4] INFO Room - 开工!干活!!!
[装修工2] INFO Room - 开工!干活!!!
[装修工3] INFO Room - 开工!干活!!!
[装修工0] INFO Room - 开工!干活!!!
[装修工1] INFO Room - 开工!干活!!!
[主管] INFO Room - 签发证明在哪里?
[主管] INFO Room - 签发证明已经拿到了!

这时有三种状态:

  1. 主管20秒等待中 施工方线程 先起来获取锁,将签发证明发放
    然后装修工成功检测到有签发证明,成功开工
    主管等待结束,同样获得了签发证明
  2. 主管20秒等待中,装修工线程 先起来获取锁,并检测是否有签发证明
    那么这时就会出现没有获得签发证明的现象
    装修工运行完毕后,施工方线程 才把签发证明发放;
    最后主管等待结束,获得签发证明
[主管] INFO Room - 签发证明还没有到位!!
[装修工4] INFO Room - 没有签发证明也能开工!干活!!!
[装修工3] INFO Room - 没有签发证明也能开工!干活!!!
[装修工2] INFO Room - 没有签发证明也能开工!干活!!!
[装修工1] INFO Room - 没有签发证明也能开工!干活!!!
[装修工0] INFO Room - 没有签发证明也能开工!干活!!!
[施工方] INFO Room - ------------签发证明发放了!
[主管] INFO Room - 签发证明在哪里?
[主管] INFO Room - 签发证明已经拿到了!
  • 解决:
package com.renex.c4;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.util.Date;
@Slf4j(topic = "Room")
public class Room {
    static final Object lock = new Object();
    static Boolean success = false;
    /**
     * 主管需要签发证明
     * @param args
     */
    @Test
    public void test2() throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                if (!success){
                    // 没有签发证明!
                    log.info("签发证明还没有到位!!");
                    try {
                        lock.wait(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("签发证明在哪里?");
                    if (success){
                        log.info("签发证明已经拿到了!");
                    }
                }else {
                    log.info("签发证明已经审批下来了!!");
                }
            }
        },"主管").start();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                synchronized (lock){
                    if (success){
                        log.info("开工!干活!!!");
                    }else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 判断等待后依旧获取不到签发证明做出的选择
                        if (success){
                            log.info("签发证明已到位开工!干活!!!");
                        }else {
                            log.info("签发证明仍然不到位,但也能开工!干活!!!");
                        }
                    }
                }
            },"装修工"+i).start();
        }
        new Thread(()->{
            synchronized (lock){
                lock.notifyAll();// 唤醒全部等待的线程
                success = true;
                log.info("------------签发证明发放了!");
            }
        },"施工方").start();
    }
}

注意!这里一定是要唤醒全部的线程!!使用notify()只会随机唤醒其中一个线程!这有可能导致装修工线程无法被唤醒从而一直等待!直到当程序结束后,它因为没有被唤醒从而一直在等待过程中而中止!

思考一个问题,如果没有做这种双重保护,重复判断是否到位。我们是否可以加上死循环来获取呢?

1.5.6.5 案例2-外卖与辣条

package com.renex.c4;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
@Slf4j(topic = "TakeOut")
public class TakeOut {
    static final Object lock = new Object();
    static Boolean success = false;
    static Boolean latiao = false;
    /**
     * 外卖与辣条
     * 张三需要外卖
     * 李四需要辣条
     */
    @Test
    public void test1() throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                while (!success){
                    try {
                        log.info("我的外卖还没到,再等等");
                        // 外卖还没有到,释放锁让其他线程送一下外卖
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (success){
                    log.info("我的外卖到了!!");
                }
            }
        },"张三").start();
        new Thread(()->{
            synchronized (lock){
                while (!latiao){
                    log.info("辣条还没到!!");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                if (latiao){
                    log.info("辣条到了");
                }else {
                    log.info("辣条飞了~!!!");
                }
            }
        },"李四").start();
        new Thread(()->{
            synchronized (lock){
                success = true;
                log.info("----------外卖已送到");
                lock.notifyAll();// 叫醒全部的人
            }
        },"外卖员").start();
    }
}

可以使用while来判断

synchronized(lock){
  // 如果条件不成立,那么就一直重复循环,不然就一直等待
    while(条件不成立){
        lock.wait();
    }
    // 线程执行到这里就一定判断是条件成立的
}
synchronized(lock){
    lock.notifyAll();
}

2. 👍JUC 专栏 - 前篇回顾👍

3. 💕👉 其他好文推荐

目录
相关文章
|
4月前
|
NoSQL 算法 Redis
【Docker】(3)学习Docker中 镜像与容器数据卷、映射关系!手把手带你安装 MySql主从同步 和 Redis三主三从集群!并且进行主从切换与扩容操作,还有分析 哈希分区 等知识点!
Union文件系统(UnionFS)是一种**分层、轻量级并且高性能的文件系统**,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem) Union 文件系统是 Docker 镜像的基础。 镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
613 5
|
4月前
|
存储 Java Linux
【Docker】(2)还在浏览网页寻找Docker命令?本文全面列举与使用Docker里的各个命令!想要什么命令直接从本文拿!
docker有着比VM更少的抽象层 由于Docker不需要Hypervisor实现硬件资源虚拟化,运行在Docker容器上的程序直接使用的都是实际物理机的硬件资源 因此在CPU、内存利用率上Docker将会在效率上有明显优势 docker利用的时宿主机的内核,而不需要加载操作系统OS内核 当新建一个容器时,Docker不需要和虚拟机一样重新加载一个操作系统内核 进而避免引寻、加载操作系统内核返回等比较费时费资源的过程,当新建一个虚拟机时,虚拟机软件需要加载OS,返回新建过程时分钟级别的。 而Docker由于直接利用宿主机的操作系统,则省略了返回过程,因此新建一个Docker容器只需
462 124
|
4月前
|
算法 Java 微服务
【SpringCloud(1)】初识微服务架构:创建一个简单的微服务;java与Spring与微服务;初入RestTemplate
微服务架构是What?? 微服务架构是一种架构模式,它提出将单一应用程序划分为一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。 每个服务允许在其独立的进程中,服务于服务间采用轻量级的通信机制互相协作(通常是Http协议的RESTful API或RPC协议)。 每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建
552 128
|
4月前
|
Java 测试技术 API
【JUC】(1)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
JUC是什么?你可以说它就是研究Java方面的并发过程。本篇是JUC专栏的第一章!带你了解并行与并发、线程与程序、线程的启动与休眠、打断和等待!全是干货!快快快!
706 2
|
4月前
|
存储 编译器 开发工具
01.C++基础语法
本教程系统讲解C++基础语法,涵盖语言特性、编译器使用、命名空间、头文件、修饰符与关键字等核心内容,助你快速掌握C++编程基础。
323 9
|
4月前
|
Java 编译器 Go
【Java】(5)方法的概念、方法的调用、方法重载、构造方法的创建
Java方法是语句的集合,它们在一起执行一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法在程序中被创建,在其他地方被引用方法的优点使程序变得更简短而清晰。有利于程序维护。可以提高程序开发的效率。提高了代码的重用性。方法的名字的第一个单词应以小写字母作为开头,后面的单词则用大写字母开头写,不使用连接符。例如:addPerson。这种就属于驼峰写法下划线可能出现在 JUnit 测试方法名称中用以分隔名称的逻辑组件。
252 4
|
4月前
|
JSON Java Go
【GoGin】(2)数据解析和绑定:结构体分析,包括JSON解析、form解析、URL解析,区分绑定的Bind方法
bind或bindXXX函数(后文中我们统一都叫bind函数)的作用就是将,以方便后续业务逻辑的处理。
328 4
|
4月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
254 0
|
4月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
247 1
|
4月前
|
Java Linux 虚拟化
【Docker】(1)Docker的概述与架构,手把手带你安装Docker,云原生路上不可缺少的一门技术!
1. Docker简介 1.1 Docker是什么 为什么docker会出现? 假定您在开发一款平台项目,您的开发环境具有特定的配置。其他开发人员身处的环境配置也各有不同。 您正在开发的应用依赖于您当前的配置且还要依赖于某些配置文件。 您的企业还拥有标准化的测试和生产环境,且具有自身的配置和一系列支持文件。 **要求:**希望尽可能多在本地模拟这些环境而不产生重新创建服务器环境的开销 问题: 要如何确保应用能够在这些环境中运行和通过质量检测? 在部署过程中不出现令人头疼的版本、配置问题 无需重新编写代码和进行故障修复
432 2