避免线程在进入内核态的阻塞状态,是我们去分析和理解锁设计的关键
java.util.concurrent.locks.AbstractQueuedSynchronizer
简称AQS
底层 采用双向链表,是队列的一种实现,可以有多个Condition
设计:
1、使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架
2、利用了一个int类型表示状态
3、使用方法是继承
4、子类通过继承并通过实现它的方法管理其他状态{acquire和release}的方法操纵状态
5、可以同时实现排它锁和共享锁模式(独占、共享)
原理:AQS内部维护了一个ClH队列来管理锁,首先线程会尝试获取锁,如果失败,就将当前线程以及等待状态等信息包成一个Node节点,加入到Sync同步队列,然后会不断尝试获取锁(如果失败就会阻塞自己,直到自己被唤醒),当持有锁的线程释放锁的时候会唤醒队列中的后继线程
AQS同步组件
1、CountDownLatch:闭锁,通过计数来保证是否需要阻塞
核心方法:countDown(),await() 一般组合使用,等countDown减到0,执行await()方法,await也可以设置时间,到了设定的值后就执行await()后面的内容
2、Semaphore(信号量):控制同一时间并发执行的数目
使用场景:仅能提供有限访问的资源:例:数据库连接数
,提供了acquire()和release() 方法
3、CyclicBarrier:功能和CountDownLatch类似
允许一组线程相互等待,直到到达某个屏障点,通过它可以完成多个线程相互等待,只有每个线程都准备就绪的时候,才能往下面执行
原理:线程调用了await()方法后,该线程就进入了等待状态,且计数器执行+1操作,当计数值达到了我们设定的初始值,调用了await() 的线程会被唤醒,继续执行,在释放等待后可以重用,所以又称为循环屏障
应用场景:可以用于多线程计算数据,最后合并计算结果
和CountDownLatch区别:
1、countDownLatch 只能使用一次 ,CyclicBarrier可以调用resret()方法重置,使用多次
2、countDownLatch主要实现一个或n个线程需要等待其他线程完成某项操作之后,才能继续往下执行,主要描述一个线程或n个线程等待其他线程的过程。而CyclicBarrier实现多个线程相互等待,直到所有线程都满足了条件之后才能继续执行后续的操作
new CyclicBarrier(5);
给定一个值,指需要有几个线程同步等待,每一个线程准备好 后,调用await()方法,当达到我们定义的值时,await() 后面的操作 才会执行,也可是设置awiat(2000,TimeUtil.MILLISECONDS),想要执行下面的内容,必须要捕捉他可能抛出的异常
**4、ReentrantLock(可重入锁):
1)ReentrantLock和synchronize的区别
1、可重入性:都是可重入锁,都是同一个线程进入一次锁的计数器自增一,所以等计数器下降为0时,才能释放锁
2、锁的实现
1)synchronize 基于JVM实现的(操作系统实现)
ReentrantLock时jdk实现的(用户敲代码实现)
3、性能的区别
1)在synchronize 性能优化以前,性能要比ReentrantLock差很多,但是自从synchronize引入了偏向锁,轻量级锁(自旋锁),俩者就差不多了,在俩种都可以使用的情况下 ,更推介synchronize,写法更容易,它的优化就是借鉴了ReentrantLock的CAS,都是在试图在用户态就把加锁问题解决避免进入内核态的线程阻塞。
4、功能的区别
1)synchronize使用更方便,并且是由编译器保证加锁和释放的,而ReetrantLock则需要手动加锁和释放锁的,为了避免手动释放锁造成死锁,所以在finalliy里中释放锁
2)细粒度和灵活度:很明显,ReentrantLock要优于synchronize
2)ReentrantLock独有的功能
1*、可指定是公平锁还是非公平锁
2*、提供了一个Condition类,可以分组唤醒需要唤醒的锁
3*、提供能够中断等待锁的线程机制,lock.lockInterruptibly()
是一种自旋锁,通过循环调用CAS操作来实现加锁,他的性能比较好,也是因为避免了使线程进入内核态的阻塞状态
synchronize的优点:除非需要对Lock有明确的需要,在使用synochronize 不可能释放锁,在退出synochronize块的时候JVM会帮助你释放锁,再用synochronize的时候,如果出现死锁,JVM能标示死锁,有利于找到问题,操作比较简单
ReentrantReadWriteLock,有读锁 和写锁,在有读或写的操作时 是不允许写或读的
如果想获得写入锁的时候,坚决不允许有读锁来保持的,如果读取情况很多的时候,会造成线程饥饿,写锁要一直等待读锁完成。(实际应用场景很少)属于悲观
StampedLock(由版本和模式组成)有三种模式;
写
读
乐观读:
StampedLock对吞吐量由巨大的改进,特别是在读线程越来越多的场景下
总结 锁的选取方式(参考):
1、当只有少量的竞争者的时候,synchronize是很好的通用锁实现
2、竞争者不少,线程增长的趋势是我们可以预估的,ReetrantLock是很好的通用锁实现
5、Condition(条件):
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() -> {
try {
reentrantLock.lock();
log.info("wait signal"); // 1
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("get signal"); // 4
reentrantLock.unlock();
}).start();
new Thread(() -> {
reentrantLock.lock();
log.info("get lock"); // 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
log.info("send signal ~ "); // 3
reentrantLock.unlock();
}).start();
}
6、FutureTask:
实现线程的俩种常用方法继承Thread和实现Runnable,但是这俩种都无法获取线程的执行结果,从java1.5开始,提供了callable,Future,通过这俩个可以得到线程执行的结果
callable和Runable接口对比
runnable : public abstract void run();
callable : V call() throws Exception;
Future接口:可以监视目标线程调用call的情况,当调用get()方法后就可以获得它的结果,通常这个时候线程可能不会完成,当前线程就开始阻塞,直到call方法结束,返回结果,线程才会继续执行
总结:Future可以得到别的线程任务方法的返回值
FutureTask类:父类是RunnableFuture,RunnableFuture又继承了Runnable和Future俩个接口,所以他最终还是执行callable类型的任务,如果构造函数是Runnable类型的话,他会转换成callable类型,他既可以作为Runnable被线程执行,又可以作为Future得到callable的返回值
推介使用FutureTask
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<String> futureTask=new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "OK";
}
});
new Thread(futureTask).start();
log.info("do something in main");
Thread.sleep(1000);
String result = futureTask.get();
log.info("result : {}",result);
}