JUC并发编程之:简单概述(六)
一、AQS
1.1、AQS原理
·AQS全程AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具类 的 框架
【类似synchronized,阻塞式锁】
##特点:
·用state属性来表示资源的状态(独占模式 和 共享模式),子类需要定义如何维护这个状态,
控制如何获取锁和释放锁
>getState:获取state状态
>setState:设置state状态
>compareAndSetState:乐观锁机制设置state状态
>独占模式是只有一个线程能够访问资源
>共享模式可以允许多个线程访问资源
·提供了基于FIFO的等待队列,类似于Monitor的EntryList
·条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor的WaitSet
##子类主要实现这样一些方法:
tryAcquire 尝试获取锁(独占锁)
tryRelease 尝试释放锁(独占锁)
tryAcquireShared 获取共享锁
tryReleaseShared 释放共享锁
isHeldExclusively 是否持有独占锁
【AQS中阻塞和恢复线程使用的是park和unpark】
1.2、自定义锁
/**
* 自定义一个不可重入锁
*/
@Slf4j
public class CustomAQS {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
try {
log.debug("t1 locking...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.debug("t1 unlock...");
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try {
log.debug("t2 locking...");
}finally {
log.debug("t2 unlock...");
lock.unlock();
}
},"t2").start();
}
}
//自定义锁(不可重入)
class MyLock implements Lock {
//自定义同步器类
class MySync extends AbstractQueuedSynchronizer{
@Override//尝试获取锁(独占锁)
protected boolean tryAcquire(int arg) {
//将资源状态state设置为1--占用
if(compareAndSetState(0,1)){
//设置owner线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override//尝试释放锁 (独占锁)
protected boolean tryRelease(int arg) {
//将owner线程设置为空
//非volatile可见的
setExclusiveOwnerThread(null);
//设置资源状态state为0--未被占用
//state为volatile可见的,有写屏障,因此将owner设置放在其上
setState(0);
return true;
}
@Override//是否持有独占锁
protected boolean isHeldExclusively() {
return getState()==1;
}
//条件变量
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync mySync = new MySync();
@Override //加锁(不成功会进入等待队列)
public void lock() {
mySync.acquire(1);
}
@Override //加锁(可打断的)
public void lockInterruptibly() throws InterruptedException {
mySync.acquireInterruptibly(1);
}
@Override //尝试加锁(尝试一次)
public boolean tryLock() {
return mySync.tryAcquire(1);
}
@Override //尝试加锁(带超时的)
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return mySync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override //解锁
public void unlock() {
//tryRelease不会唤醒其他等待的线程,release会
mySync.release(0);
}
@Override //条件变量
public Condition newCondition() {
return mySync.newCondition();
}
}
##结果:
10:49:19.965 [t1] DEBUG - t1 locking...
10:49:21.976 [t1] DEBUG - t1 unlock...
10:49:21.976 [t2] DEBUG - t2 locking...
10:49:21.976 [t2] DEBUG - t2 unlock...
二、读写锁
2.1、ReentrantReadWriteLock
·当读操作远远高于写操作时,这时候使用读写锁让 读 < -- >读 可以并发,提高性能
##例子:一个数据容器,共享数据data,我们使用读锁保护数据的read方法,
用写锁保护数据的write方法
@Slf4j
public class ReadWriteLockTest {
public static void main(String[] args) {
DataContainer dContainer = new DataContainer("a");
new Thread(()->{
dContainer.read();
},"t1").start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
dContainer.write("b");
},"t3").start();
}
}
//数据容器
@Slf4j
@Data
class DataContainer{
//共享数据
private Object obj;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//读锁
private ReentrantReadWriteLock.ReadLock r = lock.readLock();
//写锁
private ReentrantReadWriteLock.WriteLock w = lock.writeLock();
public DataContainer(Object obj) {
this.obj = obj;
}
public Object read(){
log.debug("尝试获取读锁");
r.lock();
log.debug("获取读锁成功");
try {
Thread.sleep(1000);
log.debug("读取数据"+obj);
return obj;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
log.debug("释放读锁");
r.unlock();
log.debug("释放读锁成功");
}
}
public void write(Object res){
log.debug("获取写锁");
w.lock();
log.debug("获取写锁成功");
try {
log.debug("修改数据"+res);
setObj(res);
} finally {
log.debug("释放写锁");
w.unlock();
log.debug("释放写锁成功");
}
}
}
注意事项:
·读锁不支持条件变量Condition
·重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
【读的时候不可以写】
如下:
r.lock();
try{
//...
try{
//...
w.lock();
}finally{
w.unlock();
}
}finally{
r.unlock();
}
·重入时支持降级:即支持持有写锁的情况下去获取读锁
【写的时候可以读】
【读读可并发】
【读写互斥】
##其他
·读写锁用的是同一个Sycn同步器,因此等待队列、state也是同一个
·写锁占据了state的低16位,读锁使用了state的高16位
2.2、StampedLock
·StampedLock自JDK8加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都
必须配合【戳】的使用
##加戳读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
##加戳写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
##乐观读
StampedLock支持tryOptimisticRead()方法(乐观读),读取完毕后需要做一次
戳校验,如果校验通过表示这期间确实没有写操作,数据可以安全使用,如果校验没有通过,
需要重新获取读锁,保证数据安全
//这个方法没有加任何锁
long stamp = lock.tryOptimisticRead();
//验戳
if(!lock.validate(stamp)){
//验证失败,锁升级,可以使用lock.readLock()
}else{
//验证成功,证明这期间没有写操作
}
##注意:
·stampedLock不支持条件变量
·stampedLock不支持可重入
/**
* 读写锁:乐观读
*/
@Slf4j
public class StampedLockTest {
public static void main(String[] args) {
DataContainerStamped containerStamped = new DataContainerStamped("a");
new Thread(()->{
containerStamped.read();
},"t1").start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
containerStamped.write("b");
},"t2").start();
}
}
@Slf4j
@Data
class DataContainerStamped{
//共享数据
private Object Obj;
//读写锁
private StampedLock lock = new StampedLock();
public DataContainerStamped(Object obj) {
Obj = obj;
}
public Object read(){
//乐观读
log.debug("乐观读");
long stamp = lock.tryOptimisticRead();
log.debug("乐观读 stamp:{}",stamp);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//乐观读验证--是否期间有写操作
if(lock.validate(stamp)){
log.debug("乐观读验证成功,期间没有写操作 stamp:{}, 读取数据obj:{}",stamp,getObj());
return getObj();
}
log.debug("乐观读验证失败,锁升级");
log.debug("尝试获取读锁");
stamp = lock.readLock();
log.debug("尝试获取读锁,stamp:{}",stamp);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("读取数据");
log.debug("读取数据 obj:{}",getObj());
return getObj();
} finally {
log.debug("尝试释放读锁 stamp:{}",stamp);
lock.unlockRead(stamp);
log.debug("释放读锁成功 stamp:{}",stamp);
}
}
public void write(Object res){
log.debug("尝试获取写锁");
long stamp = lock.writeLock();
log.debug("获取写锁成功 stamp:{}",stamp);
try {
log.debug("修改数据"+res);
setObj(res);
} finally {
log.debug("尝试释放写锁 stamp:{}",stamp);
lock.unlockWrite(stamp);
log.debug("释放写锁成功 stamp:{}",stamp);
}
}
}
##结果:
17:16:51.894 [t1] DEBUG - 乐观读
17:16:51.896 [t1] DEBUG - 乐观读 stamp:256
17:16:52.393 [t2] DEBUG - 尝试获取写锁
17:16:52.393 [t2] DEBUG - 获取写锁成功 stamp:384
17:16:52.393 [t2] DEBUG - 修改数据b
17:16:52.393 [t2] DEBUG - 尝试释放写锁 stamp:384
17:16:52.393 [t2] DEBUG - 释放写锁成功 stamp:384
17:16:53.897 [t1] DEBUG - 乐观读验证失败,锁升级
17:16:53.897 [t1] DEBUG - 尝试获取读锁
17:16:53.897 [t1] DEBUG - 尝试获取读锁,stamp:513
17:16:53.897 [t1] DEBUG - 读取数据
17:16:53.897 [t1] DEBUG - 读取数据 obj:b
17:16:53.897 [t1] DEBUG - 尝试释放读锁 stamp:513
17:16:53.897 [t1] DEBUG - 释放读锁成功 stamp:513
##注意:
·stampedLock不支持条件变量
·stampedLock不支持可重入
三、其他工具类
3.1、Semaphore信号量
·Semaphore信号量,用来限制能同时访问共享资源的线程上限
【限制共享资源的并发访问数量】
【同之前的ReentrantLock之类的还是不一样的,之前的都是独占共享资源】
·使用semaphore可以做简单的限流,在访问高峰期时,让请求线程阻塞,高峰期过去再
释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数。
【适用于资源数和线程数一样的情况,如数据库连接池】
@Slf4j
public class SemaphoreTest {
public static void main(String[] args) {
//信号量--限制资源的访问数
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i <10 ; i++) {
new Thread(()->{
try {
//获取信号量(总信号量减1)
semaphore.acquire();
log.debug("running");
Thread.sleep(1000);
log.debug("end");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放信号量(总信号量加1)
semaphore.release();
}
},"t"+i).start();
}
}
}
##结果:
10:57:21.996 [t0] - running
10:57:21.996 [t4] - running
10:57:21.996 [t3] - running
10:57:22.998 [t4] - end
10:57:22.998 [t3] - end
10:57:22.998 [t0] - end
10:57:22.998 [t1] - running
10:57:22.998 [t2] - running
10:57:22.998 [t5] - running
10:57:23.999 [t2] - end
10:57:23.999 [t5] - end
10:57:23.999 [t1] - end
10:57:23.999 [t6] - running
10:57:23.999 [t8] - running
10:57:23.999 [t9] - running
10:57:25.000 [t6] - end
10:57:25.000 [t9] - end
10:57:25.000 [t8] - end
10:57:25.000 [t7] - running
10:57:26.001 [t7] - end
3.2、CountDownLatch倒计数锁
·CountDownLatch用来进行线程同步协作,等待所有线程完成倒计时,才恢复运行
·几种构造参数用来初始化等待计数值,await()用来等待计数清零,countDown()用来让
计数减一
##类似join()
@Slf4j
public class CountDownLatchTest {
public static void main(String[] args) {
//初始化倒计数值为3
CountDownLatch countDownLatch = new CountDownLatch(3);
log.debug("main begin...");
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(()->{
log.debug("t1 running");
//倒计数减1
countDownLatch.countDown();
log.debug("t1 end...");
});
executorService.submit(()->{
log.debug("t2 running");
//倒计数减1
countDownLatch.countDown();
log.debug("t2 end...");
});
executorService.submit(()->{
log.debug("t3 running");
//倒计数减1
countDownLatch.countDown();
log.debug("t3 end...");
});
log.debug("main waiting...");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main running");
}
}
##结果:
11:21:06.141 [main] - main begin...
11:21:06.177 [t1] - t1 running
11:21:06.177 [t1] - t1 end...
11:21:06.177 [main] - main waiting...
11:21:06.178 [t3] - t3 running
11:21:06.178 [t2] - t2 running
11:21:06.178 [t3] - t3 end...
11:21:06.178 [t2] - t2 end...
11:21:06.178 [main] - main running
3.3、Cyclicbarrier循环栅栏
·Cyclicbarrier循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置
【计数个数】,每个线程执行到某个需要“同步”的时刻调用await()方法进行等待,
当等待的线程数满足【计数个数】时,继续执行
让一组线程达到屏障时(阻塞点)被阻塞,直到最后一个线程也达到了屏障时,屏障才会打
开,所有被屏障的线程才会继续干活。
【它会再计数编程0后,直接打印,下次再调用直接恢复原值】
【CountDownLatch的增强版】
@Slf4j
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->{
log.debug("召唤神龙!");
});
ExecutorService pool = Executors.newFixedThreadPool(7);
String[] dragonEge = new String[]{"赤","橙","黄","绿","青","蓝","紫"};
for (int i = 0; i <7 ; i++) {
int temp = i;
Future<String> f = pool.submit(() -> {
log.debug("开始收集 {}色 龙珠", dragonEge[temp]);
Thread.sleep(1000);
log.debug("收集 {}色 龙珠成功", dragonEge[temp]);
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
return dragonEge[temp];
});
}
pool.shutdown();
}
}
##结果:
14:53:22.413 [pool-1-thread-3]- 开始收集 黄色 龙珠
14:53:22.413 [pool-1-thread-5]- 开始收集 青色 龙珠
14:53:22.413 [pool-1-thread-6]- 开始收集 蓝色 龙珠
14:53:22.413 [pool-1-thread-4]- 开始收集 绿色 龙珠
14:53:22.413 [pool-1-thread-7]- 开始收集 紫色 龙珠
14:53:22.413 [pool-1-thread-1]- 开始收集 赤色 龙珠
14:53:22.413 [pool-1-thread-2]- 开始收集 橙色 龙珠
14:53:23.416 [pool-1-thread-7]- 收集 紫色 龙珠成功
14:53:23.416 [pool-1-thread-3]- 收集 黄色 龙珠成功
14:53:23.416 [pool-1-thread-6]- 收集 蓝色 龙珠成功
14:53:23.416 [pool-1-thread-1]- 收集 赤色 龙珠成功
14:53:23.416 [pool-1-thread-4]- 收集 绿色 龙珠成功
14:53:23.416 [pool-1-thread-5]- 收集 青色 龙珠成功
14:53:23.416 [pool-1-thread-2]- 收集 橙色 龙珠成功
14:53:23.416 [pool-1-thread-2]- 召唤神龙!
3.4、线程安全集合类概述
##一、遗留的安全集合(synchronized修饰)
如:Hashtable Vector
##二、修饰的安全集合(使用Collections的方法修饰,本质上还是synchronized)
如:SynchronizedMap SynchronizedList
Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet
##三、JUC安全类
Blocking类 CopyOnWrite类 Concurrent类