011-多线程-基础-基于AbstractQueuedSynchronizer自定义同步组件
一、概述
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架。
1.1、自定义独占锁同步组件
设计一个同步工具:该工具在同一时刻,只允许一个线程访问,超过一个线程的访问将被阻塞,我们将这个同步器命名为Mutex。
首先,确定访问模式,MutexLock在同一时刻只支持一个线程的访问,这显然是独占式访问,因此,需要使用AQS提供的acquire(int)方法以及release(int)等方法,这就要求Mutex必须重写tryAcquire(int)和tryRelease(int)方法,才能保证AQS的独占式同步状态的获取与释放方法得以执行。
其次,定义同步状态(或者资源数),MutexLock在同一时刻只支持一个线程的访问,我们用0表示同步状态未被获取,用其他数值表示已被获取,这里,我们就用1表示,当一个线程进行获取,通过CAS操作将同步状态设置为1,该线程释放时,将同步状态置为0,这样就实现了一个简单的独占式同步组件。MutexLock源码如下:
package com.github.bjlhx15.common.thread.juc.collection; 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 MutexLock implements Lock { private final Sync sync = new Sync(); private static final class Sync extends AbstractQueuedSynchronizer { // 当状态为0的时候获取锁,通过CAS将状态置为1 public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 释放锁,将状态置为0 public boolean tryRelease(int releases) { if (getState() == 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } } @Override public void lock() { sync.acquire(1); } @Override public void unlock() { sync.release(1); } @Override public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override public Condition newCondition() { return null; } @Override public boolean tryLock() { return sync.tryAcquire(1); } @Override public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException { return sync.tryAcquireNanos(1, arg1.toNanos(arg0)); } }
定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放锁的同步状态操作。在tryAcquire(int)方法中,如果通过CAS设置成功,则代表获取到了同步状态,而在tryRelease(int)方法中,只是将同步状态置为0,而不用CAS操作
测试
public class MutexLockTest { private final static MutexLock mutex = new MutexLock(); private static final class Worker extends Thread { @Override public void run() { super.run(); while (true) { // 获取锁 mutex.lock(); try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 mutex.unlock(); } } } } public static void main(String[] args) throws InterruptedException { // 启动10个线程 for (int i = 0; i < 10; i++) { Worker worker = new Worker(); worker.setDaemon(true); worker.start(); } Thread.sleep(10000); } }
结果
Thread-0 Thread-0 Thread-0 Thread-0 Thread-0 Thread-0 Thread-0 Thread-1 Thread-1 Thread-1
运行过程中可以看到,每次只有一个线程输出,也就是同一时刻只有一个线程获取了锁,这表明Mutex能够完成独占锁的功能,但是Mutex还存在如下的问题:
不可重入:重入是指任意线程在获取锁之后能够再次获取该锁而不会被锁所阻塞,在上述代码中,当一个调用mutex.lock()获取锁之后,如果再次调用mutex.lock(),那么该线程将会被自己阻塞,因为在Mutex在实现tryAcquire(int)方法时,没有考虑占有锁的线程再次获取锁的情况,而在线程再次调用tryAcquire(int)方法时返回了false,导致线程被阻塞。
非公平:通过程序输出我们可以发现,在10秒之内,获取锁的线程只有Thread-0和Thread-1,而其他的线程都只能阻塞,这很容易产生饥饿现象。
这两个问题在JDK提供的重入锁ReentrantLock中,都得到了解决,我们后续会对ReentrantLock进行详解。
1.2、自定义共享锁同步组件
设计一个同步工具:该工具在同一时刻,只允许最多两个线程访问,超过两个线程的访问将被阻塞,我们将这个同步器命名为TwinsLock。
首先,确定访问模式,TwinsLock在同一时刻支持多个线程的访问,这显然是共享式访问,因此,需要使用AQS提供的acquireShared(int)方法以及releaseShared(int)等方法,这就要求TwinsLock必须重写tryAcquireShared(int)和tryReleaseShared(int)方法,才能保证AQS的共享式同步状态的获取与释放方法得以执行。
其次,定义同步状态(或者资源数),TwinsLock在同一时刻允许最多两个线程的访问,我们可以将同步状态初始值设置为2,表示可用的同步资源数目,当一个线程进行获取,status减1,该线程释放时,status加1,状态的合法范围为0、1、2,其中0表示当前已经有两个线程获取了同步资源,此时再有其他线程获取同步状态,该线程只能被阻塞。同步状态的变更都通过CAS操作做原子性保障。TwinsLock源码如下:
public class TwinsLock implements Lock { private final Sync sync = new Sync(2); private static final class Sync extends AbstractQueuedSynchronizer { public Sync(int count) { if (count <= 0) { throw new IllegalArgumentException("count must larger than zero."); } setState(count); } public int tryAcquireShared(int reduceCount) { for (;;) { int currentCount = getState(); int newCount = currentCount - reduceCount; if (newCount < 0 || compareAndSetState(currentCount, newCount)) { return newCount; } } } public boolean tryReleaseShared(int returnCount) { for (;;) { int currentCount = getState(); int newCount = currentCount + returnCount; if (compareAndSetState(currentCount, newCount)) { return true; } } } } @Override public void lock() { sync.acquireShared(1); } @Override public void unlock() { sync.releaseShared(1); } @Override public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override public Condition newCondition() { return null; } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException { return false; } }
TwinsLock实现了Lock接口,提供了面向使用者的接口,使用者调用lock()方法获取锁,随后调用unlock()方法释放锁,而同一时刻只能有最多两个线程获取锁。TwinsLock包含了一个自定义同步器Sync,而该同步器面向线程访问和同步状态控制。线程获取锁时,同步器会先计算出获取后的同步状态,然后通过CAS确保状态的正确设置,当tryAcquireShared(int)方法返回值大于等于0时,表示线程获取了同步状态,对于上层TwinsLock而言,表示当前线程获取了锁。
下面编写了一个程序来对TwinsLock进行验证:
public class TwinsLockTest { private final static TwinsLock twinsLock = new TwinsLock(); private static final class Worker extends Thread { @Override public void run() { super.run(); while (true) { twinsLock.lock(); try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { twinsLock.unlock(); } } } } public static void main(String[] args) throws InterruptedException { // 启动10个线程 for (int i = 0; i < 10; i++) { Worker worker = new Worker(); worker.setDaemon(true); worker.start(); } Thread.sleep(10000); } }
运行结果(不唯一):
Thread-1 Thread-0 Thread-3 Thread-2 Thread-2 Thread-3 Thread-2 Thread-3 Thread-2 Thread-3
运行过程中可以看到,线程的名称都是成对输出,也就是同一时刻有两个线程获取了锁,这表明TwinsLock可以按照预期正常工作。但是,TwinsLock同样也存在重入和非公平的问题。