并发之父 Doug Lea
AbstractQueuedSynchronizer
Java并发编程核心在于java.concurrent.util(juc)包
juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器(state:记录当前是否加锁,加锁的次数等等,引入可重入的功能 )
- 等待队列,条件队列:公平与非公平的特性
- 独占获取:排他锁
- 共享获取:共享锁
AQS中用到了大量的循环
AQS具备特性:
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
并发编程的实现
例如Java.concurrent.util当中同步器的实现如Lock,Latch,Barrier等,都是基 于AQS框架实现一般通过定义内部类Sync继承AQS
- 一般通过定义内部类Sync继承AQS
- 将同步器所有调用都映射到Sync对应的方法
- 内部一般定义一个state 变量
AQS框架-管理状态
- AQS内部维护属性volatile int state (32位)
- state表示资源的可用状态
- State三种访问方式
- getState()、setState()、compareAndSetState()
- AQS定义两种资源共享方式
- Exclusive-独占,只有一个线程能执行,如ReentrantLock(悲观锁:除非线程1全部运行完后才会释放锁,否则其他线程无法拿到锁。可重入,公平与非公平特征 )
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
- AQS定义两种队列
- 同步等待队列:获取锁失败会进入此队列
- 条件等待队列
Node
static final class Node {
/**
* 标记节点未共享模式
* */
static final Node SHARED = new Node();
/**
* 标记节点为独占模式
*/
static final Node EXCLUSIVE = null;
/**
* 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
* */
static final int CANCELLED = 1;
/**
* 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。
*/
static final int SIGNAL = -1;
/**
* 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
* 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会被无条件地传播下去
*/
static final int PROPAGATE = -3;
/**
* 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,保证数据原子操作:内存值对比修改方式,内存值与原值是否一致,一致则修改为新值
* 即被一个线程修改后,状态会立马让其他线程可见。
*/
volatile int waitStatus;
/**
* 前驱节点,当前节点加入到同步队列中被设置
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 节点同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
* 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同步队列
CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
内部类中:Node中定义了同步队列的属性,独占模式,还是共享模式
如果Node在条件队列当中,Node必须是独占模式 (例如:阻塞队列BlockingQueue )
不管条件队列还是同步队列都是基于Node来构造的,根据指针记录前后节点是谁
信号量指的是:waitStatus
根据双向链表前后指针找到前后节点
条件队列
Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备时 ,这些等待线程才会被唤醒,从而重新争夺锁
前后指针都是空值(单项链表)只有nextWaiter中存在值
单项链表
重入锁
假设村民排队打水,水桶代表不同的业务。a,b关联度比较高,需要保证a,b都运行完成后才能释放锁
不可重入锁
公平锁
依次等候
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 加锁次数
int c = getState();
if (c == 0) {
// 判断队列中是否有等待锁的节点
// 通过cas比较更改锁的状态:保证数据原子操作:内存值对比修改方式,内存值与原值是否一致,一致则修改为新值
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 修改独占锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
非公平锁
读写锁
- 写锁(独享锁、排他锁),是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得写锁的线程即能读数据又能修改数据。
- 读锁(共享锁)是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得读锁的线程只能读数据,不能修改数据。
AQS中state字段(int类型,32位),此处state上分别描述读锁和写锁的数量于是将state变量“按位切割”切分成了两个部分
- 高16位表示读锁状态(读锁个数)
- 低16位表示写锁状态(写锁个数)
ReentrantLock
public class LockTemplete {
private Integer counter = 0;
/**
* 可重入锁,公平锁
* 公平锁:true
* 非公平锁:false 如果不指定值则是非公平锁
* 需要保证多个线程使用的是同一个锁
*
* synchronized可重入
* 虚拟机,在ObjectMonitor.hpp定义了synchronized他怎么取重入加锁 ..。hotspot源码
* counter +1
* 基于AQS 去实现加锁与解锁
*/
private ReentrantLock lock = new ReentrantLock(true);
/**
* 需要保证多个线程使用的是同一个ReentrantLock对象
* @return
*/
public void modifyResources(String threadName){
System.out.println("通知《管理员》线程:--->"+threadName+"准备打水");
//默认创建的是独占锁,排它锁;同一时刻读或者写只允许一个线程获取锁
lock.lock();
System.out.println("线程:--->"+threadName+"第一次加锁");
counter++;
System.out.println("线程:"+threadName+"打第"+counter+"桶水");
//重入该锁,我还有一件事情要做,没做完之前不能把锁资源让出去
lock.lock();
System.out.println("线程:--->"+threadName+"第二次加锁");
counter++;
System.out.println("线程:"+threadName+"打第"+counter+"桶水");
lock.unlock();
System.out.println("线程:"+threadName+"释放一个锁");
lock.unlock();
System.out.println("线程:"+threadName+"释放一个锁");
}
public static void main(String[] args){
LockTemplete tp = new LockTemplete();
new Thread(()->{
String threadName = Thread.currentThread().getName();
tp.modifyResources(threadName);
},"Thread:A").start();
new Thread(()->{
String threadName = Thread.currentThread().getName();
tp.modifyResources(threadName);
},"Thread:B").start();
}
}
当只有一个线程时head、tail都是空的,多个线程时则如图所示
当运行代码时断点可知
ReentrantLock对比Synchronized
- 资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
- synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要确保保证锁定一定会被释放,必须将unLock放到finally{}中
- ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 轮询锁,定时锁等候和中断锁等候
- 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定;
- 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断;
- 如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。
什么时候选择用 ReentrantLock 代替 synchronized
- 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。
- ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。
- 建议用 synchronized 开发,
直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock 性能会更好
。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。