Java中,锁的种类按照不同的维度可以区分出很多的类型,比如是否公平?可读写?可重入?等等。而本文主要介绍JUC并发包Lock接口下锁的实现,以及对常见的锁类型介绍、在实际使用中应该注意什么等。
如下图,Lock接口及实现类。
一、区分synchronized和Lock
这两者最大的区别:synchronized是Java的一个关键字,Lock是Java并发包下的一个接口。
细分两者区别:
二、Java锁机制
1、公平锁与非公平锁
回到本文的主题,Lock接口,用创建ReentrantLock实例来说明在创建一个它的一个实例对象的时候都干了什么?打开源码看看。
如上图,默认的构造方法会创建一个非公平锁,而这个锁则是继承一个叫做AbstractQueuedSynchronizer的类(简称AQS)。这个AQS类是源码作者Doug Lea大神在JUC并发包中用来构建锁或者其他同步组件(信号量、事件等)的基础框架类,毫不夸张的说只要弄懂这个同步队列的实现,基本整个JUC理解起来都不会有太大困难,等参悟透了再写文章分享。AQS是一个高度抽象的类,用于解决在多线程编程中遇到的并发同步、锁等复杂问题。首先从基础说起,公平锁和非公平锁指的是什么?
- 公平锁:指的是多个线程按照申请锁的顺序来获取锁,先到先得,FIFO(First In First Out)。
- 非公平锁:指的是多个线程获取锁的顺序不是按照申请顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
在实际的开发中,使用非公平锁能够保证更大的并发吞吐量,所以默认的构造器采用的是非公平锁。而synchronized本身其实就是一种非公平锁。
非公平锁和公平锁代码示例,如下图。
2、可重入锁
可重入锁:是指在同一线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。简而言之,就是链式调用使用锁的方法的时候,如果方法链上的方法都被加锁了,那么只要是使用可重入锁进行锁定的代码块都无需要再次加锁或者释放锁资源。而synchronized本身就是可重入锁。
ReentrantLock 和 Synchronized 默认就是非公平的可重入锁。可重入锁最大的作用就是避免死锁。如下两代码截图。
synchronized是可重入锁,不需要手动加锁、释放锁
使用ReentrantLock对象创建一个可重入锁,需要手动加锁和释放锁,并且加锁和释放锁的次数需要配对。
3、自旋锁
自旋锁:指的是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。CAS算法的底层实现就是使用自选锁,之前的文章中已经说过。下图:手写自旋锁。
4、独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁:指的是该锁只能被一个线程所持有。对ReentrandLock和Synchroized而言都是独占锁。
共享锁:指的是该锁可以被多个线程持有。读锁的共享锁可以保证并发读是非常高效的。读写、写读、写写的过程是互斥的。
如下图:读写不加锁
如下图:读写加锁
三、CountDownLatch / CyclicBarrier / Semaphore(倒计时锁/循环屏障/信号灯)
假设需要在某个节点的时候让特定的线程运行或者不运行,不使用这些组件的时候很难控制,如下,想让main线程在最后执行。对应在实际的例子中就是,等多线程计算完毕,main线程去获取计算结果,什么时候计算完毕不可控,得到的结果也就不正确,因为main线程不知道什么时候就去执行了。
CountDownLatch
让一些线程阻塞直到另一个线程完成一系列操作之后才被唤醒。CountDownLatch主要有两个方法,当一个或者多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法会将计数器减一(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因调用await方法被阻塞的线程会唤醒,继续执行。
CyclicBarrier
Cyclic可循环,Barrier 屏障。它要做的事情是:让一组线程到达一个屏障(也可以叫做同步点)的时候被阻塞,直到最后一个线程到达屏障的时候,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。
Semaphone 信号灯 (争车位)
典型的多个线程抢多个资源的类型,可以代替synchroized 和 lock。小米秒杀系统多半采用这种设计。
来源:CSDN
作者:destiny ~
链接:https://blog.csdn.net/XiaoA82/article/details/103459780