Lock接口
在jdk5之后,java在juc包下新增了一个Lock接口用来实现锁的功能,相比synchronized关键字来说实现锁的功能是相同的,但Lock实现的锁更加灵活,功能更加丰富,如能够尝试获得锁、能被中断的获取锁、超时获取锁。
Lock接口如下
public interface Lock {
//阻塞的获取锁
void lock();
//可打断的获取锁,即因为得不倒锁而阻塞时,可以被其他线程打断阻塞状态
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,如果获得锁了返回true,没有获得锁返回false,非阻塞的
boolean tryLock();
//带超时时间且可以被打断,如果规定的时间没有获得锁那就不去争抢锁了,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解锁
void unlock();
//创建条件变量,WaitSet
Condition newCondition();
}
Lock接口的锁一般都是聚合了一个队列同步器的子类来完成线程控制访问的。
队列同步器
AbstractQueuedSynchronizer队列同步器,简称AQS,它是一个抽象类,该类中用一个int state的变量表示锁的同步状态,还内置了一个FIFO队列来完成获取锁线程的排队功能。之所以要设计AQS和Lock是因为要分工明确,实现Lock接口的锁面向使用者,隐藏锁的实现细节,而AQS面向的是锁的开发者,简化了锁的实现方式,屏蔽了很多底层的操作。
AQS既支持独占锁也支持共享锁:
- 独占锁即同时只能一个线程占用锁
- 互斥锁可以同时多个线程得到锁,具体看我们的业务进行实现,让满足条件的那些线程可以同时得到锁
AQS采用的是模板方法模式,我们需要继承AQS并重写对应的方法,然后将这些方法组合在我们的自定义锁中。其中的state成员变量就是我们自定义锁需要维护的:
- getState():获取锁
- setState(int state):设置锁
- compareAndSetState(int expect,int update):使用CAS设置state的状态,保证状态的原子性
我们需要重写的方法如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
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();
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
}
其中tryAcquire和tryRelease是独占式的,后面带shared的是共享式的,只需重写其中的一组即可
- tryAcquire(int arg):独占式获取同步状态,该方法需要查询当前状态并判断同步状态是否符合预期,再进行CAS设置同步状态。
- tryRelease(int arg):独占式释放同步状态,唤醒等待获取同步状态的线程
- tryAcquireShared(int arg):共享式获取同步状态,返回大于等于0的值,表示获取成功,反之失败
- tryReleaseShared(int arg):共享式释放同步状态
- isHeldExclusively():一般该方法表示是否被当前线程所独占
我们重写了这些比较简单的方法后,就可以调用AQS给我们提供的模板方法,来调用我们重写的方法实现,这些方法功能如下:
void acquire(int ) | 该方法会调用我们重写的tryAcquire(),如果获得锁成功即返回true了,就返回该方法,如果返回false,则当前线程将进入内部的同步队列(阻塞队列)等待 |
void acquireInterruptibly(int arg) | 同上,但从该方法进入同步队列的线程可以被打断 |
boolean tryAcquireNanos(int arg,nanos) | acquireInterruptibly基础上增加了超时限制,从该方法进入同步队列的线程可以在规定时间后自动解除阻塞,且返回false |
boolean release(int arg) | 调用我们重写的tryRelease(),独占式的释放锁,并将同步队列的第一个节点包含的线程唤醒 |
void acquireShared(int arg) | 调用tryAcquireShared()方法,共享式的获取锁 |
void acquireSharedInterruptibly(int arg) | 同上,增加了可打断 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 同上增加了超时时间 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
Collection<Thread> getQueuedThreads() | 获取等待在同步队列上的线程集合 |
依据上面说明的方法,我们来自己实现一个简单的独占锁,来了解了解这其中的框架结构:
//自定义锁,不可重入锁
public class MyLock implements Lock {
//如果为0表示锁没有被占用,为1表示锁被占用
private static class MySync 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);//没有竞争,所以不用使用CAS
return true;
}
@Override//是否处于占用状态
protected boolean isHeldExclusively() {
return getState()==1;
}
//返回一个内部的Condition给外部
Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
//仅需将操作代理到sync里即可
@Override
public void lock() {sync.acquire(1);}
@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.tryAcquireNanos(1, unit.toNanos(time));
}
@Override//不是调用我们重写的tryRelease
public void unlock() {sync.release(0); }
@Override
public Condition newCondition() {return sync.newCondition(); }
}
该锁实现的是一个不可重入锁,所以对于的int 参数等都不需要用到。以lock()方法为例,我们只需要调用AQS模板方法的acquire()方法即可,当前线程获取锁失败就会进入阻塞状态,不需要我们控制,我们只需要写好tryAcquire()获取锁的逻辑即可,这就降低了很多底层复杂的逻辑。
ReentrantLock原理
首先来看看ReentrantLock的继承类图,它是系统已经给我们提供好的锁的实现,我们重点是关注它的原理。
加锁流程
ReentrantLock其中有非公平锁和公平锁的分类,默认是非公平锁,从它的lock()方法入手
该方法内部调用了非公平锁类的lock方法,我们再进去看看非公平锁
1. 其中加锁成功了就直接退出该方法了,跟我们以前tryAcquire里的一样,此时的状态如下
2.现在假设第二个线程来了,尝试加锁,但加锁失败则会进入acquire()方法,acquire是AQS的方法,如下:该方法的作用是首先调用tryAcquire()方法
先看tryAcquire()方法,看上图的非公平锁类中已经重写了该方法,该方法调用了如下方法
此时因为Thread-1没有释放锁,所以第二次尝试也失败。调用addWaiter()方法,方法如下:
图如下
再进入到acquire方法的第三步acquireQueued方法里
shouldParkAfterFailedAcquire()方法的作用如下,如果前驱结点的状态为SIGNAL就直接返回true进入阻塞状态,而刚开始节点的值是为0的,所以进入下面的逻辑,把前驱结点的值改为SIGNAL然后再返回false,所以退出该方法后,还会线程2还会再次尝试获得锁,获取失败后才被park
park的具体方法如下:
如上可以看到,线程2因为迟迟得不到锁被LockSupport类的park()方法阻塞了。如果当时线程1正巧释放了锁,线程2可以不用阻塞的,直接获得锁 。
如果有第三个线程来获得锁那么流程也会不一样,这里就不说了,可以仔细看看源码。
释放锁流程
首先线程1进入到release方法,该方法里又会调用图中的两个方法,来一步步看
第一个方法tryReleas是ReentrantLock里重写的方法,如下,和我们以前写的差不多
发现头结点不为空,调用第二个方法,如下
线程2也就被唤醒了,再回到线程2被阻塞的方法,如下
其中的setHead()方法会把结点的所有属性都置为null,当线程2获取锁成功后,结果图如下:
如果线程1唤醒线程2时,又来了一个线程3,线程2和线程3会同时争夺锁,这就是非公平锁的由来,如果是公平锁,线程3是会直接进入到队列的,不会插队和线程2争夺锁。
可重入原理
如下方法,上面也看到过,再来拉出来看一下
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果状态不为0,则表示有线程在占用锁,
//可重入锁会查看那个线程是否为当前线程,如果为当前线程则会把state状态+1,表示重入了一次
//释放的时候只需要减掉1,如果减为0了才真正释放锁
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;
}
重入锁的关键就在这,我们也可以实现我们自己的可重入锁。但是如果不是本线程获取占用的锁,则表明加锁失败,返回false,进入到上面的acquire方法里,执行进入阻塞队列的方法:
可打断原理
先看看看我们上面调用的不可打断的获取锁的方法,假设执行到下面这一步了,进入park后,突然被打断了,然后立马会清除打断标记,下次再循环进入该方法时还是会被park住。
private final boolean parkAndCheckInterrupt() {
//如果打断标记为true,就不会进入阻塞
LockSupport.park(this);
//如果被打断了,就返回一个true
return Thread.interrupted();//该方法会清除打断标记
}
一直等待该线程得到锁了,然后再调用selfInterrupt();自我中断。
可打断的代码如下:
公平锁实现原理
如下图,非公平锁源码可以看到,如果当前state为0则会立即去CAS争夺锁,而公平锁的先检查队列有无阻塞结点,如果有的话当前线程就乖乖排队。
await原理
先看AQS的ConditionObject类,该类也会创建一个等待结点也是Node,跟上面的Node一样,当调用await时,会把当前线程加到Condition里的等待队列,释放掉以前在获取锁的那个队列。
signal
Semaphore
共享锁,信号量,可以限制一个最大线程数,同一时刻只允许限定的线程进入,常用API如下:
public class Semaphore {
private final Sync sync;
public Semaphore(int permits) {sync = new NonfairSync(permits)};
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void release() {
sync.releaseShared(1);
}
}
使用Semaphore限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatcl的实现)
用Semaphore实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
private Semaphore semaphore;
//构造方法
public Pool(int poolSize) {
semaphore=new Semaphore(poolSize);//信号量的数量,
this.poolSize = poolSize;
connections = new Connection[poolSize];
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection();
}
states = new AtomicIntegerArray(new int[poolSize]);
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire();//信号量进入,阻塞满
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
return connections[i];
}
}
}
}
public void free(Connection connection) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == connection) {
states.set(i, 0);
semaphore.release(); //信号量归还
break;
}
}
}
CountDownLatch
CountDownLatch,用来进行线程同步协作,等待所有线程完成倒计时。 其中构造参数用来初始化等待计数值,款await()用来等待计数归零,countDown()用来让计数减一
跟join类似。
如主线程创建一个CountDownLatch对象,计数值设为3,调await()方法,主线程进入阻塞,然后需要其他线程countDown()把3减为0,这时候主线程才会被唤醒。
如下一段程序,是CoutDownLatch和线程池配合使用:
public static void main(String[] args) throws InterruptedException, SQLException {
CountDownLatch count=new CountDownLatch(10);
ExecutorService pool = Executors.newFixedThreadPool(10);
String[] array = new String[10];
Random random=new Random();
for (int i = 0; i < 10; i++) {
int k=i;
pool.submit(()->{
for (int j = 0; j <= 100; j++) {
array[k]=j+"%";
System.out.print("\r"+Arrays.toString(array));
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count.countDown();
});
}
count.await();
System.out.println("\r游戏开始...");
}
演示: