构建自定义的同步工具
-
在自定义的同步工具类中,要避免使用轮询和休眠实现方法的阻塞。
-
使用轮询等待方法操作并重试时,这种方法称为忙等待或自旋等待,此时会消耗大量的CPU时间;
// 如果缓存在很长的一段时间里都为空且一直不发生变化,这种机制会造成大量的cpu时钟周期的浪费。 public V take(){ while(true){ try{ if(!buffer.isEmpty()){ V item = buffer.take(); return item; } }catch(Exception e){ Thread.sleep(SLEEP_TIME); } } }
-
使用轮询加休眠实现重试机制时,休眠的时间间隔对于响应时间以及占用的CPU资源有着很大的影响。
// 轮询+休眠实现put方法以及添加重试机制 public void put(V v) throws InterruptedException{ while(true){ synchronized(this){ if(!isFull()){ doPut(V); return; } } // 此时的休眠时间间隔的选取十分重要 // 休眠时间越小,系统响应性就越高,但消耗的CPU资源在理论上也越高。 Thread.sleep(SLEEP_TIME); } }
-
在自定义的同步工具中要尽量使用条件队列进行线程通知的管理。条件队列:每个对象都可以作为一个条件队列,与传统队列不同的是,条件队列中的元素是一个个正在等待特定条件变成真的线程。
-
Object提供的wait、notify、notifyAll方法构成了内部条件队列的API,调用对象X的wait方法的线程会将自身加入到该对象X的内置条件队列中等待notify/notifyAll方法的调用。
-
对象的内置锁与其内部条件队列是相互关联的,要调用对象X中条件队列的任何一个方法,必须要持有对象X上的锁。与使用“休眠+轮询”方式实现等待通知+重试机制时相比较,条件队列会更高效并且响应性更高。
// 使用条件队列的put方法 public synchronized void put(V v) throws InterruptedException{ while(isFull()){ // 条件谓词:is-full wait(); // 还可以使用定时版本的wait(time)方法在预定的时间内完成该put操作 } doPut(); notifyAll(); } // 使用条件队列的take方法 public synchronized V take() throws InterruptedException{ while(isEmpty()){ // 条件谓词:is-empty wait(); } V v = doTake(); notifyAll(); return v; }
-
正确的使用条件队列关键是找出操作在哪个条件谓词上进行等待,如果找不到确定的条件谓词,条件等待机制将无法发挥正确的作用。
-
条件队列进行条件等待的代码规则为:获取对象锁—>测试条件谓词—>等待或者进行操作—>释放锁。
-
避免过早唤醒问题发生的措施是将wait()方法放在一个while循环中进行调用:当另一个线程调用notifyAll方法时,每当线程从wait中唤醒时会再次检测条件谓词是否为真,避免了由于过早唤醒使得条件谓词被其他线程修改状态的问题。
-
条件队列的通知方法:notify/notifyAll方法,调用这两个方法时,JVM会从这个条件队列上等待的线程中唤醒一个/所有,无论调用哪个方法,都必须持有与条件队列相关联的锁。
-
在使用私有的内置锁和条件队列时,会将条件队列进行封装,不在支持任何形式的客户端加锁。
-
Condition也是一种广义的内置条件队列,一个Condition和一个Lock关联在一起,在每个锁上可存在多个等待、条件等待可以是中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。
-
每个Lock可以有任意数量的Condition对象,Condition对象继承了相关的Lock的公平性。
AQS(AbstractQueueSynchronizer)
-
AQS是许多同步类的基类,AQS是一个用于构建锁和同步器的框架,子类例如:CountDownLatch、ReentrantReadWriteLock、SynchronousQueue、FutureTask和Semaphord都是基于该类实现的。
-
同步类拥有自身的一些状态,AQS负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过getState、setState以及compareAndSetState等protected类型方法进行原子操作。这个整数可以表示任意状态:
- ReentrantLock用它来表示所有者线程已经重复获取该锁的次数
- Semaphore用它来表示剩余的许可数量
- FutureTask用它来表示任务的状态
-
支持独占的获取操作的同步器需要实现tryAcquire、tryRelease和isHeldExclusively等保护方法。
-
支持共享的获取操作的同步器需要实现tryAcquireShared、tryReleaseShared等保护方法。
-
AQS中的acuqire、acquireShared、release、releaseShared等方法都将调用子类中带有try前缀的方法来判断某个操作是否能够执行。在子类中可以根据getState、setState以及CompareAndSetState方法检查和更新状态并通过重写的try前缀方法返回的状态值来告知基类获取或释放同步器的操作是否成功。
// 自定义实现的二元闭锁的例子,整数状态用0和1表示闭锁的关闭和打开 public class OneShotLatch { private final Sync sync = new Sync(); public void signal() { sync.releaseShared(0); //开启闭锁 } public void await() throws InterruptedException { sync.acquireShared(0); // 以共享方式进行获取操作 } private class Sync extends AbstractQueuedSynchronizer { // 子类实现的try版本的共享获取方法模板 protected int tryAcquireShared(int ignored) { // 如果闭锁是开的 (state == 1),那么这个操作将成功,否则返回负数表示失败 return (getState() == 1) ? 1 : -1; } protected boolean tryReleaseShared(int ignored) { setState(1); // 打开闭锁 return true; // 释放同步器,其他线程可以进行获取该闭锁 } } }
-
在同步器中还可以自行管理一些额外的状态变量,例如ReentrantLock保存了锁的当前持有者信息用来区分某个获取操作是重入的还是竞争的。
// 基于非公平的ReentrantLock实现tryAcquire protected boolean tryAcquire (int ignore){ final Thread current = Thread.currentThread(); int c = getState(); if(c==0){ if(compareAndSetState(0,1)){ // 原子的更新状态信息 owner = current; // 如果是第一次进入会设置当前锁的持有者信息 return true; } }else if(current == owner){ // 如果不是第一此进入,如果是锁的持有者则锁计数加一 setState(c+1); return true; } return false; }
-
CLH同步队列:AQS内部维护了一个双向链表队列, 遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程。当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。
-
在将等待线程加入CLH队列时,会进行CAS的自旋操作直到将该等待队列加入到队列尾端为止
// AQS中添加新节点的方法实现 private Node enq(final Node node) { //CAS"自旋",直到成功加入队尾 for (;;) { Node t = tail; if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。 if (compareAndSetHead(new Node())) tail = head; } else {//正常流程,放入队尾 node.prev = t; if (compareAndSetTail(t, node)) { // 使用类似CAS的原子操作进行链表更新 t.next = node; return t; } } } } // AQS中使当前线程阻塞的方法实现,线程阻塞之后会将阻塞的线程生成一个Node节点并加入到CLH队列中 private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//调用park方法使线程进入waiting状态,jvm的native方法 return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。 }
-
Lock.newCondition()将返回一个新的ConditionObject实例,这是AQS的一个内部类。