java并发包的基石:AbstractQueuedSychronier及synchornized
Posted 我爱看明朝
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发包的基石:AbstractQueuedSychronier及synchornized相关的知识,希望对你有一定的参考价值。
java并发包的基石:AbstractQueuedSychronier
简介
AQS: AbstractQueuedSychronizer(抽象的队列同步器)是java的j.u.c包中Lock、Semaphore、ReentrantLock等这些锁都是基于AQS框架实现的。
AQS有两种模式:
1. 独占模式ReentrantLock 一次只有一个线程可以竞争到锁
2. 共享模式 CountDownLatch 一次可以多个线程获取到资源
核心
AQS核心数据结构:
// CAS: compare and set 共享资源通过cas操作来保证并发修改的正确性
// state有两种共享方式:1. exclusive独占锁 ReentrantLock 2.share共享锁 CountDownLatch
volatile int state;
// 同步队列
FIFO CHL;
// 当前独占锁占有的线程
Thread exclusiveOwnerThread;
自定义同步器
不同的自定义同步器竞争共享资源的方式也不同,自定义同步器在实现时只需要实现共享资源state的获取和释放即可,至于具体线程等待队列的维护(如获取资源失败入队,唤醒出队等), AQS已经在顶层实现好了,自定义同步器实现时主要实现以下几种方法:
// 该线程是否正在独占资源
isHeldExclusively();
// 独占方式: 尝试获取资源,成功返回true,失败返回false
tryAcquire(int);
// 独占方式,释放资源,成功返回true,失败返回false
tryRelease();
// 共享方式,尝试获取资源,负数表示失败,0表示成功,但是没有剩余资源;正数表示成功,且有剩余资源
tryAcquireShared(int);
// 共享方式,尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回false
tryReleaseShared(int)
ReentrantLock
ReentrantLock reentrantLock = new ReentrantLock();
//加锁
reentrantLock.lock();
//释放锁
reentrantLock.unlock();
加锁
lock & unlock
public void lock()
sync.lock();
final void lock()
acquire(1);
public final void acquire(int arg)
// tryAcquire获取到锁 直接返回
// tryAcquire没有获取到锁,acquireQueued当前线程加入等待队列
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
tryAcquire
fairSync 公平锁
protected final boolean tryAcquire(int acquires)
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0)
// 当前没有线程获取到锁
// cas compare and set
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))
// 设置当前线程获取到锁
setExclusiveOwnerThread(current);
return true;
// 可重入锁,当前线程再次获取到锁
else if (current == getExclusiveOwnerThread())
if nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
// 设置 state值,为当前重入次数
setState(nextc);
return true;
// 无法获取到锁
return false;
acquireQueued
final boolean acquireQueued(final Node node, int arg)
// 标记是否获取到锁资源
boolean failed = true;
try
// 标记等待过程中是否被中断
boolean interrupted = false;
//自旋锁
for (;;)
// 当前尝试获取锁的节点的前一个节点
final Node p = node.predecessor();
// 如果当前进程时队列中的老二,那么便可尝试获取锁(等待第一个线程释放锁就可以获取到锁了)
if (p == head && tryAcquire(arg))
// 可以获取到锁,将head指向该节点
setHead(node);
p.next = null;
// 表示成功获取到锁了
failed = false;
//返回等待过程中是否被中断过
return interrupted;
// 是否可以被挂起,挂起后等待唤醒
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 有被中断过
interrupted = true;
finally
// 自旋锁没有获取到锁,可能是超时中断,那么可以取消节点在队列中的等待
if (failed)
cancelAcquird(node);
isHeldExclusively
//该线程是否正在独占锁
protected final boolean isHeldExclusively()
return getExclusiveOwnerThread() == Thread.currentThrea();
class AbstractOwnableSynchronizer
private transient Thread exclusiveOwnerThread;
// 获取当前获取锁的线程
protected final Thread getExclusiveOwnerThread()
return exclusiveOwnerThread;
释放锁
public void unlock()
sync.release(1);
public final boolean release(int arg)
if (tryRelease(arg))
// 表示锁已经完全释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
return false;
protected final boolean tryRelease(int release)
int c = getState() - releases;
// 当前线程是不是获取到锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitirStateException();
boolean free = false;
// state等于0比表示占领锁的线程已经全部(重入锁)被释放了
if (c == 0)
free = true;
//exclusiveOwnerThread 设置为null
setExclusiveOwnerThread(null);
setState(c);
return free;
java中的一些锁
synchornized
synchornized关键字是同步锁,可以加在对象、类、方法、代码块,静态方法;Sychornized是可重入的锁。
对象锁: 对象、方法、代码块
类锁:类、静态方法
类锁
public Test
public void demo()
// 同一个jvm只有一个线程某刻可以获取到锁
sychornized(Test.class)
// do something
静态方法
public Test
// 同一个jvm只有一个线程某刻可以获取到锁
public static sychornized void demo()
// do somethig
对象
public Test
public void demo()
// 同一个Test实例只有一个线程可以获取到锁
sychornized(this)
// do something
pulic void demo1()
Test test1 = new Test();
Test test2 = new Test();
// test1与test2可以同时获的锁,因为他们获得是两把锁
test1.demo();
test2.demo();
方法
pubic Test
// 同一个Test实例只有一个线程某刻可以获取到锁
public sychornized void demo()
// de something
代码块
public Test
// 同一个Test实例只有一个线程某刻可以获取到锁
pulic void demo(Test test)
sychornized(test)
// do something
原理
当代码块使用sychornized编译后会在代码中加入监视器Monitor。
编译后的代码:
monitorenter
// 代码块编译后额命令
monitorexit
当方法声明使用sychornized时,方法编译后会设置ACC_SYNCRONIZED来标识;
当方法调用时,设置指针将会检查方法的ACC_SYNCRONIZED是否被设置了,如果被设置了,执行线程将先持有monitor,然后在执行方法,最后方法完成时释放 monitor。
sychornized是可重入的锁,因此当持有的锁再一次获取到锁,monitor的计数器仍然会加1.每一次释放monitor计数器会减一。
sychornized的锁升级过程: 无锁偏向锁-------> 轻量级锁(自旋锁) ---------> 重量级锁
偏向锁:实际无竞争,只有一个线程在获取锁,在对象的mark word通过cas记录owner为当前线程,owner的值一定会从null到有值的。
轻量级锁:自旋的获取锁,不需要进行线程切换,适合持有锁时间短,竞争不激烈,也是通过cas设置owner的值,多个线程交替短时间内持有锁
重量级锁: 当自旋一段时间,无法获取锁,轻量级锁升级为重量级锁,重量级锁通过 monitor监视器实现。
从这里看起来sychornized重入的实现和AQS的state思想是一致的。
sychornized和ReentranLock的区别
实现上: sychornized是java的关键字,有jvm实现,ReentranLock是API层面提供的。
使用上:sychornized有编译器保证锁的加锁和释放;ReentranLock需要手工声明来加锁和释放锁,忘记释放锁会造成死锁。
性能:sychornized优化后移入了偏向锁、轻量级锁(自旋锁)后两者性能差不多,官方建议如果使用场景一致,建议使用sychornized。
公平非公平锁:sychornized是非公平的,ReentranLock默认是非公平锁,可以设置为公平锁,构造方法传true。
功能区别上:
ReentranLock支持:
-
等待可中断 持有锁的线程长时间不释放的时候,正在等待的锁可以选择放弃等待,lockInterruptibly方法
-
公平锁, ReentranLock可以实现公平锁
-
锁定多个条件 ReentranLock提供了一个条件类Condition,用来实现多个对象分组唤醒线程;sychornized只能要么随机唤醒一个线程要么全部唤醒
两者的实现:
sychornized实现: 重量级锁通过monitor entry 和 monitor exist来加锁代码,偏向锁和轻量级锁通过mark word对owner字段cas设置。
ReentranLock通过AQS实现: state CHL 当前持有锁的线程。
如果选择:
当要使用ReentranLock的三个特性: 等待可中断、公平锁、多条件则选择ReentranLock,否则用sychronized
死锁
两辆车在单行道桥的两端,如果一方不让出路来,那么两俩车都过不了,这就是死锁。
innodb处理死锁的方式: 将持有最少行派他锁的事务进行回滚。
我们在实际开发中,避免死锁:大事务业务上允许插成小事务就插成是小事务。
活锁
两辆这在单行道桥的两端,两方都在给对方让路,那么两辆车都过不去,这就是活锁。
公平与非公平锁
公平锁与非公平锁在AQS中的实现:
- 公平锁:当线程想要获取锁,直接插入到CHL队列的队尾,当前线程释放了锁state等于0,则从队列队首获得锁,队列从队首到队尾依次获得。
- 当线程想要获取锁,参与竞争锁,如果竞争到则获的锁,没有竞争到,插到CHL队列队尾,后面的和公平锁一样。
也就是公平锁与非公平锁的区别是: 当线程获取锁是直接参与竞争还是不参与直接插入队尾。
重入与不可重入
可重入锁的实现:当获取state不等于0,以及持有锁的线程等于当前线程则state加1,释放的时候state减1,直到为0。
可重入锁:同一个线程可以多次获取同一把锁
不可重入锁;同一个线程如果要再次获取锁,必须等待之前获取到的锁释放,不可重入锁很容易引入死锁。
自旋锁
一个线程尝试获取锁,获取不到,不会立即阻塞,而是采用循环的方式尝试获取,acquiedQueued方法中的for(;;)就是在自旋,自旋旋可以减少线程切换的上下文开销,但是如果自旋时间过长会非常耗费cpu的性能。
乐观与悲观锁
悲观锁
在操作资源前先加锁,例如数据库中的行锁,一个线程读取到准备操作资源,其他线程只能阻塞等待了。
悲观锁认为别人在操作资源的时候会对资源进行修改,所以在它持有资源的时候,会阻塞其他线程。
select col from table where col1 > 1 for update
乐观锁
乐观锁通过版本号来实现对资源的修改,当版本号不对则重新读取再次修改,不会阻塞其他线程。
如线程A,B同时读取到资源id = 2, data = 1, 他们都做加一操作:
则:
A: update set data = 2 , version = version +1 where id = 2 and version =1 成功
B: update set data =2, version =version +1 where id = 2 and verison =1 失败
CAS: compare and set.也是乐观锁,因为CAS没有版本号所以容易发生ABA问题。
参考
以上是关于java并发包的基石:AbstractQueuedSychronier及synchornized的主要内容,如果未能解决你的问题,请参考以下文章