一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情
JUC是Java面试中的重点,知识点很多很杂,总是学完一遍之后就感觉自己会了,但是过了一段时间之后又似乎不懂了。可能有几个原因
-
内容太多,没有关联
-
经验不足,没有理解各自的使用场景 在JAVA并发工具包(juc)里,总结起来就是VCSL
-
V是volatile
-
C是CAS
-
S是synchronized
-
L是JDK实现的工具类Lock
这几个都可以处理并发,用于不同的场景。
虽然有些看起来原理很发杂,但并没有真的很复杂,理解了之后,它们也就只是一个纸老虎🐯,只是一些简单的套路和编程思想。我编了一些顺口溜来帮助更简单的理解和记忆。
- JMM缓存和寄存
- 无法保证可见性
- V能保证可见性
- 写读穿透到主存
- volatile的原理
- 指令前缀加lock
- 内存屏障保有序
- 还能防止重排序
- CAS自旋锁
- 比较并修改变量
- 底层硬件原子性
- 不需切换非阻塞
- ABA问题加版号
- AQS的基础
- Syn使用很简单
- 对象上面加把锁
- 加锁使用防并发
- 包装使用很清晰
- 方法修饰是this
- 要说原理也不难
- 字节码 monitor来支持
- enter加锁exit解
- Java对象头Mark Word
- Mark Word包含锁标记
- 无锁升级偏向锁
- 轻量再变重量级
- Lock原理并不难
- AQS来实现
- 多重功能来设计
- 条件共享公平否
如果这200字左右还不足以回想起所有的并非知识,可以看看下面的具体分析和讲解。
Volatile
Volatile的出现主要是因为JMM模型。每个线程都存在自己的高速缓冲。
- JMM缓存和寄存
- JMM是指java内存模型
- 无法保证可见性
- 因为缓存的原因,修改过后的变量可能不会马上写回到主存
happens-before原则
- JMM天然有序性
- Java 内存模型,虽然有这个概念,但是可以认为所有的语言都是这样的一个编程模型。各个线程使用自己独有的栈空间,线程局部变量存在于独有的栈,公共的变量使用字节码的save&load去存取。
- 遵循happens-before原则
- happens-before原则虽然看起来很多,但是对于我们来说,就是自然而然的事情。
- 程序顺序-可重排
- 单线程结果能正确
- 多线程结果可混乱
保证可见性
当一个Volatile变量,被赋值的时候,这个变量就被马上写到主存(save)。其他变量读这个变量的时候(load)也是从主从同步。
-
V能保证可见性
-
写读穿透到主存
-
防止指令重排序
- 不影响最终结果的时候,指令在运行的时候是可以重排序的,
-
volatile的原理
-
指令前缀加lock
-
内存屏障保有序
-
还能防止重排序
常用demo
用于单例的lazy实现如下:
class Singleton{
volatile static Singleton instance = null;
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
volatile变量instance被赋值的时候能马上写到内存,其他线程也能马上读到,也能防止指令重排异常。
程序的原子性
我们学习机器码的时候,一定知道一条机器码的执行是原子的。 如果不是原子的,则很有可能不一致。
- 不能保证指令原子性
volatile int i=2;//
i++;//读i,+1,写回主存
但是多步操作则不是原子的,比如上面这个就有3步,i++包含读,修改,写回几个步骤。 volatile 不能保证原子性,这个时候还是需要加锁。
volatile总结
volatile使用了特殊的指令前缀和内存屏障:
- 保证了变量的可见性
- 防止指令重排序 开销小!
CAS自旋-不是真的锁
CAS是提供的比较并交换的API:
-
CAS自旋锁
-
比较并修改变量
-
硬件原子来保证
-
不需切换非阻塞
- 不需要切换CPU,是非阻塞的
-
ABA问题加版号
- ABA问题,如果需要的话,可以使用版本号技术解决。
-
AQS原理的基础 总结来说,CAS一般比较简单,是一种编码的技术,不加锁,使用一个while循环去修改变量,直到修改成功。可能会造成比较大的CPU开销。
原理
AtomicInteger类的原子修改方法:
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe.compareAndSwapInt为什么是原子性的?
原子性是由硬件指令实现的,底层硬件通过将 CAS 里的多个操作在硬件层面语义实现上,通过一条处理器指令保证了原子性操作。在IA64,x86 指令集中有cmpxchg指令完成 CAS 功能,在大多数处理器上 CAS 都是个非常轻量级的操作,这也是其优势所在。
Synchronized真加锁
Synchronized是JDK早期版本就开始提供的并发加锁方法。
- S 使用很简单
- 对象上面加把锁
- 加锁使用防并发
使用
对象,类对象都是对象,类对象是指Class是类的对象。
- 包装使用很清晰
- 方法修饰是this
- 静态方法类对象
- 代码块也是指对象
public synchronized void method() {
System.out.println("Hello World!");
}
public static synchronized void method() {
System.out.println("Hello World!");
}
Object obj=new Object();
public void method() {
synchronized(obj){
System.out.println("Hello World!");
}
}
-
要说原理有点杂
-
但是其实也不难
-
字节码 monitor来支持
-
enter加锁exit解
-
Java对象头Mark Word
- Mark Word包含锁标记
- 无锁升级偏向锁
- 轻量再变重量级
markword起始于对象头offset=0的位置,在64位虚拟机上占8个字节。
升级过程
- 未锁偏向都标记01
- 首次加锁试偏向
- 偏向标记设为1
- 初次竞争升轻量00
- 轻量自旋来加锁
- 栈中锁记录指针记
- 再次竞争加重锁10
- 互斥量指针记录记
- 重锁未得先阻塞
- 阻塞切到内核态
- hash码和年龄哪里去
- 解锁时候恢复来
原来存monitor指针的地方或栈上面的指针存放了这个对象的hashcode等信息,解锁的时候这个对象处于无锁的状态了,也可以从锁记录指针恢复这些信息了。
Java Lock-AQS来实现
那么synchronized这么厉害,到底有没有什么缺点呢? synchronized比较简单粗暴,能支持的功能有限。主要有以下几个方面:
- 加锁不难中断
- 加锁不支持条件
- 不是公平锁
- 不支持共享锁
Java Lock的功能很丰富灵活、能支持这些。
使用
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
try {
// do something...
}finally {
lock.unlock();
}
}
原理
-
Lock原理并不难
-
AQS是基础
-
利用状态和队列
-
帮助实现JAVA锁 AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。 AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列),提供tryAcquire和tryRelease方法给具体的锁实现使用。
- 接口实现花样多
- 一般是说可重入
- 等待队列 Condition
- 还可考虑公平锁
- 当然还有读写锁
- 加锁异常怎么办?
- 中断情况要考虑
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
具体实现可看看源码。
总结
本来就是用极少的概括性的文字来概括JUC机制,虽然没有讲具体的源码实现,但在使用和原理上给出了关联性、帮助理解和记忆。有些具体的地方感兴趣还可以继续探究,比如AQS的设计原理。