文章目录
JUC 面试题
谈谈 volatile
volatile 是由 Java 虚拟机提供的一种轻量级的同步机制。
- 保证可见性
- 不保证原子性
- 禁止重排序
可见性验证
class MyData {
// int number = 0;
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
}
public class VolatitleDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\tcome in");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
myData.addTo60();
System.out.println(Thread.currentThread().getName()+"\tupdate number value " + myData.number);
}, "AAA").start();
while (myData.number == 0) {
// main 一直等待
}
System.out.println(Thread.currentThread().getName()+"\t mission is over");
}
}
为什么不保证原子性
以 20 个线程各自执行 1000 次 number++ 操作为例:
class MyData {
// int number = 0;
volatile int number = 0;
public void addPlusPlus() {
number++;
}
}
public class VolatitleDemo {
public static void main(String[] args) throws InterruptedException {
MyData myData = new MyData();
// CountDownLatch countDownLatch = new CountDownLatch(20);
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
}
// countDownLatch.countDown();
}, String.valueOf(i)).start();
}
// countDownLatch.await();
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("现在的 number : " + myData.number);
}
}
运行结果:
现在的 number : 17860
运行多次发现 number 无法达到 20000。
原因:由于各个线程对 number 进行操作需要先从主内存复制 number 到工作内存,然后将 number 加 1 再写回工作内存,此时可能出现多个线程取得相同得 number 值(比如 20),加 1 后为21,各个线程重复将 21 写回主内存,造成最终结果偏小。
如何解决原子性
- 使用 synchronized
- 使用原子类 atomicInteger
JMM(Java 内存模型)
JMM 本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。
可见性
由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。Java 内存模型规定所有变量都存储在主内存,主内存是共享区域,所有线程可以访问。但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再写回主内存,写回主内存后必须第一时间通知其他线程:这个共享变量的值修改了。这种能够第一时间通知的机制就叫 Java 内存模型里的可见性。
原子性
有序性
DCL (Double Check Lock)版本单例模式
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
但是!由于有指令重排序的存在,还是可能会存在很小的几率出现问题!加入 volatitle 可以禁止指令重排序。

如果不加 volatile ,有一种情况:在 A 线程,当 instance 已经被分配内存,并且 instance 已经指向分配的内存空间(此时 instance 还没有初始化,但是 instance != null),此时 B 线程进入方法,instance != null,获得了一个还没有初始化完成的 instance,B 线程使用 instance 的时候就会出现问题!所以需要给 instance 添加 volatile 关键字来禁止指令重排序。
CAS(CompareAndSwap)
CAS 是一条 CPU 并发原语。
它的功能是判断内存某个位置是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS 并发原语体现在在 JAVA 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现 CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现原子操作。(CAS 是一种系统原语,属于擦操作系统层面)
AtomicInteger 底层是调用 Unsafe 类方法。
Unsafe 类
unsafe.getAndAddInt(this, valueOffset, 1);
- this:当前对象
- valueOffset:内存偏移地址(有了当前对象和内存偏移量,Unsafe 就可以直接操作内存中的数据)
/**
* var1: 对象
* var2: 进行 CAS 操作变量的内存偏移地址
* var4: 在原变量上需要加上的值
* var5: 变量当前值
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); // 通过对象和内存偏移地址获取变量当前值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 判断变量值是否被修改,如果没有就加上对应值
return var5;
}
举例

CAS 的缺点
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
- ABA 问题
ABA问题
举例
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
}, "thread01").start();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "thread02").start();
}
}
AtomicReference 使用
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 22);
User li4 = new User("li4", 25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get());
System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get());
}
}
ABA 问题解决
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("=========以下是ABA问题的产生==========");
new Thread(() -> {
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
}, "thread01").start();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "thread02").start();
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("=========以下是ABA问题的解决==========");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号: " + stamp);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号: " + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号: " + atomicStampedReference.getStamp());
}, "thread03").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号: " + stamp);
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t修改成功否: "+result+"\t当前最新版本号: "+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t当前实际最新值: "+atomicStampedReference.getReference());
}, "thread04").start();
}
}
集合类不安全问题
-
故障现象
java.util.ConcurrentModificationException
-
导致原因
在多线程环境下,一个线程正在遍历集合类,另一个线程同时修改该集合类。(非迭代器遍历)
-
解决方案
-
使用 Vector
-
使用 Collections.synchronizedList()
原理:使用 synchronizedCollection 包装传入的集合类,将传入的集合类作为 mutex 信号量的值,执行Collections 接口的方法时会锁传入的集合类
-
使用 CopyOnWriteArrayList
-
-
优化建议
ArrayList 并发修改异常 举例:
public class ContainerNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}, String.valueOf(i)).start();
}
// java.util.ConcurrentModificationException
}
}
公平锁和非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获得锁。
- 非公平锁:如果线程来的时候资源已经被占用,则进入等待队列,资源锁释放时会唤醒队头线程**(此时等待队列中线程获得锁的顺序等同于公平锁)**。与公平锁的不同点在于:如果线程来的时候资源刚好释放,则来的线程可以直接获得资源。

可重入锁(递归锁)
可重入锁就是递归锁。
可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
作用:避免死锁
LockSupport
LockSupport:加强版的线程等待唤醒机制(wait / notify)
原理
底层是调用 Unsafe 的 native 方法

使用及对比
public class LockSupportDemo {
static Object objectLock = new Object();
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
/**
* LockSupport
*/
public static void main(String[] args) {
Thread a = new Thread(() -> {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"------come in"+System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒"+System.currentTimeMillis());
}, "A");
a.start();
Thread b = new Thread(() -> {
/**
* 通知可以在阻塞之前执行,先通知了阻塞将无效
*/
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"\t"+"------通知了");
}, "B");
b.start();
}
/**
* Lock
*/
private static void lockAwaitSignal() {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
condition.await();
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
} finally {
lock.unlock();
}
}, "B").start();
}
/**
* synchronized
*/
private static void synchronizedWaitNotify() {
new Thread(() -> {
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}
}, "A").start();
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
}, "B").start();
}
}
本文探讨了Java并发编程中的关键概念,包括volatile的可见性和原子性问题,Java内存模型(JMM)的特性,DCL单例模式的潜在风险,CAS操作的原理与优缺点,ABA问题及其解决方案,以及集合类的并发安全性问题。同时,讲解了公平锁与非公平锁、可重入锁的概念,和LockSupport在多线程同步中的作用。
1万+

被折叠的 条评论
为什么被折叠?



