java主要分两类锁,一种是synchronized关键字修饰的锁,另一种是J.U.C.提供的锁。J.U.C里核心锁就是ReentrantLock
ReentrantLock(可重入锁)和synchronized区别
- 可重入性
- 锁的实现,synchronized关键字是依赖于JVM实现的,而ReentrantLock是JDK实现的,这两个有什么区别?说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。synchronized实现依赖于JVM,很难查到源码,而ReentrantLock可以通过阅读源码来实现。
- 性能区别,自从synchronized引入偏向锁、轻量级锁(自旋锁)之后性能差不多,官方建议使用synchronized。
- 功能区别,synchronized使用更方便,由编译器保证加锁与释放,而ReentrantLock需要手动声明加锁与释放锁。
ReentrantLock独有的功能:
- 可指定公平锁还是非公平锁,而synchronized只能是非公平锁。公平锁:先等待的线程先获得锁。
- 提供了一个Condition类,可以分组唤醒需要唤醒的线程。而不是像synchronized,随机
- 提供了一种能够中断等待锁的线程的机制,lock.lockInterruptibly()。ReentrantLock是一种自旋锁,通过循环调用CAS操作来实现加锁,性能比较好也是因为避免了使线程进入内核态的阻塞状态,想尽办法避免线程进入内核的阻塞状态,是我们分析和理解锁设计的关键钥匙
使用场景
如果需要ReentrantLock独有的功能时可以使用ReentrantLock。其他情况下可以根据性能来选择使用ReentrantLock还是synchronized。
synchronized能做的事情ReentrantLock都能做,而ReentrantLock能做的synchronize却不一定能做,性能方面ReentrantLock也不必synchronize差。那么我们要不要抛弃synchronize?当然是否定的,java.util.current.lock下的类一般用于高级用户和高级情况的工具,一般来说除非对lock的某个高级特性有明确的需要或者有明确的证据标明在特定的情况下,同步已经成为可伸缩性的瓶颈的时候,否则建议继续使用synchronized。
即使对于这些高级的锁定类来说,synchronized仍然有一些优势,比如在使用synchronized时候不可能忘记释放锁,在退出synchronize块时,jvm会为你做这些事情,否则一旦忘记释放锁而产生死锁很难查出原因,因此不建议初级开发人员使用lock。
另外当jvm用synchronized来管理锁定请求和释放时,jvm在生成线程转储时,能够包括锁定信息,这些对调试非常有价值,因为他们能标识死锁或者其他异常行为的来源。而lock类只是一个普通的类,jvm不知道具体哪个线程拥有lock对象,而且几乎每个开发人员都熟悉synchronized,可以在jvm的所有版本中工作,在大部分使用场景下都可以使用synchronized来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 1@Slf4j
2public class LockExample2 {
3
4 // 请求总数
5 public static int clientTotal = 5000;
6
7 // 同时并发执行的线程数
8 public static int threadTotal = 200;
9
10 public static int count = 0;
11
12 private final static Lock lock = new ReentrantLock();
13
14 public static void main(String[] args) throws InterruptedException {
15 //线程池
16 ExecutorService executorService = Executors.newCachedThreadPool();
17 //定义信号量
18 final Semaphore semaphore = new Semaphore(threadTotal);
19 //定义计数器
20 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
21 for(int i = 0; i < clientTotal; i++) {
22 executorService.execute(() ->{
23 try {
24 semaphore.acquire();
25 add();
26 semaphore.release();
27 } catch (InterruptedException e) {
28
29 log.error("exception", e);
30 }
31 countDownLatch.countDown();
32
33 });
34 }
35 countDownLatch.await();
36 executorService.shutdown();
37 log.info("count:{}", count);
38 }
39
40 public static void add() {
41 lock.lock();
42 try{
43 count++;
44 }finally {
45 lock.unlock();
46 }
47 }
48
49}
50
ReentrantReadWriteLock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 1@Slf4j
2public class LockExample3 {
3
4 private final Map<String, Data> map = new TreeMap<>();
5 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
6 private final Lock readLock = lock.readLock();
7 private final Lock writeLock = lock.writeLock();
8
9 public Data get (String key){
10 readLock.lock();
11 try{
12 return map.get(key);
13 } finally {
14 readLock.unlock();
15 }
16 }
17 public Set<String> getAllKeys() {
18 readLock.lock();
19 try{
20 return map.keySet();
21 }finally {
22 readLock.unlock();
23 }
24 }
25 public Data put(String key, Data value) {
26 writeLock.lock();
27 try {
28 return map.put(key, value);
29 }finally {
30 writeLock.unlock();
31 }
32 }
33 class Data {
34
35 }
36
37}
38
39
write.lock()保证在没有读锁的时候才进行写入操作,对数据同步做的更多一些,使用悲观读取,也就是说如果有写入锁时,坚决不允许有读锁还保持着,保证了在写的时候其他事情已经做完了。这样就会有一个问题,在读取情况较多而写入很少的时候,调用上面例子中的put方法就会遭遇饥饿(写锁一直想执行,但是读锁一直在,导致写锁永远无法执行,一直在等待)
StampedLock
stampedLock控制锁有三种模式,分别是写、读、乐观读。StampedLock由版本和模式两个模块组成,返回一个数字(票据stamp),用相应的锁状态来表示并控制相应的访问,数字0表示没有写锁被访问。读锁分为乐观锁和悲观锁,乐观锁是当读的情况很多,写的情况很少,可以认为读写同时发生的几率很小,因此不悲观的使用完全悲观的锁定,程序可以读取数据之后是否得到写入执行的变更,再采取后续的措施,这一个小小的改进,可以大幅度提高程序的吞吐量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 1@Slf4j
2public class LockExample4 {
3
4 // 请求总数
5 public static int clientTotal = 5000;
6
7 // 同时并发执行的线程数
8 public static int threadTotal = 200;
9
10 public static int count = 0;
11
12 private final static StampedLock lock = new StampedLock();
13
14 public static void main(String[] args) throws InterruptedException {
15 //线程池
16 ExecutorService executorService = Executors.newCachedThreadPool();
17 //定义信号量
18 final Semaphore semaphore = new Semaphore(threadTotal);
19 //定义计数器
20 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
21 for(int i = 0; i < clientTotal; i++) {
22 executorService.execute(() ->{
23 try {
24 semaphore.acquire();
25 add();
26 semaphore.release();
27 } catch (InterruptedException e) {
28
29 log.error("exception", e);
30 }
31 countDownLatch.countDown();
32
33 });
34 }
35 countDownLatch.await();
36 executorService.shutdown();
37 log.info("count:{}", count);
38 }
39
40 public static void add() {
41 long stamp = lock.writeLock();
42 try{
43 count++;
44 }finally {
45 lock.unlock(stamp);
46 }
47 }
48
49}
50
总结
synchronized:是在jvm层面实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常JVM也可以释放锁定,jvm会自动加锁与解锁。
ReentrantLock、ReentrantReadWriteLock、StampedLock:都是对象层面的锁定,要保证锁一定会被释放,一定要把unlock操作放在finally中才安全一些。StampedLock对吞度量有巨大的改进,特别是在读线程很多的场景下。
那么我们该如何选择使用哪个锁?
- 当只有少量的竞争者时,synchronized是一个很好的通用锁实现。
- 竞争者较多,而且竞争者的增长趋势可以预估,ReentrantLock是一个很好的通用锁实现。
**我们在用锁时不是看哪个锁高级用哪个。适合自己使用场景的才是最关键的。 **
需要注意的是synchronized不会引发死锁,jvm会自动解锁。而其他的Lock一旦使用不当会造成死锁,有可能会在一些情况下未执行unlock操作。