(Java并发编程——JUC)共享问题解决与synchronized对象锁分析!全程干货!!快快收藏!!

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线程中的栈帧内。而且,也并没有组合使用方法,因此不会出现安全问题。

  4. Thread thread = new Thread(() -> {
        int amount = window.sell(rand);
        amounts.add(amount);
    }, "t" + i);
    

    主要来看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;
        }
    }
    

    可以看到这里有判断,有赋值操作,在并发情况下就会出现所属,而且count变量是一个成员变量;

  5. 找到问题所在:TicketWindow类内部的sell方法

  6. 解决办法:

    synchronized public int sell(int amout){
        // 判断剩余票数是否大于要卖出的票数
        if (this.count >=amout){
            this.count -= amout;
            return amout;
        }else {
            return 0;
        }
    }
    

    为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锁记录地址
执行同步块A00(轻量锁)线程1锁记录地址
访问同步块B,把Mark复制到线程A的锁记录00(轻量锁)线程1锁记录地址
CAS修改Mark为线程A锁记录地址00(轻量锁)线程1锁记录地址
失败(发现是自己锁的)00(轻量锁)线程1锁记录地址
锁重入00(轻量锁)线程1锁记录地址
执行同步块B00(轻量锁)线程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锁记录地址
执行同步块A00(轻量锁)线程1锁记录地址
执行同步块A00(轻量锁)线程1锁记录地址访问同步块A,把Mark复制到线程2的
执行同步块A00(轻量锁)线程1锁记录地址CAS 修改Mark为线程2锁记录地址
执行同步块A00(轻量锁)线程1锁记录地址失败(发现其他线程已经占用锁)
执行同步块A00(轻量锁)线程1锁记录地址CAS修改Mark为重量级锁
执行同步块A10(重量级)重置锁指针阻塞中
执行完毕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(重量锁)
访问同步块,获取monitor10(重量锁)重置锁指针
成功(加锁)10(重量锁)重置锁指针
执行同步块10(重量锁)重置锁指针
执行同步块10(重量锁)重置锁指针访问同步块,获取monitor
执行同步块10(重量锁)重置锁指针自旋重试
执行完毕10(重量锁)重置锁指针自旋重试
成功(解锁)10(重量锁)自旋重试
10(重量锁)重置锁指针成功(加锁)
10(重量锁)重置锁指针执行同步块
1.5.3.2 自旋重试失败情况
线程1(cpu1上)对象Mark线程2(cpu2上)
10(重量锁)
访问同步块,获取monitor10(重量锁)重置锁指针
成功(加锁)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队列阻塞太多线程,也会拖慢程序的运行效率

  2. BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片

  3. BLOCKED线程会在Owner线程释放锁时唤醒

  4. 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. 💕👉 其他好文推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值