JUC P2 可重入锁,JMM 基础+代码

JUC P2 可重入锁,JMM

教程:https://www.bilibili.com/video/BV16J411h7Rd

5. 可重入锁(ReentrantLock)

相对于 synchronized 具备的特点:

  • 可以打断(一个线程可以取消另外一个线程获取的锁,为了防止死锁且不能打断的情形,lockInterruptibly()
  • 可以设置超时时间(规定时间内获取不到锁,放弃锁的争抢,执行其他逻辑)
  • 可以设置为公平锁(减少饥饿,默认状态下是不公平的)
  • 支持多个条件变量(支持多个 WaitSet)

与 synchronized 一样支持可重入(同一个线程对同一把锁多次获取)

用法:

// 获取锁
reentrantLock.lock();
try {
    // 临界区。。。
} finally {
    // 释放锁
    reentrantLock.unlock();
}

Note:
为什么不把 reentrantLock.lock(); 放到 try { } 中?

  • 因为不确定获取锁是否成功,如果获取锁失败,解锁的时候会出现异常

Note:
管程(Monitor)实现区别:

  • ReentrantLock 基于 Java 实现的管程
  • synchronized 基于 JVM(C++)实现的管程

5.1 可打断

@Slf4j(topic = "c.InitTest")
public class InitTest {
    private static final ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                log.debug("尝试获取锁...");
                reentrantLock.lockInterruptibly();
            } catch (InterruptedException e) {
                log.debug("没有获取到锁...");
                throw new RuntimeException(e);
            }
            try {
                log.debug("获取锁...");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        reentrantLock.lock();
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        log.debug("打断 t1...");
        t1.interrupt();
    }
}

在这里插入图片描述

5.2 锁超时

t1 线程若 1 s 后能获取到锁,执行临界区;若 1 s 后不能获取到锁,直接返回 false:

@Slf4j(topic = "c.InitTest")
public class InitTest {
    private static final ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获取锁...");
            // 若 1 s 后能获取到锁,执行临界区;若 1 s 后不能获取到锁,直接返回 false
            try {
                if (!reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("没有获取到锁...");
                    return;
                }
            } catch (InterruptedException e) {
                log.debug("没有获取到锁...");
                throw new RuntimeException(e);
            }
            try {
                log.debug("获取到锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        reentrantLock.lock();
        log.debug("获取到锁...");
        t1.start();
        reentrantLock.unlock();
        log.debug("释放锁");
    }
}

在这里插入图片描述

锁超时实现哲学家就餐问题

不会发生死锁:

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("c1");
        Chopstick c2 = new Chopstick("c2");
        Chopstick c3 = new Chopstick("c3");
        Chopstick c4 = new Chopstick("c4");
        Chopstick c5 = new Chopstick("c5");
        new Philosopher("张三", c1, c2).start();
        new Philosopher("尼古拉斯·赵四", c2, c3).start();
        new Philosopher("王二麻子", c3, c4).start();
        new Philosopher("职业法师·刘海柱", c4, c5).start();
        new Philosopher("IKUN", c5, c1).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    private final Chopstick left;
    private final Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() {
        log.debug("eating...");
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void run() {
        while (true) {
            if (left.tryLock()) {
                try {
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }
}

class Chopstick extends ReentrantLock{
    private final String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}

在这里插入图片描述

5.3 公平锁

ReentrantLock 默认是非公平锁(源码):

public ReentrantLock() {
  sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

Note:
公平锁一般没有必要,会降低并发度

5.4 支持多个条件变量

synchronized 中也有条件变量:waitSet,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量可以支持多个条件变量:也就是说 synchronized 不满足条件的线程只能到一个 waitSet 中,而 ReentrantLock 支持多个 waitSet,唤醒的时候可以根据不同类别的 waitSet 进行唤醒

Note:
synchronized 只有一个休息室,ReentrantLock 有多个休息室。

  • await() 执行的条件是当前线程已经获得锁
  • await() 执行后,会释放锁,进入 conditionObject (休息室,类似 waitSet)等待
  • await() 的线程被唤醒(或打断(signal)、或超时)会重新竞争锁
  • 竞争锁成功后,从 await() 后的代码继续执行

Note:
这里省略一个例子:P127

5.5 设计模式

5.5.1 顺序控制

要求两个线程按顺序执行,先执行线程 2,再执行线程 1。

wait / notify 版本
@Slf4j(topic = "c.InitTest")
public class InitTest {
    private static final Object lock = new Object();
    private static boolean t2runned = false;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!t2runned) {
                    try {
                        lock.wait();
                        log.debug("1");
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log.debug("2");
                t2runned = true;
                lock.notify();
            }
        }, "t2");

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

在这里插入图片描述

Note:
其实 Join 也能实现类似的效果,比如可以在 t1 线程中调用 t2.join()

  • 有个弊端,假如 t2 在执行完能满足 t1 线程执行的条件后,又执行一些其他的事情,那么 t1 线程就得等 t2 线程执行完全部的事情才能继续执行。
await / signal 版本
@Slf4j(topic = "c.InitTest")
public class InitTest {
    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                condition.await();
                log.debug("1");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("2");
                condition.signal();
            } finally {
                lock.unlock();
            }
        }, "t2");
        
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

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

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

在这里插入图片描述

5.5.2 交替执行

三个线程交替执行输出结果 abcabcabcabcabc

  • 第一个线程输出 a
  • 第二个线程输出 b
  • 第三个线程输出 c
wait / notify 版本
@Slf4j(topic = "c.InitTest")
public class InitTest {
    private static final Object lock = new Object();
    private static int flag = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            int constI = i;
            char output = (char) (i + 'a');
            new Thread(() -> {
                synchronized (lock) {
                    for (int j = 0; j < 5; j++) {
                    	// 若当前不是本线程的标记,进入 wait
                        while (flag != constI) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        log.debug("{}", output);
                        // 修改为下一个线程执行的标记
                        flag = (flag + 1) % 3;
                        lock.notifyAll();
                    }
                }
            }, "t" + (i + 1)).start();
        }
    }
}

在这里插入图片描述

await / signal 版本
@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        List<Condition> conditions = new ArrayList<>();
        // 添加条件变量
        for (int i = 0; i < 3; i++) {
            conditions.add(awaitSignal.newCondition());
        }
        // 三个线程
        for (int i = 0; i < 3; i++) {
            int constI = i;
            new Thread(() -> {
                awaitSignal.print("" + (char)('a' + constI), conditions.get(constI), conditions.get((constI + 1) % 3));
            }, "t" + (i + 1)).start();
        }
        // 主线程启动
        TimeUnit.SECONDS.sleep(1);
        awaitSignal.lock();
        try {
            conditions.get(0).signal();
        } finally {
            awaitSignal.unlock();
        }
    }
}

@Slf4j(topic = "c.AwaitSignal")
class AwaitSignal extends ReentrantLock {
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    public void print(String msg, Condition cur, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            lock();
            try {
                cur.await();
                log.debug(msg);
                next.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                unlock();
            }
        }
    }
}

在这里插入图片描述

park / unpark 版本
@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        ParkVersion parkVersion = new ParkVersion(5);
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            int constI = i;
            threads.add(new Thread(() -> {
                parkVersion.print("" + (char) ('a' + constI), threads.get((constI + 1) % 3));
            }, "t" + (i + 1)));
        }
        threads.forEach(Thread::start);

        // 主线程启动
        TimeUnit.SECONDS.sleep(1);
        LockSupport.unpark(threads.get(0));
    }
}

@Slf4j(topic = "c.ParkVersion")
class ParkVersion {
    private int loopNumber;
    public ParkVersion(int loopNumber) {
        this.loopNumber = loopNumber;
    }

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

在这里插入图片描述

6. Java 内存模型 JMM

JMM,定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

JMM 体现在以下几个方面:

  • 原子性:保证指令不会收到线程上下文切换的影响
  • 可见性:保证指令不会受到 CPU 缓存的影响
  • 有序性:保证指令不会受到 CPU 指令并行优化的影响

6.1 可见性

可见性:一个线程对共享变量的修改,更够及时的被其他线程看到。

先提出两个问题:

第一个问题:下面这段代码会一直执行,为什么?:

private static boolean run = true;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        while (run) {
            int a = 1;
        }
    }, "t").start();

    TimeUnit.SECONDS.sleep(1);
    log.debug("停止 t 线程");
    run = false;
}

在这里插入图片描述

第二个问题:为什么加了锁就更能符合逻辑正常运行呢?

private static boolean run = true;
private static final Object obj = new Object();

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        while (run) {
            synchronized (obj) {
                int a = 1;   
            }
        }
    }, "t").start();

    TimeUnit.SECONDS.sleep(1);
    log.debug("停止 t 线程");
    run = false;
}

在这里插入图片描述

Note:
Java 把内存分为主存(共享信息)和工作内存(线程独享)

  • 主内存就是常说的堆区,方法区(元空间)
  • 工作内存就是虚拟机栈和本地方法栈,PC 寄存器
  1. 初始状态:t 线程刚开始从主存读取了 run 的值到工作内存
    在这里插入图片描述
  2. 因为 t 线程要频繁从内存中读取 run 的值,JIT 编译器会将 run 的值缓存到自己工作内存中的高速缓存(Cache)中,以减少对主存中 run 的访问,提高效率
    在这里插入图片描述
  3. 1s 后主线程修改了 run 的值,并同步至主存,而 t 线程是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述

第一个问题可以解答了:一个线程对主内存的数据进行修改,对另外一个线程不可见。

第一个问题如何解决呢,即如何保证一个线程修改主内存的数据对其他线程可见?

  • volatile 关键字(英文翻译为易变,不稳定的)
  • volatile 只能修饰成员变量和静态成员变量,不能修饰局部变量(因为局部变量线程私有)
  • 该关键字可以避免线程从自己的工作缓存中查找变量的值,必须到主内存获取值,线程操作 volatile 变量都是直接操作主存

Note:
关于 volatile 了解更多可以参考:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html

  • 可见性实现原理:通过内存屏障实现,若变量被修改后,会立刻将变量由工作内存回写到主存中
  • volatile 还有个功能:防止指令重排序(以后细说)

加上 volatile 之后,表示不能再从高速缓存中读取了,每次都从主内存拿:

private static volatile boolean run = true;

6.2 原子性和可见性

原子性:不可被中断的一个或一系列操作

volatile 可以保证在多个线程之间,一个线程对变量的修改对另外一个线程可见。但是不能保证原子性,仅可使用在一个写线程,多个读线程的情况。

若两个线程一个 i++,一个 i–,只能保证每次操作看到最新值,但是不能解决指令交错。

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。 缺点就是相对更加重量,性能相对较低。

Note:
上面第二个问题提前尝试回答一下:加了锁会刷新工作内存,破坏 JIT 即时编译器的优化,因此会重新从主内存拿数据

6.3 终止模式之两阶段种植模式

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        MonitorThread monitorThread = new MonitorThread();
        monitorThread.start();

        TimeUnit.SECONDS.sleep(5);
        monitorThread.stop();
    }
}

@Slf4j(topic = "c.MonitorThread")
class MonitorThread {
    private Thread monitor;
    private volatile boolean stop = false; // 加上 volatile 保证 stop 变量可见性

    // 启动监控
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                if (stop) {
                    log.debug("准备退出......");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1); // case 1 :该处被打断, 打断标记还是 false
                    log.debug("执行监控记录");          // case 2 : 该处被打断, 打断标记置为 true
                } catch (InterruptedException e) {
                }
            }
        });
        monitor.start();
    }

    // 停止监控
    public void stop() {
        stop = true;
        monitor.interrupt();// 防止线程睡眠, 应该让线程直接退出
    }
}

在这里插入图片描述
注意:其实该例子中不加 volatile 也是可以的,因为判断语句块中的 log.debug() 语句源码里面会自动加锁,使 JIT 即时编译器不生效 。

  • 后来通过实验我发现其实 sleep() 也有类似的效果,大家可以自己尝试一下。

6.4 同步模式之 Balking 犹豫模式

在一个县城发现另一个线程或者本线程已经做了某一件相同的事情,那么本线程就无需再做了,直接结束返回。

下面代码中调用两次 monitorThread.start(),只会执行一个线程中的内容,因为第二次调用没必要再创建线程了,因为第一次调用已经创建相同的线程任务去执行了:

@Slf4j(topic = "c.InitTest")
public class InitTest {
    public static void main(String[] args) throws InterruptedException {
        MonitorThread monitorThread = new MonitorThread();
        monitorThread.start();
        monitorThread.start(); // 第二次调用

        TimeUnit.SECONDS.sleep(5);
        monitorThread.stop();
    }
}

@Slf4j(topic = "c.MonitorThread")
class MonitorThread {
    private Thread monitor;
    private volatile boolean stop = false; // 加上 volatile 保证 stop 变量可见性

    private volatile boolean starting = false; 

    // 启动监控
    public void start() {
        // 加上一个标记即可,尽可能让同步代码块短一些
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }
        monitor = new Thread(() -> {
            while (true) {
                if (stop) {
                    log.debug("准备退出......");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1); // case 1 :该处被打断, 打断标记还是 false
                    log.debug("执行监控记录");          // case 2 : 该处被打断, 打断标记置为 true
                } catch (InterruptedException e) {
                }
            }
        });
        monitor.start();

    }
    // 停止监控
    public void stop() {
        stop = true;
        monitor.interrupt();// 防止线程睡眠, 应该让线程直接退出
    }
}

在这里插入图片描述

Note:
同步代码块内的可见性可以由 synchronized 保证,同步代码块外的可见性必须由 volatile 保证。

6.5 有序性

JVM 会在不影响正确性的前提下,可以调整语句执行的顺序:

static int i;
static int j;

// 某个线程下
i = ...; // 1
j = ...; // 2

i 和 j 谁先执行对最终结果都不会影响,因此代码真正执行可以是先执行 1 再执行 2,也可以是先执行 2 再执行 1。

这种特性称之为 指令重排,多线程下指令重排会影响正确性。

Note:
为什么要有指令重排?

  • 现代 CPU 支持多级指令流水线,可以同时支持 取指令-指令译码-执行指令-内存访问-数据写回 的处理器,CPU 在一个时钟周期内可以同时执行五条指令的不同阶段。流水线技术不能缩短单条指令的执行时间,但变相提高了指令吞吐率。
  • 因此,在不改变结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令并行

在这里插入图片描述

解决重排序:volatile 关键字

6.6 volatile 原理

volatile 实现的底层原理是内存屏障(Memory Barrier)。

  • volatile 变量的写指令之后加入写屏障
  • volatile 变量的读指令之前加入读屏障
保证可见性
  1. 写屏障保证在该屏障之前,对共享变量的改动,都同步到主存当中。
num = 2;
ready = true; // ready 是 volatile 变量
// 写屏障
  1. 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据。
// 读屏障
if (ready) { // ready 是 volatile 变量
	r1 = num + num;
} else {
	r1 = 1;
}
保证有序性
  1. 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  2. 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
不能解决指令交错
  • 写屏障只能保证之后的读能读取到最新的结果,不能保证写屏障之前的读
  • 有序性只保证本线程内的相关代码不被重排序

在这里插入图片描述

双检锁问题
class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {
    }

    // 提供一个静态方法,返回实例化对象, 加入同步处理代码块
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

getInstance 方法字节码:
在这里插入图片描述

  • 17 表示创建对象,将对象引用压入操作数栈
  • 20 表示复制一份引用再压入操作数栈
  • 21 表示弹出一个对象引用调用构造方法
  • 24 表示弹出一个对象引用赋值给 static INSTANCE

Note:为什么会在 new 之后进行 dup?
可以参考:https://blog.csdn.net/dabusiGin/article/details/104701338
一个对象创建在执行构造函数的时候会消耗一个引用

JVM 有可能把把顺序优化为:先执行 24,再执行 21,两个线程并发执行时序图:
在这里插入图片描述

这样就会导致 t1 线程还没有进行构造初始化的时候,t2 使用对象导致空指针异常。

解决方案

加上 volatile 关键字:

private static volatile Singleton INSTANCE;

Note:
synchronizedvolatile 有序性的区别:

  • synchronized 的有序性是持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生重排序,使块与块之间有序可见。(块和块之间的有序性)
  • volatile 的有序性是通过插入内存屏障来保证指令按照顺序执行。不会存在后面的指令跑到前面的指令之前来执行。是保证编译器优化的时候不会让指令乱序。(指令和指令之间的有序性)

6.7 happens-before 规则

happens-before 规定了对共享变量的写操作对其他线程的读操作可见,它是可见性和有序性的一套规则。如果抛开该规则,JMM 并不能保证一个线程对共享变量的写,以及对其他线程的读可见。

  • 线程解锁之前对变量的写,对于接下来加锁的其他线程对该变量的读可见
  • 线程对 volatile 变量的写,对接下来其他线程对该变量的读可见
  • 线程 start() 前对变量的写,对该线程开始后对该变量的读可见
  • 线程结束前对变量的写,对其他线程得知他结束后的读可见(比如,t1.join() 等待结束)
  • 线程 t1 打断 t2 前对变量的写,对于其他线程得知 t2 被打断后对该变量的读可见
  • 对默认值(0,false,null)的写,对其他线程对该变量的读可见
  • 传递性,volatile 变量防止重排序可以影响到其他变量,会把线程内的变量修改都同步到主存

补充图

Note:
存储设备层次结构: 在这里插入图片描述

图2 来自 https://xiaolincoding.com/os/1_hardware/storage.html#cpu-cache
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇咔咔负负得正

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值