Java并发(面试)

作者:加菲猫 2018-06-17 173 0

进程、线程、协程

进程:是一个执行中的程序,是系统进行资源分配和调度的一个独立单位。

线程:是进程的一个实体,是 CPU 调度和分派的基本单位,也是比程序更小的、能独立运行的基本单位。

一个程序至少有一个进程,一个进程至少有一个线程。在执行过程中,进程有独立的内存单元,多个线程共享内存资源,可以减少切换次数,从而提高效率。同一进程中的多个线程,可以并发执行。

协程:使用程序切换,不消耗操作系统资源。协程是比线程更小的单位,一个线程中可以有多个协程。

守护线程和非守护线程

程序运行完毕,JVM 会等待非守护线程完成后关闭,不会等待守护线程。守护线程最经典的例子就是 GC 线程。

多线程上下文切换

是指 CPU 控制权由一个正在执行的线程,切换到另一个准备就绪并等待获取 CPU 控制权的线程的过程。

多线程的实现方式

实现 java.lang.Runable 接口。(推荐)

继承 java.lang.Thread 类,不推荐,Java 不支持多继承,应该留机会给其它类。

如何指定多个线程的执行顺序?

设定一个 orderNum,每个线程执行结束之后,更新 orderNum,知名下一个要窒息的线程,并且唤醒所有的等待线程。在每个线程的开始,使用 while 判断 orderNum 是否等于要求的值,相等执行,不相等 wait()。

使用 3 个线程依次打印 “ABC” 10 次

// 打印类public class LetterPrinter {        
    private char letter = 'A';        
    public void print() {
        System.out.print(letter);
    }        
    public void nextLetter() {        switch(letter) {            case 'A':
                letter = 'B';                break;            case 'B':
                letter = 'C';                break;            case 'C':
                letter = 'A';                break;
        }
    }        
    public char getLetter() {        return letter;
    }
        
}// 打印线程public class PrintRunnable implements Runnable {        
    private LetterPrinter letterPrinter = null;    private char letter = '';        
    public PrintRunnable(LetterPrinter letterPrinter, char letter) {        super();        this.letterPrinter = letterPrinter;        this.letter = letter;
    }        
    @Override
    public void run() {        for(int i = 0; i < 10; i++) {            synchronized(letterPrinter) {                while(letter != letterPrinter.getLetter()) {
                    letterPrinter.wait();
                }
                letterPrinter.print();
                letterPrinter.nextLetter();
                letterPrinter.notifyAll();
            }
        }
    }
        
}// 测试public class PrintThreadTest() {    
    public static void main(String[] args) {
        LetterPrinter letterPrinter = new LetterPrinter();
        
        ExecutorService service = Executors.newFixedThreadPool(3);
        service.execute(new PrintRunnable(letterPrinter, 'A'));
        service.execute(new PrintRunnable(letterPrinter, 'B'));
        service.execute(new PrintRunnable(letterPrinter, 'C'));
        
        service.shutdown();
    }
    
}

Thread 类中的 start() 和 run() 有什么区别?

start():用来启动新线程,内部调用了 run()。

run():只会在原来的线程中调用,没有新的线程启动。

FutureTask 是什么?

FutureTask 表示一个异步运算的任务。可以传入一个 Callable 的实现类,然后可以做对这个运算任务的结果进行等待、判断是否完成、取消等操作。由于 FutureTask 是 Runnable 接口的实现类,所以可以放入线程池中。

Runnable 和 Callable 的区别?

Runnable 中的 run() 没有返回值。

Callable 中的 call() 有返回值,是一个泛型,和 Future、FutureTask 配合可以获取异步执行的结果。

什么情况会导致线程阻塞?

sleep():以毫秒为参数,使线程在这段时间内阻塞,之后线程恢复可执行状态。例如(等待某个资源就绪):检测发现不满足条件,使用 sleep() 阻塞线程,过段时间后再检测,直到满足条件为止。

suspend() + resume():suspend() 使线程阻塞,不会自动恢复,使用 resume() 后,线程恢复可执行状态。例如(等待另一个线程产生的结果):检测发现结果还没有产生,使用 suspend() 阻塞线程,另一个线程产生结果后,调用 resume() 使其恢复。

yield():使当前线程放弃已分得的 CPU 控制权,但不使线程阻塞,随时可能再分得 CPU 控制权。

wait() + notify():wait() 使线程阻塞。传毫秒数作为参数时,调用 notify() 或超出时间后会恢复线程为可执行状态。不传参数时,必须调用 notify() 才能使线程恢复。

wait() 和 sleep() 的区别

比较项目 wait() wait(milliseconds) sleep()
来源 Object Object Thread
功能 阻塞线程 阻塞线程 阻塞线程
是否会释放锁
如何唤醒 notify() / notifyAll() 超出时间自动恢复 超出时间自动恢复

wait() + notify() 和 suspend() + resume() 的区别

wait() + notify():属于 Object 类,阻塞时会释放锁。要在 synchronized 方法或块中调用,只有这样当前线程才占有锁,才有锁可以被释放,不然会抛出 IllegalMonitorStateException 异常。调用 notify() 时,恢复的线程是从所有调用 wait() 阻塞的线程中随机选取的,notifyAll() 可以恢复所有线程,但是只有拿到锁的那一个线程进入可执行状态。

suspend() + resume():属于 Thread 类,阻塞时不会释放锁。

suspend() 和不知道时间的 wait() 都可能产生死锁。

为什么 wait() 和 notify() / notifyAll() 要在同步块中执行?

这是 JDK 强制的,因为调用前必须先获得对象的锁。

为什么 wait() 和 notify() / notifyAll() 不放在 Thread 类中?

因为 Java 提供的锁是对象级的,不是线程级的,每个对象都有锁,通过线程获得。当线程需要等待某些锁时,可以调用对象的 wait(),如果这个方法定义在 Thread 类中,线程就不知道正在等待的是哪个锁了。因为 wait() 和 notify() / notifyAll() 都是锁级别的操作,而锁又属于对象,所以要把它们定义到 Object 类中。

怎样检测一个线程是否持有对象监视器?

Thread 中的 holdsLock(Object obj) 方法,当对象的监视器被某个线程持有时,返回 true。这是一个 static 方法,所以 “某个线程” 指的就是当前线程。

wait() 和 notify() / notifyAll() 在放弃对象监视器时的区别

wait():立即释放对象监视器。

notify() / notifyAll():会等待线程剩余代码执行完毕,再释放对象监视器。

Java 中用到的线程调度算法是什么?

抢占式。一个线程用完 CPU 之后,操作系统会根据线程的优先级、饥饿情况等数据,计算出一个总的优先级,然后把下一个时间片给某个线程执行。

Thread.sleep(0) 的作用是什么?

由于 Java 采用抢占式的线程调度算法,因此可能会出现某一线程经常获取到 CPU 控制权的情况,为了让优先级较低的线程也能获取到 CPU 控制权,可以使用 Thread.sleep(0) 来手动触发一次操作系统分配时间片的操作,这是平衡 CPU 控制权的一种操作。

怎样唤醒一个阻塞的线程?

如果线程是因为调用了 wait()、sleep()、join() 导致阻塞的,可以中断线程,通过抛出 InterruptedException 来唤醒它。IO 阻塞无法唤醒,因为 IO 是操作系统实现的。

哪些情况会产生死锁?

互斥:一个资源每次只能被一个线程使用。

请求保持:一个线程因请求资源而阻塞时,对已请求的资源保持不放。

不剥夺:线程已获得资源,在未使用完之前,不能强行剥夺。

循环等待:若干线程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?

指定获取锁的顺序,比如某个线程只有获得 A 锁和 B 锁才能对某资源进行操作,规定只有获得 A 锁的线程才有资格获取 B 锁,按顺序获取就可以避免死锁。

线程中出现了运行时异常怎么办?

如果这个异常没有被捕获,线程就停止执行了。如果这个线程持有某个对象的监视器,也会被释放。

如何在线程间共享数据?

通过 wait() + notify() / notifyAll()、await() + signal() / signalAll() 进行等待和唤起。或者使用阻塞队列 BlockingQueue。

如何正确使用 wait()?该用 if 还是 while?

在循环中使用,因为当线程获取到 CPU 开始执行时,其它条件可能还不满足,所以在处理前要循环检查条件是否满足。

synchronized (obj) {    while(...) {        // 不满足执行条件
        obj.wait();
    }
}

什么是线程局部变量 ThreadLocal?作用是什么?

ThreadLocal 是局限于线程内部的变量,不在线程间共享,是实现线程安全的一种方式。但在管理环境下,如 web 服务器,工作线程的生命周期比其它应用的生命周期都长,一旦在工作完成后没有释放,就会存在内存泄漏的风险。

使用生产者 – 消费者模式有什么好处?

通过平衡生产者的生产能力和消费者的消费能力,来提升整个系统的运行效率。

解耦,生产者和消费者之间的联系少,可以独自发展,不受相互的制约。

实现一个生产者 – 消费者模型

使用阻塞队列实现

// 生产者public class Producer implements Runnable { 
    private final BlockingQueue<Integer> queue; 
    public Producer(BlockingQueue q) {        this.queue = q;
    }
 
    @Override    public void run() {        try {            while (true) {
                Thread.sleep(1000);// 模拟耗时
                queue.put(produce());
            }
        } catch (InterruptedException e) {
 
        }
    } 
    private int produce() {        int n = new Random().nextInt(10000);
        System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);        return n;
    }
     
}// 消费者public class Consumer implements Runnable { 
    private final BlockingQueue<Integer> queue; 
    public Consumer(BlockingQueue q) {        this.queue = q;
    }
 
    @Override    public void run() {        while (true) {            try {
                Thread.sleep(2000);// 模拟耗时
                consume(queue.take());
            } catch (InterruptedException e) {
 
            }
        }
    } 
    private void consume(Integer n) {
        System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n);
    }
     
}// 测试public class Main { 
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(100);
        Producer p = new Producer(queue);
        Consumer c1 = new Consumer(queue);
        Consumer c2 = new Consumer(queue); 
        new Thread(p).start();        new Thread(c1).start();        new Thread(c2).start();
    }
     
}

使用 wait() + notify() 实现

// 生产者public class Producer implements Runnable { 
    private final Vector sharedQueue;    private final int SIZE; 
    public Producer(Vector sharedQueue, int size) {        this.sharedQueue = sharedQueue;        this.SIZE = size;
    }
 
    @Override    public void run() {        for (int i = 0; i < 7; i++) {
            System.out.println("Produced:" + i);            try {
                produce(i);
            } catch (InterruptedException ex) {
             
            }
        }
    } 
    private void produce(int i) throws InterruptedException {        while (sharedQueue.size() == SIZE) {            synchronized (sharedQueue) {
                System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: " + sharedQueue.size());
                sharedQueue.wait();
            }
        }         
        synchronized (sharedQueue) {
            sharedQueue.add(i);
            sharedQueue.notifyAll();
        }
    }
     
}// 消费者public class Consumer implements Runnable { 
    private final Vector sharedQueue;    private final int SIZE;     
    public Consumer(Vector sharedQueue, int size) {        this.sharedQueue = sharedQueue;        this.SIZE = size;
    }
 
    @Override    public void run() {        while (true) {            try {
                System.out.println("Consumer: " + consume());
                Thread.sleep(50);// 模拟耗时
            } catch (InterruptedException ex) {
             
            }
        }
    } 
    private void consume(Integer n) throws InterruptedException {        while (sharedQueue.isEmpty()) {            synchronized (sharedQueue) {
                System.out.println("Queue is empty " + Thread.currentThread().getName() + " is waiting , size: " + sharedQueue.size());
                sharedQueue.wait();
            }
        }         
        synchronized (sharedQueue) {
            sharedQueue.notifyAll();            return (Integer) sharedQueue.remove(0);
        }
    }
     
}// 测试public class Main { 
    public static void main(String[] args) {
        Vector sharedQueue = new Vector();        int size = 4;
        Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer");
        Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer");
        prodThread.start();
        consThread.start();
    }
     
}

线程池

java.util.concurrent.ThreadPoolExecutor 类就是一个线程池,客户端调用 submit(Runnable task) 提交任务。

  • 当前线程池大小:实际工作线程的数量。

  • 最大线程池大小(maxinumPoolSize):允许存在的工作线程的数量上限。

  • 核心线程大小(corePoolSize):不大于最大线程池大小的工作线程数量上限。

如果运行的线程少于 corePoolSize,Executor 添加新的线程,而不进行排队。

如果运行的线程等于的多于 corePoolSize,Executor 将请求加入队列,而不是添加新线程。如果队列已满,则创建新的线程,除非线程数超过了 maxinumPoolSize,这时任务将被拒绝。

为什么要是有线程池?

避免频繁的创建和销毁线程,实现线程对象的重用,还可以控制并发数。

提交任务时线程池队列已满,会发生什么?

如果使用的是无界队列,如 LinkedBlockingQueue,可以继续添加任务到阻塞队列。 如果使用的是有界队列,如 ArrayBlockingQueue,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。

CAS 是什么?

CAS 就是比较-替换(Compare And Swap)。当多个线程操作一个静态变量时,为了防止操作被覆盖,可以使用 synchronized 同步锁,但性能较低。使用原子操作类可以提高效率,在 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类,如 AtomicInteger、AtomicBoolean、AtomicLong 等,使用这些包装类声明变量,既能保证线程安全,又能获得较高的性能。

原子操作类的底层就是利用了 CAS 机制,CAS 机制的底层是利用了 unsafe 类提供的 compareAndSwap(),此方法有 4 个参数:要修改的对象、要修改变量的偏移量、变量修改之前的值、变量修改后的值。

假设有三个操作数:内存值-V、旧的预期值-A、要修改的值-B,当且仅当 V 和 A 相同时,才会将内存值修改为 B,并返回 true,否则什么都不做,并返回 false。

  • 在内存地址 V 中,存储着值为 10 的变量。

  • 线程-1 想把变量的值增加 1。对 线程-1 来说,旧的预期值 A = 10,要修改的值 B = 11。

  • 在 线程-1 提交更新之前,线程-2 就抢先一步,把内存地址 V 中的变量率先改成了 11。

  • 线程-1 开始提交更新,首先比较 旧的预期值 A 和地址 V 中的实际值,发现 A 不等于 V 的实际值,提交失败。

  • 线程-1 重新获取内存地址 V 中的当前值,并重新计算想要修改的新值 B。此时对 线程-1 来说,A = 11,B = 12。这个重新尝试的过程叫做自旋。

  • 这一次比较幸运,没有其它线程改变地址 V 中的值。线程-1 进行 Compare,发现 A 等于 V 的实际值。

  • 线程-1 进行 Swap,把地址 V 中的值替换为 B,也就是 12。

CAS 有哪些缺点?如何解决?

CPU 开销大:在并发量较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给 CPU 带来很大压力。

不能保证代码块的原子性:CAS 机制只能保证一个变量的原子性,如果需要保证多个变量共同进行原子性更新,就不得不使用 synchronized 了。

ABA 问题:小明有 100 元存款,要取出 50 元,但是提款机故障,启动了 2 个线程,都是要获取当前的 100,修改为 50。线程-1 首先执行成功,当前余额是 50 元,线程-2 因为某种原因被阻塞了。这时,小明的妈妈正好给小明转账 50 元,启动了 线程-3,线程-3 成功执行,把余额修改为 100 元。线程-2 恢复执行,阻塞前已经获取了内存值 100,经过 Compare 检查,此时实际存款也是 100,于是成功把余额改成了 50。这就需要在 Compare 阶段,不光要比较期望值 A 和地址 V 中的实际值,还要比较变量的版本号是否一致。AtomicStampedReference 类就实现了用版本号做比较的 CAS 机制。

  • 在内存地址 V 中,存储着值为 A 的变量,当前版本号是 01。线程-1 获得了当前值 A 和版本号 01,想更新为 B,但是被阻塞了。

  • 内存地址 V 中的变量发生了多次变化,版本号提升为 03,但变量值仍是 A。

  • 线程-1 恢复执行,进行 Compare 操作。经过比较,线程-1 之前所获得的值和地址 V 的实际值都是 A,但版本号不一致,所以更新失败。

Java 中有哪几种锁?

自旋锁:JDK 1.6 之后默认开启。因为共享数据的锁定状态只会持续很短的时间,为了这一小段时间挂起和恢复线程有些浪费,所以就让后面请求锁的线程再等一下,但是不放弃 CPU 的控制权,看看持有锁的线程能否快速释放。为了让线程等待,需要让它执行一个忙循环,也就是自旋操作。等待时间不固定,由上次在同一个锁上的自旋时间及锁的拥有者状态来决定。

偏向锁:JDK 1.6 之后引入的锁优化,为了消除在无竞争环境下的同步操作,进一步提升性能。偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其它线程获取,则持有偏向锁的线程就不需要同步。偏向锁可以提高带有同步但无竞争的程序性能,如果程序中大多数锁都被多个不同的线程访问,就起不到作用了。

轻量级锁:为了减少获得和释放锁产生的性能消耗,引入了偏向锁和轻量级锁。锁一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,随着竞争情况升级。锁可以升级,但不能降级。也就是说,一旦升级为轻量级锁,就不能再回到偏向锁状态了。

synchronized 的实现原理

进入时,执行 monitorenter,将计数器 + 1,释放时,计数器 -1。当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用,否则线程进入等待状态。

synchronized 是在加对象锁,对象锁是一种重量锁(monitor)。根据线程的竞争情况,会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争)、自旋锁等。

synchronized 修饰静态方法、普通方法、代码块的区别

普通方法:给当前实例加锁,进入同步代码前要获得当前实例的锁。

静态方法:给当前类对象加锁,进入同步代码前要获得当前类对象的锁。由于类对象存在于永久代,因此相当于全局锁。

代码块:给指定对象加锁,进入同步代码前要获得指定对象的锁。

synchronized 和 Lock 的区别

比较项目 synchronized Lock
类型 关键字
释放锁 已获取锁的线程执行完同步代码 / 线程执行发生异常,释放锁 在 finally 中必须释放锁,不然容易造成死锁
获取锁 线程-1 获得锁,线程-2 等待,如果 线程-1 阻塞,线程-2 一直等待 有多个获取锁的方式,不需要一直等待
锁状态 无法判断 可以判断
锁类型 可重入、不可中断、非公平 可重入、可中断、可公平
性能 少量同步 大量同步

synchronized 和 ReentrantLock 的区别

synchronized:是关键字。操作的是对象头中的关键字。

ReentrantLock:是类。是基于 AQS 实现的。ReentrantLock 作为类,有锁投票、定时锁、中断锁。

AQS 是什么?

在 AQS 内部会保存一个状态变量 state,通过 CAS 修改该变量的值,修改成功的线程表示获取到该锁,修改失败或者发现 state 已经是加锁状态,则使用一个 Waiter 对象封装线程,添加到等待队列中,并挂起,等待被唤醒。

乐观锁和悲观锁

乐观锁:认为竞争不总是会发生,因此不需要持有锁。就像 CAS 机制,将比较-替换作为一个原子操作,尝试去修改内存中的变量,如果失败则表示发生冲突,进行重试。

悲观锁:认为竞争总是会发生,因此每次对资源进行操作时,都会持有一个独占的锁,就像 synchronized。

CyclicBarrier 和 CountDownLatch 的区别

都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上。

  • CyclicBarrier 中的线程运行到某个点上之后,该线程停止运行,只有所有线程都运行到这个点上后,所有线程才重新运行。CountDownLatch 中的线程运行到某个点上之后,只是给某个数值 -1,该线程继续运行。

  • CyclicBarrier 只能唤起一个任务。CountDownLatch 可以唤起多个任务。

  • CyclicBarrier 可重用。CountDownLatch 不可重用,计数值为 0 时,该 CountDownLatch 就不可用了。

Java 中的 ++ 操作符是线程安全的吗?

不是线程安全的。它涉及到多个指令,如读取变量、增加、然后存储回内存,整个过程可能会出现多个线程交叉。

SimpleDateFormat 是线程安全的吗?

DateFormat 的所有实现类都不是线程安全的,不应该在多线程中使用,除非是在对外线程安全的环境中,如将 SimpleDateFormat 限制在 ThreadLocal 中。推荐使用 joda-time 库。

如何实现单例模式?

要想让一个类只能构建一个对象,自然不能让它随便 new,因此构造方法时私有的,但是要提供一个获取对象的方法。

public class Singleton {    private Singleton() {}    
    private static Singleton instance = null; // 懒汉模式    
    public static Singleton getInstance() {        if(instance == null) {            instance = new Singleton();
        }        return instance;
    }
    
}

这个单例不是线程安全的,因此在 new 对象前加上 synchronized 同步锁,锁住整个类,之后还要做一次判空,叫做双重检测机制。

public class Singleton {    private Singleton() {}    
    private static Singleton instance = null; // 懒汉模式    
    public static Singleton getInstance() {        if(instance == null) {            synchronized(Singleton.class) {                if(instance == null) {                    instance = new Singleton();
                }
            }
        }        return instance;
    }
    
}

这个单例有可能会获取到没有被初始化完成的对象,因为JVM 会对指令进行重排,instance = new Singleton(); 会被编译成 3 个指令:

  • memory = allocate(); // 1:分配对象内存空间

  • ctorInstance(memory); // 2:初始化对象

  • instance = memory; // 3:设置 instance 指向刚分配的内存地址 经过 JVM 的优化,第 3 步会在第 2 步之前进行,此时 instance 对象还未完成初始化,但已经不指向 null 了。if(instance == null) 返回 false,从而返回一个没有初始化完成的 instance 对象。

public class Singleton {    private Singleton() {}    
    private volatile static Singleton instance = null; // 懒汉模式    
    public static Singleton getInstance() {        if(instance == null) {            synchronized(Singleton.class) {                if(instance == null) {                    instance = new Singleton();
                }
            }
        }        return instance;
    }
    
}

volatile 可以防止 JVM 的指令重排。私有的静态内部类也可以实现单例模式。

public class Singleton {    
    private static class LazyHolder {        
        private static final Singleton INSTANCE = new Singleton();
        
    }    
    pricate Singleton() {}    
    public static Singleton getInstance() {        return LazyHolder.INSTANCE;
    }
    
}

上边的单例都可以用反射打破,因为构造器可以设置为访问。

// 获得构造器Constructor con = Singleton.class.getDeclaredConstructor();// 设置位可访问con.setAccessible(true);// 构造两个不同的对象Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();// 验证是否是不同对象System.out.print(singleton1.equals(singleton2));

用枚举实现单例模式,获取构造器时,会抛出 NoSuchMethodException 异常。

public class SingletonEnum {
    INSTANCE;
}
比较项目 双重锁检测 静态内部类 枚举
是否线程安全
是否懒加载
是否防止反射构建

可以创建 Volatile 数组吗?

可以,不过只是一个指向数组的引用,不是整个数组。如果改变引用指向的数组,将受到 volatile 的保护,当多个线程同时改变数组的元素,volatile 标识符就不能起到保护作用了。

volatile 能使一个非原子操作变成原子操作吗?

能。由于 long 和 double 类型都是 64 位的,所以读取操作分为 2 部分,写读前 32 位,再读后 32 位,这个过程不是原子的。当一个类中有 long 或 double 类型的成员变量,且它们会被多个线程访问时,使用 volatile 修饰变量,可使读写变成原子操作。

volatile 类型的变量

避免指令重排 JVM 为了获得更好的性能会对语句进行重排,volatile 类型的变量即使在没有同步块的情况下赋值,也不会与其它语句重排。

保证可见性

  • 读 volatile 变量之前,Java 内存模型会插入一个读屏障,保证所有线程都能看见写的值。

  • 写 volatile 变量之前,Java 内存模型会插入一个写屏障,保证所有线程写的值都能被看见。

提供原子性 读 64 位的数据类型时,如 long 和 double,可避免分两次读取。

发表评论