一、Lock接口
1、Lock
Lock跟Synchronized一样,都是用在多线程中用来控制并发,保证共享资源安全的一种同步机制。
Synchronized是隐式的加锁/解锁;而Lock是显示的加锁/解锁,也就是说我们使用的时候需要先lock.lock()锁定资源,执行逻辑,用完后在lock.unlock()释放资源。
2、特性
Lock有Synchronized没有的特性,这几个特性使我们在开发中更方便灵活
1、非阻塞性获取同步状态。tryLock方法可以立即返回是否获取到同步状态,先尝试获取一次,有就有,没有就没有,不用去等待;
2、支持中断。自己释放不了同步状态,可以被中断,通过中断信号跳出某种状态,拿到同步状态干到一半干不下去就不干了释放同步状态给别人用吧。lockInterruptbly()是响应中断方法。
3、支持超时。到一定时间再干不完就不干了,释放同步状态。支持带时间参数的tryLock()。
3、用法
lock.lock()加锁;lock.unlock()解锁。
/**
* Lock的标准用法,注意两点:
* 1、try外上锁
* 2、finlly里解锁
*/
public class LockUseCase {
static Lock lock = new ReentrantLock();//定义锁
public static void main(String[] args) {
lock.lock();//try外上锁
try{
//todo 业务逻辑
}finally {
lock.unlock();//解锁
}
}
}
为什么要将lock放到try外面呢?因为lock方法可能会抛出异常,在try内使用,加锁的时候如果抛出异常加锁失败就进入finally里去解锁,加锁都没成功,解锁自然也会抛出异常;而在try外使用,如果加锁抛出异常就不走finally了,没有加锁自然也不会解锁,当然没问题。lock()是自定义同步方法,跟进源码会发现不管是公平模式还是非公平模式都有一大串的代码,要尝试获取同步状态,获取不成功还要入队,等待,被唤醒等操作,这过程也存在抛异常的可能,lock方法不去捕获异常,遇到问题就直接中断了,不会影响其他线程,这样更安全。总之,在try外使用就是防止lock抛异常时出现问题。
二、可重入锁 ReentrantLock
1、ReentrantLock
由名字可以看出,他跟synchronized一样,同一个线程可以多次拿锁,并且它有两种模式获取锁:公平模式获取同步状态和非公平模式获取同步状态。公平模式就是按入队顺序,挨个出队去拿同步状态,非公平模式是争抢式拿同步状态,谁抢到就是谁的。效率上肯定是非公平模式更高,默认就是非公平模式。
2、源码
/***
* 可重入锁。独占式锁的代表,AQS的一个应用
* 它支持公平和非公平获取同步状态
*/
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//自定义同步器,继承AQS。这是一个大方面的抽象类同步器,下面再细分公平和非公平同步器
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();//定义抽象方法,子类去具体实现
/***
* 定义一个非公平获取同步状态的方法,由子类非公平同步器调用
* 公平获取同步状态的方法在AQS里已经存在了,子类直接重写就可以了
* 非阻塞式,立即返回结果;是否获取成功
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//同步状态没有被占用,是空闲的,尝试用cas获取同步状态
if (compareAndSetState(0, acquires)) {//cas获取成功
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程就是获取了同步状态的线程,说明这是重新进入(可重入锁实现逻辑),可以获取同步状态
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//重写方法:独占式释放同步状态
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//持有同步状态的线程是否是当前线程
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
//自定义的非公平模式同步器
static final class NonfairSync extends Sync {
//自定义方法:阻塞式获取同步状态,既然式获取同步状态了,肯定式同步资源没被占用,所以是0
final void lock() {
//下面就是争抢了,谁先cas成功,就把同步资源给那个线程,cas不成功的只能进入等待队列了
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
}else {
acquire(1);//调用模板方法:aqs的独占式获取同步状态,模板方法会调用下面重写的那个方法
}
}
//重写方法:独占式获取同步状态
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//自定义的公平模式同步器
static final class FairSync extends Sync {
//自定义方法:阻塞式获取同步状态,公平模式原理就是从等待队列里挨个取,先进先出原则
final void lock() {
acquire(1);//调用模板方法:aqs的独占式获取同步状态,模板方法会调用下面重写的那个方法
}
//重写方法:独占式获取同步状态
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;
setState(nextc);
return true;
}
return false;
}
}
//两个构造函数,默认是非公平模式
public ReentrantLock() {
sync = new NonfairSync();
}
//参数可以指定用哪种模式
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public void unlock() {
sync.release(1);
}
}
三、读写锁 ReentrantReadWriteLock
1、读写锁
现实中大部分场景都是读多写少的,读写锁正是这种背景下诞生。读写锁分为读锁和写锁,读锁是共享式获取同步状态,同一时刻可以有多个线程获的同步状态;写锁是独占式获取同步状态,是排他锁,同一时刻只能有一个线程获取同步状态。
如果写线程正在执行写操作,其他线程都会被阻塞,禁止获取读或写操作,原因要是保证变量的可见性。也就是说只要有线程拿着读锁,写锁一定被获取不到。
一个同步状态的变量要维护两个状态(读写)是通过对int类型变量切分实现的,同步状态是一个int类型变量,是占32位,把他分成高16位代表读,低16位代表写。这样读写状态的改变通过int类上高低16位的位运算计算就可以实现了。
2、锁降级
锁降级是指由写锁降级为读锁的过程。写是排他性的,只能有一个进行,所以级别比较高;读是共享的,可以多个同时进行,级别低。
锁降级的过程是先拿到写锁,再去拿读锁,再释放写锁。
为啥会有这个过程,不是直接释放写锁去拿读锁?如果直接释放写锁,可能刚释放会被其他线程拿到写锁,其他线程修改了你的值,你再去用就不是你想要的值了,如果在持有写锁的过程中再拿读锁,这样即使释放了写锁,其他线程也无法拿到写锁,因为你本身在占据着读锁,这保证了数据变量的可见性。
3、用法
public class LockUseCase {
static ReadWriteLock rwLock = new ReentrantReadWriteLock();
static Lock readLock = rwLock.readLock();
static Lock writeLock = rwLock.writeLock();
public static void readTest(){
readLock.lock();
try{
//todo 业务逻辑
}finally {
readLock.unlock();
}
}
public static void writeTest(){
writeLock.lock();
try{
//todo 业务逻辑
}finally {
writeLock.unlock();
}
}
}
4、部分源码
/**
* 可重入读写锁。支持读锁(共享式获取同步状态)和写锁(独占式获取同步状态),也是AQS的应用
*/
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReadLock readerLock;
private final WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock() {
this(false);
}
/***
* 构造函数就确定了是否是公平模式并且创建了读锁实例和写锁实例
* @param fair
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public WriteLock writeLock() { return writerLock; }
public ReadLock readLock() { return readerLock; }
/**
* 内部类形式的自定义同步器
* 一个state状态怎么区分读写的呢?通过高低16位,位运算来实现.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }//共享式同步资源数量
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }//独占式同步资源数量
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter extends ThreadLocal< Sync.HoldCounter> {
public Sync.HoldCounter initialValue() {
return new Sync.HoldCounter();
}
}
//重写方法:释放独占式同步资源
protected final boolean tryRelease(int releases) {
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
/***
* 重写写锁(独占式)获取同步状态方法
* @param acquires
* @return
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();//获取同步状态
int w = exclusiveCount(c);//获取独占(写锁)它的数量
if (c != 0) {//同步状态已经被拿到了,有可能是它或者其他线程
//当前线程不是已经获取写锁的线程,返回获取失败
if(current != getExclusiveOwnerThread()){
return false;
}
//已经获取了同步状态,但是写锁呢又是0,所以这个同步状态是读锁的
//存在读锁,也返回失败
if (w == 0) {
return false;
}
//给同步状态加1计数,是当前线程重入的
setState(c + acquires);
return true;
}
//还没线程占有同步状态
//如果写锁被阻塞了,已经被别人拿了写锁就返回失败
if(writerShouldBlock()){
return false;
}
//如果cas设置同步状态失败,返回失败
if (!compareAndSetState(c, c + acquires)){
return false;
}
//否则,设置上当前线程后返回成功
setExclusiveOwnerThread(current);
return true;
}
/***
* 重写 读锁(共享锁)获取同步状态方法
* @param unused
* @return
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();//一个同步状态通过“按位切割”高低16位运算记录读状态和写状态
//独占锁(写锁)已经被获取了,并且还不是本线程
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) {
return -1;
}
int r = sharedCount(c);//获取共享(读锁)得同步状态数量
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {//如果读之前没被获取过
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
Sync.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 1;
}
return fullTryAcquireShared(current);
}
}