释放同步状态后,同步队列的变化过程和共享节点获取到同步状态后的变化过程一致,此处不再进行赘述。
/** * 释放同步状态,如果释放成功,唤醒后面等待的节点 * */ public final boolean releaseShared(int arg) { // 1. 尝试释放同步状态 if (tryReleaseShared(arg)) { // 2. 释放成功后,唤醒后续等待共享节点 doReleaseShared(); return true; } return false; } 四,基于AQS实现互斥锁读到此处,大部分人应该还比较懵逼,似懂非懂。接下来笔者将通过AQS实现一个互斥锁带你打开AQS的正确打开姿势。
多线程环境count += 1可能会存在问题,详情可以看在中介绍的三大原因。正如大多数人都知道的,我们通常可以使用synchronized关键字进行同步,接下来我们就基于AQS自定义一个互斥锁来完成相同的功能。
4.1 代码实现 /** * 自定义互斥锁 * * @author cruder * @time 2019/11/29 23:23 */ public class MutexLock { private static final Sync STATE_HOLDER = new Sync(); /** * 通过Sync内部类来持有同步状态, 当状态为1表示锁被持有,0表示锁处于空闲状态 */ private static class Sync extends AbstractQueuedSynchronizer { /** * 是否被独占, 有两种表示方式 * 1. 可以根据状态,state=1表示锁被占用,0表示空闲 * 2. 可以根据当前独占锁的线程来判断,即getExclusiveOwnerThread()!=null 表示被独占 */ @Override protected boolean isHeldExclusively() { return getExclusiveOwnerThread() != null; } /** * 尝试获取锁,将状态从0修改为1,操作成功则将当前线程设置为当前独占锁的线程 */ @Override protected boolean tryAcquire(int arg) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } /** * 释放锁,将状态修改为0 */ @Override protected boolean tryRelease(int arg) { if (getState() == 0) { throw new UnsupportedOperationException(); } setExclusiveOwnerThread(null); setState(0); return true; } } /** * 下面的实现Lock接口需要重写的方法,基本是就是调用内部内Sync的方法 */ public void lock() { STATE_HOLDER.acquire(1); } public void unlock() { STATE_HOLDER.release(1); } } 4.2 锁的测试我们定义一个计数器类,里面定义了2个不同的计数方法,其中一个使用互斥锁进行同步。开启10个线程并发执行,每个线程计数10000次,然后对比统计结果与预期的100,000是否相符。
package myLock; import java.util.concurrent.*; /** * 自定义锁测试 * * @author liqiang * @time 2019/11/29 12:39 */ public class MyLockTest { public static void main(String[] args) throws InterruptedException { int threadNum = 10; int countPerThread = 10000; // 线程池创建的正确姿势 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(threadNum, threadNum, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), new ThreadPoolExecutor.AbortPolicy()); CountDownLatch countDownLatch = new CountDownLatch(threadNum); Counter counter = new Counter(); Counter counterUnsafe = new Counter(); for (int i = 0; i < threadNum; i++) { threadPool.submit(() -> { for (int j = 0; j < countPerThread; j++) { counter.getAndIncrement(); counterUnsafe.getAndIncrementUnSfae(); } countDownLatch.countDown(); }); } countDownLatch.await(); System.out.printf("%s 个线程,每个线程累加了 %s 次,执行结果:safeCounter = %s, unsafeCounter = %s ", threadNum, countPerThread, counter.get(), counterUnsafe.get()); threadPool.shutdownNow(); } } class Counter { private MutexLock mutexLock; private volatile int count; Counter() { this.mutexLock = new MutexLock(); } int get() { return count; } int getAndIncrement() { mutexLock.lock(); count++; mutexLock.unlock(); return count; } int getAndIncrementUnSfae() { count++; return count; } }结果和预期一样,用自定义锁实现的计数器统计没有误差。
五,总结AQS通过一个int同步状态码,和一个(先进先出)队列来控制多个线程访问资源
支持独占和共享两种模式获取同步状态码
当线程获取同步状态失败会被加入到同步队列中
当线程释放同步状态,会唤醒后继节点来获取同步状态
共享模式下的节点获取到同步状态或者释放同步状态时,不仅会唤醒后继节点,还会向后传播,唤醒所有同步节点
使用volatile关键字保证状态码在线程间的可见性,CAS操作保证修改状态码过程的原子性。