1、AbstractQueuedSynchronizer
锁必然要知道AbstractQueuedSynchronizer(AQS),AQS提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。
1.1、state
AQS是一个依赖状态(state)的同步器,子类中利用state来表示不同的含义。
private volatile int state;
操作stae的方式:
-
protected final int getState()
-
protected final void setState(int newState)
-
protected final boolean compareAndSetState(int expect, int update)
1.2、独占模式和共享模式
-
Node.EXCLUSIVE:独占,只有一个线程能执行,如ReentrantLock(悲观锁:除非线程1全部运行完后才会释放锁,否则其他线程无法拿到锁。可重入,公平与非公平特征 )
-
Node.SHARED:共享,多个线程可以同时执行,如Semaphore/CountDownLatch
```java
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;
}
}
1.3、同步等待队列、条件等待队列
Node中定义了同步队列(CLH)的属性
,独占模式,还是共享模式
ConditionObject
,条件队列,前后指针都是空值(单项链表)只有nextWaiter中存在值
2、锁
Lock是juc中实现的锁接口,定义了锁的一些行为规范,他的设计目的是为了解决synchronized关键字在一些并发场景下不适用的问题,相对synchronized更灵活。
Lock.java
/**
* Lock实现提供了比使用synchronized方法和语句更广泛的锁操作。
* 它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的条件对象。
*/
public interface Lock {
/**
* 尝试获取锁,如果当前锁被其他线程占用,
* 将会等待直到获取为止
*/
void lock();
/**
* 获取锁,假如当前线程在等待锁的过程中被中断,
* 那么将退出等待,并且抛出中断异常
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试获取锁,并立即返回,返回true代表获得锁
*/
boolean tryLock();
/**
* 在一段时间内尝试获取锁,假如期间内被中断,那么会抛出异常
* @return
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 新建一个绑定在当前Lock上的Condition对象
* 指定锁的一些条件
*/
Condition newCondition();
}
Condition.java
public interface Condition {
/**
* 使当前线程等待,直到发出信号或被中断为止
*/
void await() throws InterruptedException;
/**
* 使当前线程等待,直到发出信号
*/
void awaitUninterruptibly();
/**
* 使当前线程等待,直到发出信号或中断它,或经过指定的等待时间
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
* 使当前线程等待,直到发出信号或中断它,或经过指定的等待时间
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
* 使当前线程等待,直到发出信号或中断它,或经过指定的等待期限
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
* 唤醒一个等待线程
*/
void signal();
/**
* 唤醒所有等待的线程
*/
void signalAll();
}
2.3、公平锁与非公平
公平锁:依次进入等待队列中排序,每次获取锁的时候判断是否是下一个可执行的线程
非公平锁:直接尝试获取锁,失败则放到等待队列中,以自旋的方式尝试获取锁
3、ReentrantLock
可扩展的可重入互斥锁,可选是否为公平锁。
- true:公平锁
- false或无参构造:非公平锁
重入:是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。
3.1、获取锁
非公平锁获取锁
:在第一次获取锁NonfairSync.lock()后,修改state为1同时AbstractOwnableSynchronizer.exclusiveOwnerThread记录锁的线程持有者(设置线程独占
),第二次获取锁时(假设第一次已拿到锁,并未释放),发现state的状态值为1,无法获取锁,继续判断持有锁的线程是否是当前线程,如图
public static void main(String[] args) {
// 多线程方式获取锁,演示非公平锁获取锁的先后顺序
ReentrantLock reentrantLock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
tryLock(reentrantLock);
}
},"线程"+i).start();
}
}
public static void tryLock(ReentrantLock reentrantLock){
String name = Thread.currentThread().getName();
System.out.println(name+",获取锁");
reentrantLock.lock();
try {
System.out.println(name+",获取到锁-----");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
System.out.println(name+",释放锁");
}
}
}
3.2、释放锁
3.3、条件队列addConditionWaiter
ReentrantLock可以实现synchronized的wait、notify类似的功能。并且功能比synchronized更加强大,能够实现中断、限时等线程通信方式
3.3.1、线程等待
3.3.2、线程唤起
3.3.3、代码示例
public class DemoReentrantLock {
static class NumberWrapper {
public volatile int value = 1;
}
public static void main(String[] args) {
//初始化可重入锁
final ReentrantLock lock = new ReentrantLock();
// 线程A条件
final Condition threeACondition = lock.newCondition();
// 线程B条件
final Condition threeBCondition = lock.newCondition();
final NumberWrapper num = new NumberWrapper();
//初始化A线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("threadA 开始执行");
//需要先获得锁
lock.lock();
try {
System.out.println("threadA 获取到锁");
//A线程先输出前3个数
while (num.value <= 3) {
System.out.println(num.value);
num.value++;
}
// 唤起线程
System.out.println("threadA 唤起等待的线程");
threeACondition.signal();
} finally {
System.out.println("threadA 释放锁1");
lock.unlock();
}
lock.lock();
try {
//等待输出6的条件
System.out.println("threadA 线程等待被唤起");
threeBCondition.await();
System.out.println("threadA 继续执行"+ num.value);
//输出剩余数字
while (num.value <= 9) {
System.out.println(num.value);
num.value++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("threadA 释放锁2");
lock.unlock();
}
}
},"threadA");
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("threadB 开始执行1");
lock.lock();
System.out.println("threadB 获取到锁1");
while (num.value <= 3) {
// 等待3输出完毕的信号
System.out.println("threadB 线程等待被唤起");
threeACondition.await();
System.out.println("threadB 线程被重新唤起");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("threadB 释放锁1");
lock.unlock();
}
try {
System.out.println("threadB 开始执行2");
lock.lock();
//已经收到信号,开始输出4,5,6
System.out.println("threadB 获取到锁2");
while (num.value <= 6) {
System.out.println(num.value);
num.value++;
}
//4,5,6输出完毕,告诉A线程6输出完了
System.out.println("threadB 唤起等待的线程");
threeBCondition.signal();
} finally {
System.out.println("threadB 释放锁2");
lock.unlock();
}
}
},"threadB");
//启动两个线程
threadB.start();
threadA.start();
}
}
3.4、ReentrantLock对比Synchronized
对比内容 | ReentrantLock | Synchronized |
---|---|---|
构成 | java语言的关键字,是原生语法层面的互斥,需要jvm实现,监视器锁(monitor)的monitorenter和monitorexit实现加锁和解锁 | 它是JDK 1.5之后提供的API层面的互斥锁类 |
实现 | 通过JVM加锁解锁,自动释放锁 | api层面的加锁解锁,需要手动释放锁。 |
代码编写 | 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全 | ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块保证释放锁 |
灵活性 | 锁的范围是整个方法或synchronized块部分 | Lock因为是方法调用,可以跨方法,灵活性更大 |
等待可中断 | 不可中断,除非抛出异常 | 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待(1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;2.lockInterruptibly()放代码块中,调用interrupt()方法可中断) |
是否公平锁 | 非公平锁 | 两者都可以,默认非公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁,无参非公平锁 |
线程间通信 | wait()与notify()都只能在synchronized方法或代码块中使用,wait()与notify()都是Object的方法 | 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能 |
获取锁的状态 | 提供很多方法用来监听当前锁的信息,如:getHoldCount() 、getQueueLength()、 isFair()、isHeldByCurrentThread()、isLocked() | |
适用场景 | 资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,可读性非常好 | ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。 |
建议用 synchronized 开发
,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock 性能会更好
。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
4、读写锁ReentrantReadWriteLock
ReentrantReadWriteLock 是读写锁
,和ReentrantLock会有所不同,对于读多写少的场景使用ReentrantReadWriteLock 性能会比ReentrantLock高出不少
。在多线程读时互不影响,不像ReentrantLock即使是多线程读也需要每个线程获取锁。不过任何一个线程在写的时候就和ReentrantLock类似,其他线程无论读还是写都必须获取锁。需要注意的是同一个线程可以拥有 writeLock 与 readLock (但必须先获取 writeLock 再获取 readLock, 反过来进行获取会导致死锁)
从构造方法上可知同ReentrantLock一样支持公平锁与非公平锁可选
4.1、功能
-
支持公平和非公平的获取锁的方式;
-
支持可重入:读线程在获取读锁后还可以获得读锁。写线程获取写锁后可以再次获得写锁或者读锁。
-
允许从写入锁降级为读取锁:先获取写锁,再获取读锁,最后释放写锁。不允许从读锁升级到写锁;
-
读锁和写锁都支持锁获取期间的中断;
-
Condition支持写入锁提供一个Condition实现,读取锁不支持Condition(抛出UnsupportedOperationException).;
缺点
如果有线程在读是无法获取写锁的
- 饥饿问题
5、StampedLock
StampedLock与ReadWriteLock相比,在读的过程中也允许后面的一个线程获取写锁对共享变量进行写操作,为了避免读取的数据不一致,使用StampedLock读取共享变量时,需要对共享变量进行是否有写入的检验操作,并且这种读是一种乐观读。
StampedLock在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,使用乐观读避免数据不一致的问题,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁。
允许读写锁之间相互转换
,可以更细粒度控制并发
设计初衷:作为一个内部工具类,辅助开发其他线程安全组件,容易产生死锁或其他问题,不支持重入
5.1、StampedLock三种锁模式
StampedLock与ReadWriteLock,ReadWriteLock支持两种锁模式:一种是读锁,另一种是写锁,并且ReadWriteLock允许多个线程同时读共享变量,在读时,不允许写,在写时,不允许读,读和写是互斥的,所以,ReadWriteLock中的读锁,更多的是指悲观读锁。
StampedLock支持三种锁模式:写锁、读锁(悲观读锁)和乐观读(乐观读锁)。写锁和读锁与ReadWriteLock中的语义类似,允许多个线程同时获取读锁,但是只允许一个线程获取写锁,写锁和读锁也是互斥的。与ReadWriteLock不同的地方在于,StampedLock在获取读锁或者写锁成功后,都会返回一个Long类型的变量,之后在释放锁时,需要传入这个Long类型的变量。
StampedLock内部是基于CLH锁实现的,CLH是一种自旋锁,能够保证没有“饥饿现象”的发生,并且能够保证FIFO(先进先出)的服务顺序。
在CLH中,锁维护一个等待线程队列,所有申请锁,但是没有成功的线程都会存入这个队列中,每一个节点代表一个线程,保存一个标记位(locked),用于判断当前线程是否已经释放锁,当locked标记位为true时, 表示获取到锁,当locked标记位为false时,表示成功释放了锁。
5.2、示例
public class DemoStampedLock {
private static StampedLock stampedLock = new StampedLock();
private static int data = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
get();
}
}, "threadA").start();
new Thread(new Runnable() {
@Override
public void run() {
get();
}
}, "threadB").start();
new Thread(new Runnable() {
@Override
public void run() {
get();
set(2);
}
}, "threadC").start();
new Thread(new Runnable() {
@Override
public void run() {
get();
set(2);
}
}, "threadD").start();
}
public static void get(){
//上读锁,其他线程只能读不能写
System.out.println(Thread.currentThread().getName()+"-"+"等待获取乐观读锁");
long readLockId = stampedLock.tryOptimisticRead();
// 检查是否有写锁在操作
if(!stampedLock.validate(readLockId)){
System.out.println(Thread.currentThread().getName()+"-"+"有写锁操作数据,升级为悲观读锁");
// 如果有写锁操作可能造成数据不一致所以升级为读锁
readLockId = stampedLock.readLock();
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"-"+"get:"+data+","+formatDate(new Date()));
Thread.sleep(400);
// 转换为写锁
// stampedLock.tryConvertToWriteLock(readLockId);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
System.out.println(Thread.currentThread().getName()+"-"+"释放读锁");
stampedLock.unlockRead(readLockId);
}
}
}
public static void set(int setData){
//上写锁,不允许其他线程读也不允许写
System.out.println(Thread.currentThread().getName()+"-"+"等待获取写锁");
long writeLockId = stampedLock.writeLock();
try {
data = setData;
System.out.println(Thread.currentThread().getName()+"-"+"set:"+data+","+formatDate(new Date()));
Thread.sleep(1500);
// 转换为读锁
// stampedLock.tryConvertToReadLock(writeLockId);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
System.out.println(Thread.currentThread().getName()+"-"+"释放写锁");
stampedLock.unlockWrite(writeLockId);
}
}
/**
* 格式化时间
* @param date 时间
* @param format 返回及入参格式类型 例如:yyyy-MM-dd HH:mm:dd:ss.S
* @return
*/
public static String formatDate(Date date, String format) {
return new SimpleDateFormat(format).format(date.getTime());
}
public static String formatDate(Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:dd:ss.S").format(date.getTime());
}
}
5.3、对比ReentrantReadWriteLock不足
StampLock不支持重入,不支持条件变量,线程被中断时可能导致CPU暴涨