二、Java并发编程之ReentrantLock、Java内存模型

B站黑马课程

4. AQS

只使用一把锁时,锁住整个对象

class BigRoom {
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}

可以设置多把细粒度锁,提高并发度,潜在风险是死锁

class BigRoom {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    public void sleep() {
        (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}

4.1 锁的活跃性

死锁

示例

t1 线程获得 A对象 锁,接下来想获取 B对象的锁
t2 线程获得 B对象 锁,接下来想获取 A对象的锁

定位死锁

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

jps	#查看java线程id
jstack 34628	#查看指定id的线程

最终观察其中有如下内容

"t2" #23 prio=5 os_prio=0 tid=0x0000016d4ec3b000 nid=0x5bb0 waiting for monitor entry [0x0000000decffe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.ConcurrentApplication.lambda$main$1(ConcurrentApplication.java:33)
        - waiting to lock <0x000000076ff202f0> (a java.lang.Object)
        - locked <0x000000076ff20300> (a java.lang.Object)
        at com.example.ConcurrentApplication$$Lambda$4/1590550415.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"t1" #22 prio=5 os_prio=0 tid=0x0000016d4ec3a000 nid=0x87a4 waiting for monitor entry [0x0000000deceff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.ConcurrentApplication.lambda$main$0(ConcurrentApplication.java:23)
        - waiting to lock <0x000000076ff20300> (a java.lang.Object)
        - locked <0x000000076ff202f0> (a java.lang.Object)
        at com.example.ConcurrentApplication$$Lambda$3/392292416.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
...

"t2":
        at com.example.ConcurrentApplication.lambda$main$0(ConcurrentApplication.java:23)
        - waiting to lock <0x000000076ff20300> (a java.lang.Object)
        - locked <0x000000076ff202f0> (a java.lang.Object)
        at com.example.ConcurrentApplication$$Lambda$3/392292416.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

解决方法之一:顺序加锁

哲学家就餐问题

在这里插入图片描述

演示

class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }
    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();

活锁

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

演示

public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

饥饿

常定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

4.2 AQS

源码没看明白,总结一下大概流程

  • AQS 就是一个抽象类,可用来在多线程环境下构建锁,比如ReentranctLock就是基于它的独占锁。我对它的了解不是非常深入,只能大概说一下。它有两个非常重要的组件,一个表示锁状态的state,在构建锁时可以根据自己的需要定义state的含义,比如ReentrantLock里面就是state=0时表示没有加锁,state=1表示加锁,state>1表示被冲入了。另外一个重要组件就是一个双向队列,用来存储等待锁的线程。当第一个线程来获取锁时,非公平条件下它会尝试通过CAS操作去改变state的值,如果成功说明锁空闲,失败就以CAS操作加入到队列的尾部,等待它的前一个线程结点来唤醒它。

  • AQS可以实现独享锁和共享锁。比如ReentrantLock就是独占锁,它又可以分为公平锁和非公平锁,公平锁按照队列的顺序获取锁,非公平锁就是当新的线程来到时,它先去尝试获取一下,获取不到再入队。共享锁有countDownLatch之类的,会定义一个初始计数器,表示可共享的个数,具体不是很了解

4.3 ReentrantLock

ReentrantLock主要基于CAS和AQS实现,支持公平锁和非公平锁

ReentrantLock原理

ReentrantLock 类内部总共存在SyncNonfairSyncFairSync三个类,NonfairSync与 FairSync类继承自 Sync类,Sync类继承自 AbstractQueuedSynchronizer (AQS) 抽象类

详见:https://blog.csdn.net/weixin_42039228/article/details/123135122

ReentrantLock基础

基本语法

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

相对于 synchronized 它具备如下特点

  • 可中断

    • synchronized是不可中断的,也就是说锁除了自身放弃,是不能被其他线程夺走的
    private static ReentrantLock lock = new ReentrantLock();
    try {
        //如果没有竞争那么此方法就会获取lock对象锁
        //如果有竞争就进入阻塞队列,可以被其他线程使用 interrupt 方法打断
        lock.lockInterruptibly();
    } catch (InterruptedException e) {
        e.printStackTrace();
        return;
    }
    
  • 可以设置超时时间

    tryLock:在规定时间内获取锁

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] agrs) {
        Thread t1 = new Thread(()->{
            log.debug("尝试获取锁");
            try {
                //tryLock()无参表示获取一次
                if(!lock.tryLock(2, TimeUnit.SECONDS)) {//在2秒内尝试获取锁
                    log.debug("获取锁失败");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try{
                //临界区
                log.debug("获取锁成功");
            }finally{
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        t1.start();
        sleep(1);
        lock.unlock();
    }
    
  • 可以设置为公平锁

    • 很少设置,因为会降低并发度
  • 支持多个条件变量 (await / signal)

    • synchronized 中也有条件变量,当条件不满足时进入 waitSet 等待
      ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的

      static ReentrantLock lock = new ReentrantLock();
      public static void main(String[] agrs) {
          //创建一个新的条件变量
          Condition condition1 = lock.newCondition();
          Condition condition2 = lock.newCondition();
          lock.lock();//先加锁
          //进入条件变量condition1中等待
          condition1.await();
          //叫醒阻塞在condition1中的线程
          condition1.signal();
      }
      
    • 使用要点

      • await 前需要获得锁
      • await 执行后,会释放锁,进入 conditionObject 等待
      • await 的线程被唤醒(或打断、或超时)后重新竞争 lock 锁
      • 竞争 lock 锁成功后,从 await 后继续执行

ReentrantLock 与 synchronized 一样,都支持可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
    如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

ReentrantLock解决哲学家就餐

使用tryLock(),先获取左筷子,再获取右筷子。如果右筷子获取失败,会释放左筷子

package com.example;

@Slf4j(topic = "c.Test")
public class ConcurrentApplication{
    public static void main(String[] agrs) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

class Chopstick extends ReentrantLock{
    String name;
    public Chopstick(String name){this.name=name;}
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread{
    Chopstick left;
    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 {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

4.4 同步模式之顺序控制

固定顺序

需求:要求先运行线程2,再运行线程1

1. wait/notify方案

static final Object lock = new Object();
static boolean t2runned = false;//判断t2是否运行过

public static void main(String[] agrs) {
    Thread t1 = new Thread(() -> {
        synchronized (lock){
            while(!t2runned){
                try{
                    lock.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            log.debug("1");
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        synchronized (lock){
            log.debug("2");
            t2runned = true;
            lock.notify();
        }
    }, "t2");

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

2. pack/unpack方案

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();

*交替输出

需求:线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

1. wait/notify方案

  • 使用一个整型变量 flag 标识当前该谁执行了
@Slf4j(topic = "c.Test")
public class ConcurrentApplication {
    public static void main(String[] agrs) {
        WaitNotify waitNotify = new WaitNotify(1, 5);
        new Thread(() -> {
            waitNotify.print("a", 1, 2);
        }, "t1").start();
        new Thread(() -> {
            waitNotify.print("b", 2, 3);
        }, "t2").start();
        new Thread(() -> {
            waitNotify.print("c", 3, 1);
        }, "t3").start();
    }
}

class WaitNotify{
    private int flag;//等待标记,1,2,3表示不同线程
    private int loopNumber;//循环次数

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

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

2. await/signal方案

@Slf4j(topic = "c.Test")
public class ConcurrentApplication {
    public static void main(String[] agrs) {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        new Thread(() ->{
            awaitSignal.print("a", a, b);
        }, "t1").start();
        new Thread(() ->{
            awaitSignal.print("b", b, c);
        }, "t2").start();
        new Thread(() ->{
            awaitSignal.print("c", c, a);
        }, "t3").start();
        Thread.sleep(1000);
        awaitSignal.lock();
        try{
            System.out.println("开始!");
            a.signal();
        }finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock{
    private int loopNumber;
    public AwaitSignal(int loopNumber){this.loopNumber = loopNumber;}
    public void print(String str, Condition current, Condition next){
        for(int i=0; i<loopNumber; ++i){
            lock();
            try{
                current.await();
                System.out.print(str);
                next.signal();
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                unlock();
            }
        }
    }
}

3. park/unpark方案

@Slf4j(topic = "c.Test")
public class ConcurrentApplication {
   static Thread t1, t2, t3;
   public static void main(String[] agrs) {
       ParkUnpack pu = new ParkUnpack(5);
       t1 = new Thread(() ->{
           pu.print("a", t2);
       }, "t1");
       t2 = new Thread(() ->{
           pu.print("b", t3);
       }, "t2");
       t3 = new Thread(() ->{
           pu.print("c", t1);
       }, "t3");
       t1.start();
       t2.start();
       t3.start();
       LockSupport.unpark(t1);
   }
}

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

5. 共享模式之内存

Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性
除此之外,共享变量还有可见性有序性的问题

JMM(Java Memory Model,Java内存模型) 体现在以下几个方面

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

重点注意

JVM内存模型Java内存模型是不一样的

  • Java内存模型
    • 规定所有的变量都是存在主存中,每个线程都有自己的工作内存
    • 线程堆变量的操作都必须在工作内存进行,不能直接堆主存进行操作,并且每个线程不能访问其他线程的工作内存
    • Java内存模型重点在,Volatile`关键字,原子性、可见性、有序性
  • JVM内存模型
    • 和Java虚拟机的运行时区域有关

5.1 可见性

static boolean run = true;	//添加volatile
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(run){
        // ....
        }
	});
    t.start();
    sleep(1);
    run = false; // 线程t不会如预想的停下来
}

JVM优化时,会将循环超过1w次的代码作为热点代码
JVM会把热点代码的字节码编译成机器码放到方法区,下次执行时直接执行对应的机器码来提高执行效率
因此这里修改为false无用

在while中加入 log.debug(“d”); 即可停止下来
另外:println是synchronized修饰的

可见性的产生原因
在这里插入图片描述

  • t 线程频繁从主内存中读取 run 的值,JIT 编译器于是将 run 值缓存至自己工作内存(CPU缓存)中的高速缓存中,以提高效率
  • main修改了主存中的 run 值,然而t线程不去主存中读取,因此感知不到 run 值的修改

解决方案:volatile

volatile

volatile static boolean run = true

可以用来修饰成员变量静态成员变量,避免线程从自己的工作缓存中查找变量的值,强制到主存中获取它的值

  • 加锁synchronized也可以避免可见性的问题

    static boolean run = true;
    final static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(true){
                synchronized (lock){
                    if(!run){
                        break;
                    }
                }
            }
        });
        t.start();
        sleep(1);
        synchronized (lock){
            run = false;
        }
    }
    

可见性 vs 原子性

  • violate只保证可见性,并不保证原子性

  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性
    但缺点是synchronized 是属于重量级操作,性能相对更低

5.2 终止模式之两阶段终止模式

之前的两阶段终止模式是通过 interrupt 实现的
这里使用violate改进

@Slf4j(topic = "c.test")
public class ConcurrentApplication {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();

        Thread.sleep(3500);
        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread monitor;
    private volatile boolean stop = false;

    public void start(){
        monitor = new Thread(()->{
            while(true){
                Thread current = Thread.currentThread();
                if(stop){
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);//如果在这里sleep被打断,将进入catch里面
                    log.debug("执行监控记录");
                }catch (InterruptedException e){
                }
            }
        });
        monitor.start();
    }

    public void stop(){
        stop = true;
        monitor.interrupt();//使得stop后立即停止
    }
}

5.3 同步模式之犹豫模式

Balking (犹豫)模式

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

上面监控线程存在的问题:如果创建2个监控线程,那么这两个线程将在同时刻打印监控信息,导致重复
需求:使得监控方法 start() 只执行一次

@Slf4j(topic = "c.test")
public class ConcurrentApplication {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        tpt.start();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread monitor;
    private volatile boolean stop = false;

    //判断是否执行过 start()
    private boolean starting = false;

    public void start(){
        //犹豫模式:Balking
        synchronized (this){
            if(starting) return;
            starting = true;
        }
        monitor = new Thread(()->{...});
        monitor.start();
    }

    public void stop(){
        stop = true;
        monitor.interrupt();//使得stop后立即停止
    }
}

常用在web开发中,这样前端即便点击多次start按钮,也能保证仅有一个监控程序

此外还可以用在实现单例模式

public final class Singleton {
    private Singleton() {
    }
    private static Singleton INSTANCE = null;
    public static synchronized Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

5.4 有序性

指令重排序优化

指令重排:JVM 会在不影响正确性的前提下,调整语句的执行顺序

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

此时无论是先执行 i 还是先执行 j,对结果没有影响
这种情况下,JVM可能对上面代码的执行顺序进行重排

指令重排的原因和原理

  • 现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令

  • 每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段

    在这里插入图片描述

  • 在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行

    在这里插入图片描述

    • 现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线
    • 这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令)
    • 本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令的吞吐率。

指令重排举例

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;//r是一个只有r1成员变量的对象
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

问:最终 r1 的结果为多少?这里分析一种奇怪的结果

  • r1 = 0

    线程2中进行指令重排,使得 ready = true 在 num = 2 之前执行,就会导致 r1 = 0

禁止指令重排 - volatile

volatile boolean ready = false;

5.5 volatile原理

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

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

保障可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

保障有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

5.6 double-checked locking 问题

以著名的 double-checked locking 单例模式为例

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

问题:第一次 if(INSTANCE == null) 在 synchronized 之外,有指令重排的危险性

原理:

0: getstatic 		#2 				// 获得静态变量INSTANCE
3: ifnonnull 		37				// 判断是否null,不为null,跳转到37行
6: ldc		 		#3 				// 获取类对象Singleton.class
8: dup								// 复制引用地址
9: astore_0							// 将复制的引用地址存入寄存器
10: monitorenter					// 进入同步代码块
11: getstatic 		#2			 	// 获得静态变量INSTANCE
14: ifnonnull 27					// 判断是否null,不为null,跳转到27行(拿出类对象,用于解锁)
17: new 			#3				// new Singleton();
20: dup								// 复制一份新创建对象的地址
21: invokespecial 	#4 				// 调用构造方法
24: putstatic 		#2 				// 将创建的对象赋值给静态变量
27: aload_0							// 类对象解锁
28: monitorexit
29: goto 			37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic 		#2
40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21,即先赋值再构造。此时可能出现
线程t1执行同步代码块,线程t2在INSTANCE未被构造的情况下获取到了它,然后正常使用。结果是使用了一个未构造的对象,导致报错

synchronized保护的共享变量是可以保障原子性、可见性、有序性的,但是这里的INSTANCE因为有部分在synchronized之外,因此可能出问题

解决方案

private static volatile Singleton INSTANCE = null;

是通过读写屏障阻止了重排序而实现的

5.7 happens-before

参考:https://www.jianshu.com/p/b9186dbebe8e

happens-before原则

  • 如果操作1 happens-before 操作2,那么第操作1的执行结果将对操作2可见,而且操作1的执行顺序排在第操作2之前
  • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法

如何判断是否为 happens-before?

  • 线程解锁 lock 之前对变量的写,对于接下来对 lock 加锁的其它线程的读可见
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待
    它结束)
  • 线程 t1 打断 t2(interrupt)前对变量的写,在打断之后对其他线程读可见(通过t2.interrupted 或 t2.isInterrupted)
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x -> y 并且 y -> z 那么有 x -> z ,

5.8 习题

balking 模式习题

希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?

public class TestVolatile {
    volatile boolean initialized = false;
    void init() {
        if (initialized) {
            return;
        }
        doInit();
        initialized = true;
    }
    private void doInit() {
    }
}

有问题:只能保障可见性,不能保障原子性,可以改用synchronized

线程安全单例习题

实现1

饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

public final class Singleton implements Serializable {
    private Singleton() {}
    private static final Singleton INSTANCE = new Singleton(); 
    public static Singleton getInstance() {
        return INSTANCE;
    }
    public Object readResolve() {
        return INSTANCE;
    }
}

问题1:类为什么加 final?

  • 表明此类不能被继承,可以防止子类重写其中的方法导致单例被破坏

问题2:如果实现了序列化接口(implements Serializable), 还要做什么来防止反序列化破坏单例?

关于序列化和反序列化:https://zhuanlan.zhihu.com/p/340258358

  • 增加 readResovle() 方法

    //固定名称的方法,在反序列化中如果发现readResolve()返回了一个对象,就会使用这个对象,而非反序列化后生成的对象
    public Object readResolve(){
        return INSTANCE;
    }
    

问题3:构造方法为什么设置为私有? 是否能防止反射创建新的实例?

  • 防止对象创建破坏单例;
  • 不能,反射可以获取Constructor,通过set方法暴力修改

问题4:这里INSTANCE的初始化是否能保证单例对象创建时的线程安全?

  • 是线程安全的
  • 静态变量是在类加载阶段初始化的,类加载阶段会由JVM保障线程安全

问题5:为什么提供静态方法getInstance而不是直接将 INSTANCE 设置为 public, 说出你知道的理由

  • 方法可以提供更好的封装性
  • 可以实现一些懒惰的初始化,有更多的控制
  • 可以提供一些泛型的支持
实现2

枚举方式实现的单例

enum Singleton {
	INSTANCE;
}

问题1:枚举单例是如何限制实例个数的

  • 反编译后可以看到,实际上INSTANCE也是一个 public final static 类型,是单实例的

问题2:枚举单例在创建时是否有并发问题

  • 无并发问题。同样由于是静态变量,会在初始化时由JVM保障线程安全性

问题3:枚举单例能否被反射破坏单例

  • 不能

问题4:枚举单例能否被反序列化破坏单例

  • 不能。默认继承了Serializable接口,在实现时考虑到了这个问题,做了相应措施

问题5:枚举单例属于懒汉式还是饿汉式

  • 饿汉式

问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做

  • 编写一个构造方法
实现3

懒汉式的单例

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;

    public static synchronized Singleton getInstance() {
        if( INSTANCE != null ){
            return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

注意 synchronized 不要加在 INSTANCE 上,一个是因为它是null,另外synchronized需要加在不变的对象上,即final

分析这里的线程安全, 并说明有什么缺点

  • 锁范围大,性能低
实现4

DCL

public final class Singleton {
    private Singleton() { }
    private static volatile Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton.class) {
            if (INSTANCE != null) {
                return INSTANCE;
            }
            INSTANCE = new Singleton();
            return INSTANCE;
        }
    }
}

问题1:解释为什么要加 volatile ?

  • 防止重排序

问题2:对比实现3, 实现4的写法的意义

  • 性能更优,非第一次调用可直接返回

问题3:为什么要第二次加空判断, 之前不是判断过了吗

  • 首次创建对象时,两个线程同时锁住了Singleton.class,不加第二次判断就可能创建多个INSTANCE
实现5
public final class Singleton {
    private Singleton() { }
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

问题:属于懒汉式还是饿汉式

  • 懒汉式。类加载本身就是懒惰的,如果不调用 getInstance(),是不会触发静态内部类的

问题:在创建时是否有并发问题

  • 无并发问题,JVM保障了其线程安全
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值