目录
Lock接口
Lock lock=new ReentrantLock();
lock.lock();
try{
...
} finally {
lock.unlock();
}
Lock接口具备synchronized不具备的特性
lock提供了synchronized不具备的特性:超时获取锁,中断获取锁。
超时获取锁:线程尝试获取锁,若获取锁的时间用光了,则放弃获取锁。
中断获取锁:线程在获取锁的过程中,或者在同步队列等待获取锁时,其它线程调用了该线程的interrupt方法,线程会响应中断,放弃获取锁。
队列同步器
队列同步器AbstractQueuedSynchronizer,是用来构建锁或其它同步组件的基础框架,使用了一个int类型的state变量来表示同步状态,通过内置了FIFO队列来为任务分配线程。
独占锁和共享锁
在AbstractQueuedSynchronizer类中,如下方法是会抛出UnsupportedOperationException异常。
//独占锁的获取
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//独占锁的释放
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//共享锁的获取
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//共享锁的释放
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
需要使用独占锁,则子类实现tryAcquire和tryRelease方法;
需要使用共享锁,则子类实现tryAcquireShared和tryReleaseShared方法。
以独占锁的实现为例
public class MyMutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(getState()==0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);//当前锁没有线程占用
setState(0);//锁未占用状态
return true;
}
Condition newCondition(){
return new ConditionObject();
}
}
final Sync sync=new Sync();
@Override
public void lock() {
sync.acquire(1);//sync父类中的acquire方法,它将会调用子类的tryAcquire方法,后面原理会解释
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//可中断的方式获取锁
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);//sync父类中的release方法,它将会调用子类的tryRelease方法,后面原理会解释
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
独占锁只有状态0和1,0表示锁未被占用,1表示锁已占用。
同步队列原理
在基础同步器类AbstractQueuedSynchronizer中,提供了一个同步队列,用来暂存获取锁失败的线程状态和线程引用。
当一个线程释放锁时,会唤醒首结点对应线程;
而当一个线程获取锁失败时,会将线程状态和线程引用信息新建一个Node,加入到同步队列的尾结点。
也就是,先进先出FIFO的方式。
独占锁获取与释放原理
锁获取分析
锁释放分析
释放比较简单,因为锁是独占的,持有锁的线程直接释放就好了,tryRelease修改state状态,然后唤醒首结点线程(如果有的话)。
共享锁的获取与释放
独占锁是最多只有一个线程获得该锁(如写锁),共享锁是可以有多个线程同时获取该锁(如读锁,Semaphore)。
锁获取分析
tryAcquireShared返回>=0时,说明获取共享锁成功。
若返回值<0,则进入doAcquireShared进行自旋获取锁。
只有当前Node的前驱结点为首结点时,才会尝试获取锁,tryAcquireShared返回值r>=0时,才说明获取锁成功,同时会响应中断。
锁释放分析
由于共享锁可以被多个线程持有,因此释放共享锁需要循环CAS自旋的方式。
tryReleaseShared修改state状态,然后调用doReleaseShared方法(循环+CAS)。
超时获取独占锁
流程如下:
可重入锁
看如下例子:
public class M {
public void synchronized m1(){
m2();
}
public void synchronized m2() {
...
}
}
当线程进入m1()方法后(必然获取了M对象的锁),还需要继续进入m2()方法,m1方法和m2方法的对象锁是一样的,因此在m1里的线程可以直接进入m2,这就是可重入锁。synchronized隐式是支持可重入锁的。
可重入锁,每进入一个同步块都需要加一把锁,离开同步块都需要释放一把锁。上述例子实际上是一个线程(进入m2方法后)持有了两把M锁,离开m2方法时,释放一把M锁,再离开m1方法时,再释放一把M锁。
ReentrantLock可重入锁
可重入锁的条件:1.获得锁的线程再次获取锁,获取成功,state++; 2. 释放锁时,需要state--,直到持有锁的线程释放所有的锁时,即state=0时,才会唤醒其它阻塞的线程。
公平锁和非公平锁
在ReentrantLock中,静态内部类Sync继承AbstractQueuedSynchronizer,也内置了FairSync和UnfairSync(都继承自Sync)。
公平锁,简单理解就是,所有想获得锁的线程,都会老老实实排队等候,禁止插队存在,按照先来后到的顺序获取锁,这叫“文明锁”;
公平锁的获取,可重入性关键在于current==getExclusiveOwerThread(),若已经持有锁的线程再次尝试获取锁。若c==0,说明没有线程持有锁,这是要判断是不是队列首结点,只有队列首结点才能获得锁!
非公平锁,已经在排队获得锁的线程,不会插队,当服务员喊道下一个,这时,队列首结点线程和刚刚来准备排队的线程会一哄而上,竞争锁,这叫“野蛮锁”。
非公平锁的获取
第一次尝试CAS获取锁,获取成功则当前线程获得锁,否则acquire(1);
尝试tryAcquire获得锁,若失败后,则生成结点,加入队列尾部,acquireQueued方法使得线程循环获取锁。
非公平锁获取的方法在ReentrantLock的静态内部类Sync中实现了。
非公平的关键在于if(compareAndSetState(0,acquires))这一行,不需要判断是不是队列首结点,这是因为,只要是醒着的线程都可以竞争锁。
公平锁和非公平锁的释放,都在Sync中实现了。
通过观察源码中的state变量,可以发现,可重入锁并不能保证state的值就是1,第一次获得锁,state=1,第二次获得锁,state=2,.......
释放锁也是一样,只有当state减到0时,才会返回free=true,设置当前独占锁线程为null。
公平锁的优缺点:公平锁不会出现饿死线程的情况,但性能通常比较低;
非公平锁的优缺点:非公平锁的性能比较高,有可能会饿死线程。
为什么非公平锁性能好?
CPU频繁切换线程,会造成性能损好,切换线程的过程,需要保存“离开线程”的现场计算信息,也需要调入“进场线程”的信息。
非公平锁模式下,持有锁的线程释放锁(1号竞争者),唤醒首结点线程(2号竞争者),还有其它刚到的线程(3,4,5...),同时竞争这把锁,而队列的第2~第n号结点不参与竞争,如果刚刚释放锁的线程重新获得了锁,这时候就可以减少线程的切换,提高吞吐量。同时,在队列后面的线程,由于没有竞争权利,很可能会出现长时间不能竞争锁,而饿死。
而公平锁模式下,持有锁的线程释放锁后,会唤醒首结点线程,首结点线程理所当然地获取到了锁,刚释放锁的线程乖乖去队列尾部再次排队。这样自然很公平,但是cpu整体性能不高,花费了大量cpu在线程切换上。
公平锁的耗时大约是非公平锁的100倍,线程切换次数大约也是100倍左右。
读写锁
读锁直接使用共享锁,写锁使用排他锁。前文提到的ReentrantLock锁就是排它锁。
读写锁有两把锁,一把是读锁,一把是写锁。分离读写锁,可以有效提升线程读取的性能。
读写锁的特性:支持公平锁和非公平锁(默认是非公平锁,因为吞吐量高),重入性(所有获得锁的线程可以再次获得锁),锁降级(写锁能降级为读锁)。
Java的读写锁是ReentrantReadWriteLock,读写锁用一个32位的变量维持状态:
高16位表示读锁状态,第16位表示写锁状态,数字表示读锁和写锁获取的次数(包括可重入)。
写锁状态=S&0x0000FFFF,位运算取低16位,写锁状态增加1时,直接S+1,减一为S-1;
读锁状态=(S>>16),位运算取高16位,读锁状态增加1时,S+(1<<16),减一为S-(1<<16)。
由于读状态和写状态都是16位的,最大值为65535。
写锁的获取与释放
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {//c!=0,说明必然存在读锁或写锁中的一个
int w = exclusiveCount(c);//取低16位,即写锁
//若w=0,说明当前没有写锁,有读锁, 当前写锁获取失败
//若当前线程不为已经获得写锁的线程,当前写锁获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//当前线程重入锁的次数达到上限,65535
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
//执行到这里,可能是state=0(读写锁为空),
//也可能是当前线程获得了写锁,正在进行可重入
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁的独占的,线程持有写锁时,当前必然没有读锁和其他写锁,因此释放写锁比较简单。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
读锁的获取和释放
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
//若存在写锁(排它锁),
//或当前线程没有获取写锁(因为写锁可以可重入读锁),则获取读锁失败(这里是写锁降级为读锁的关键)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
//获取读锁状态
int r = sharedCount(c);
//达到读锁上限
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//尝试将读锁状态+1
if (compareAndSetState(c, c + SHARED_UNIT)) {
//如果原来的读锁状态为0
if (r == 0) {
//第一个线程读锁为当前线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//第一个线程读锁可重入数量+1
firstReaderHoldCount++;
} else {
//累加读锁获取数量(注意,不包括第一位读者线程,这或许为了节省空间)
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
//若第一位读者,只重入了一次,则firstReader设置为null;否则减一
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
//读锁数量-1
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//尝试循环CAS来将读锁状态-1
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
只有当读锁减少为0时,才返回true,否则返回false(读锁仍然被占用)。
锁降级
先看简易版本:
public void test(){
writeLock.lock();
readeLock.lock;
writeLock.unlock();
//现在只能读,不能写了,写锁降级为读锁
//...
readLock.unlock();
}
public class LockUngrade {
private int i = 0;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock writerLock = readWriteLock.writeLock();
private Lock readLock = readWriteLock.readLock();
public void doSomething() {
writerLock.lock();
try {
i++;
readLock.lock();
} finally {
writerLock.unlock();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("i = " + i);
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
LockUngrade lock = new LockUngrade();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lock.doSomething();
}).start();
}
}
}
LockSupport工具
阻塞和唤醒线程的工具。
park()会一直阻塞线程,直到unpark(当前线程)调用,或者当前线程被中断,才会返回;
parkNanos()超时阻塞,被唤醒会立刻返回,或时间到了会自动返回。
parkUntil(),被unpark唤醒,或直到某个时间点,自动返回。
unpark(),唤醒线程。
Condition接口
Object和Condition都是监视器(即锁),Condition比Object有更多的支持,如在等待下不响应中断,等待到某个时间点,多个等待队列等。
有一个子类ConditionObject,能用就用Condtion吧。
参考书籍:
Java并发编程的艺术