文章目录
1、Lock
java.util.concurrent
包简称 JUC,Lock 就在这个包下的 locks
包下。包含如下方法:
void lock()
:获取锁void lockInterruptibly()
:可中断的获取锁boolean tryLock()
:尝试非阻塞的获取锁,立即返回booleanboolean tryLock(long time, TimeUnit unit)
:功能同上,超时的获取锁void unlock()
:释放锁Condition newCondition()
:这是一个获取等待通知的组件
2、CAS
CAS(Compare And Swap)比较并交换 是一种原子操作,用于实现多线程环境下的同步操作。 CAS 操作包含三个操作数:内存位置(V)、旧的预期值(A)和新值(B)。当且仅当预期值 A 和内存位置 V 的值相同时,CAS 会将内存位置 V 的值更新为新值 B。
主要包含以下三个步骤:
- 读取:读取内存位置V的值。
- 比较:将读取到的值与期望的原值A进行比较。
- 交换:如果读取到的值等于A,则将内存位置V的值设置为新值B;否则开始 自旋(Spinning)来持续尝试重新执行CAS操作,直到成功为止。
无锁的概念:
- 乐观派:无锁,总是认为对临界资源的访问没有冲突。一旦发生冲突则采用 CAS 技术来保证线程安全。这就是乐观锁。
- 悲观派:加锁,总是认为对临界资源的访问存在冲突。synchronized 就是悲观锁。
◎ CAS 实现的基石:Unsafe 类
CAS 想要保证线程是安全的,一个实现的关键就是要保证 比较并交换 是一个原子操作。
在 Java 里面就是用 Unsafe 类来实现 CAS 的原子操作,它通过 JNI(Java Native Interface;Java本地接口) 方式去访问本地的 C++库,从而使得 Java 拥有能够直接操作内存空间的能力。
◎ CAS 在 Java 中的应用
比如:java.util.concurrent.atomic
包、java.util.concurrent.locks
包下面的一系列的类。
在 JDK1.6+,synchronized 升级为重量级锁之前,也是采用的 CAS 机制。
◎ CAS 的经典三大问题
- CAS 采用自旋的方式,循环性能开销大,浪费CPU资源
- 优化方案:在Java中,很多使用自旋CAS的地方,会有一个自旋次数的限制,超过一定次数,就停止自旋。
- 不能保证代码块的原子性,只能保证一个变量的原子性操作
- 优化方案1:可以考虑改用 juc 包中的锁来保证操作的原子性。
- 优化方案2:使用原子类,或可以考虑合并多个变量,将多个变量封装成一个对象,通过 AtomicReference 来保证原子性。
- 优化方案3:使用 asynchronized
- ABA问题:并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
怎么解决 ABA问题?加版本号。
每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。
Java提供了 AtomicStampReference 类,它的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。
3、AQS
AbstractQueuedSynchronizer 抽象同步队列,简称 AQS ,它是 Java并发包的根基,并发包中的锁就是基于AQS实现的。
AQS能干什么?
- 同步队列的管理和维护
- 同步状态管理
- 线程阻塞、唤醒管理
独占锁:也叫互斥锁、排它锁,就是一个线程获得了锁,那么其他想要获取这把锁的线程就得等待。
共享锁:可以多个线程同时获取的锁,典型的如:读/写锁里的读锁。
- AQS是一个一个 FIFO 的双向队列(基于 CLH 队列实现),Node 节点中的 thread 变量用来存放进入 AQS队列里的线程,SHARED 表示是获取共享资源时被阻塞挂起后放入 AQS队列的, EXCLUSIVE 表示是获取独占资源时被挂起后放入 AQS队列的。
- AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 state 值时通过 CAS 机制来保证修改的原子性。
- 获取 state 的方式分为两种,独占方式
tryAcquire()
和共享方式tryAcquireShared()
, 一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过 CAS 的方式进行获取,tryAcquire()
和tryAcquireShared()
是抽象方法,需要子类自行实现。 - 如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到队列中,线程进入队列后会进行自旋,自旋一定次数后,会使用
LockSupport.park()
进入阻塞状态。 - 获取到锁的线程可以重入,每重入一次,state+1,释放资源的时候,会使用 CAS 操作将 state 修改为 0,重入多少次,释放多少次,并使用
LockSupport.unpark()
唤醒处于等待状态的线程。
CLH 是什么:AQS 是 CLH(Craig、Landin、Hagersten)锁定队列的变体。CLH队列结构如下:
● 用法
使用这个类用作同步的基础上,重新定义以下方法,如适用,通过检查和/或修改使用所述同步状态 getState()
, setState(int)
和/或 compareAndSetState(int, int)
:
● 独占锁和共享锁实现差异
▼ 出队差异
正常情况下,独占锁,只有持有锁的线程运行结束了,释放锁后独占锁的节点才会出队。
共享锁,是在唤醒了下一个节点,并且被唤醒的节点成功被设置为 head 节点后,自己这个节点就出队了。
▼ 唤醒差异
独占锁,只有在释放锁的时候,才会去看看要不要唤醒下一个节点。
共享锁,是在两个地方去看看是不是要唤醒下一个节点;一个是在获取锁的过程中调用 setHeadAndPropagate
方法的时候,另一个是释放锁的过程中调用 unparkSuccessor
方法的时候.
4、Demo 锁
4.1、synchronized 实现锁(Low)
1、简单锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyLock implements Lock {
private boolean hasLocked = false;
@Override
public synchronized void lock() {
while(hasLocked){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(!hasLocked)
hasLocked = true;
}
@Override
public synchronized void unlock() {
if(hasLocked) {
hasLocked = false;
notifyAll();
}
}
// 其余 Override 略(主要是没写)
}
2、可重入锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyReentrantLock implements Lock {
private boolean hasLocked = false;
private Thread current = null;
private int count = 0;
@Override
public synchronized void lock() {
Thread entryThread = Thread.currentThread();
while(hasLocked && !entryThread.equals(current)){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(!hasLocked) {
hasLocked = true;
current = entryThread;
count++;
} else if (entryThread.equals(current)){
count++;
}
}
@Override
public synchronized void unlock() {
if(hasLocked) {
if(Thread.currentThread().equals(current)){
count--;
if(count == 0){
hasLocked = false;
current = null;
notifyAll();
}
}
}
}
// 其余 Override 略(主要是没写)
}
4.2、AQS 实现锁
1、简单锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 不可重入锁
*/
public class MyLock implements Lock {
private final Sync sync = new Sync();
private class Sync extends AbstractQueuedSynchronizer {
// 尝试以独占模式获取锁
public boolean tryAcquire(int acquires){
assert acquires == 1;
if(compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁(该方法总是由执行释放的线程调用)
protected boolean tryRelease(int releases) {
assert releases == 1;
if(!isHeldExclusively()){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public boolean isLocked() {
return getState() != 0;
}
// 返回true如果同步仅针对当前(调用)线程进行保存
public boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
public Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire