Java中的锁分类的介绍 —— 公平锁/非公平锁、可重入锁、自旋锁、读写锁、分段锁、偏向锁等

偏执的太偏执、 2022-04-25 01:08 376阅读 0赞

文章目录

    • 1、乐观锁/悲观锁
    • 2、公平锁/非公平锁
    • 3、独占锁/共享锁
      • 1)独占锁/共享锁的定义
      • 2)区别
    • 4、可重入锁(递归锁)
      • 1)定义:
      • 2)举例说明
      • 3)常用的可重入锁
      • 4)可重入锁的原理:
    • 5、自旋锁
      • 1)自旋锁的定义
      • 2)优缺点
      • 3)参数配置
      • 4)面试题
    • 6、互斥锁/读写锁
    • 7、分段锁
    • 8、偏向锁/轻量级锁/重量级锁
    • 推荐文章:

学习 java 过程中,遇到很多锁,下面总结对每个锁的含义。

1、乐观锁/悲观锁

乐观锁和悲观锁是在数据库中引入的名词,但是在并发包锁里面也引入了类似的思想。乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合 ”写操作“ 非常多的场景,乐观锁适合 ”读操作“ 非常多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

2、公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Java ReentrantLock ,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized ,也是一种非公平锁。由于其并不像 ReentrantLock 是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

3、独占锁/共享锁

1)独占锁/共享锁的定义

  • 独占锁 : 该锁一次只能被一个线程所持有。比如 ReentrantLockSynchronized
  • 共享锁 : 该锁可被多个线程所持有。

独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

2)区别

ReentrantReadWriteLock ,读锁是共享锁,写锁是独占锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

读 – 读, 能共存。
读 – 写,不能共存
写 – 读,不能共存
写 – 写,不能共存。

所以,共享锁 和 独占锁 可以是同一把锁。

4、可重入锁(递归锁)

1)定义:

可重入锁 又名 递归锁 ,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

2)举例说明

  1. public sychrnozied void test() {
  2. xxxxxx;
  3. test2();
  4. }
  5. public sychronized void test2() {
  6. yyyyy;
  7. }

上面代码中,test()test2() 都是同步方法,执行时都需要获得当前对象作为监视器的对象锁,但 test() 中又调用 test2() 的同步方法。

因为 sychronized 是可重入锁,所以线程在 test() 中调用 test2() 时,并不需要再次获得当前对象的锁,可以直接进入 test2() 进行操作。

假如 锁不是可重入锁,那么线程在 test() 中调用 test2 时,必须会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有,不可能再次获得,所以会造成死锁。

如果锁是不具有可重入性,那么线程在调用同步方法、含有锁的方法地位时就会产生 死锁

3)常用的可重入锁

ReentrantLocksynchronized 都是可重入锁。可重入锁的一个好处是可一定程度避免死锁。

注:
ReentrantLock :可拆分成 ReentrantLock,意思是 重新进入锁。

4)可重入锁的原理:

synchronized 内部锁是 可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个钱程获取了该锁时,计数器的值会变成1 ,这时他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。

但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1。当计数器值为 0 时,锁里面的线程标示被重置为null , 这时候被阻塞的线程会被唤醒来竞争获取该锁。

5、自旋锁

1)自旋锁的定义

为了实现某项资源的保护,要求在任何时刻,对该项资源的使用 只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

对于 互斥锁第6节 ) , 如果资源已经被占用, 申请者 只能进入睡眠状态。
但是,自旋锁 的申请者 不会睡眠,申请者 会一直循环,查看该自旋锁的保持者是否已经释放了锁。自旋 由此得名。

自旋 的反义词 是 阻塞 。所以,除自旋锁外,其他锁的申请者 没有获得锁时,会进主 阻塞(即睡眠)。

简而言之,自旋锁 就是而是采用循环的方式,不停地去尝试获取锁。

在这里插入图片描述

2)优缺点

好处是 减少线程上下文切换的消耗,
缺点是 循环会消耗 CPU。

3)参数配置

自旋锁,默认尝试次数是10 ,可以使用 -XX:PreBlockSpinsh 参数设置该值。

很有可能在后面几次尝试中其他线程己经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。

由此看来,自旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白白浪费了。

4)面试题

自已实现一个自旋锁,可以参考 并发编程网 - ifeve.com

  1. public class SpinLock {
  2. private AtomicReference<Thread> sign =new AtomicReference<>();
  3. public void lock(){
  4. Thread current = Thread.currentThread();
  5. while(!sign .compareAndSet(null, current)){ // 一直在循环
  6. }
  7. }
  8. public void unlock (){
  9. Thread current = Thread.currentThread();
  10. sign.compareAndSet(current, null);
  11. }
  12. }

6、互斥锁/读写锁

上面讲的 独占锁/共享锁 就是一种广义的说法,互斥锁/读写锁 就是具体的实现。

互斥锁 的具体实现就是 ReentrantLocksynchronized

读写锁 的具体实现就是 ReentrantReadWriteLock (读写锁)

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.concurrent.locks.ReentrantReadWriteLock;
  4. public class MyTask {
  5. private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  6. private volatile Map<String,Object> cache=new HashMap();
  7. public void write(String key,Object value) {
  8. try {
  9. lock.writeLock().lock();
  10. System.out.println(Thread.currentThread().getName() + " 正在写入:"+key);
  11. Thread.sleep(300);
  12. cache.put(key, value);
  13. System.out.println(Thread.currentThread().getName() + " 写入完成");
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. } finally {
  17. lock.writeLock().unlock();
  18. }
  19. }
  20. public void read(String key) {
  21. try {
  22. lock.readLock().lock();
  23. System.out.println(Thread.currentThread().getName() + " 正在读取");
  24. Thread.sleep(200);
  25. Object result=cache.get(key);
  26. System.out.println(Thread.currentThread().getName() + " 读取完成:"+result);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. } finally {
  30. lock.readLock().unlock();
  31. }
  32. }
  33. }

测试类

  1. public class Main {
  2. public static void main(String[] args) {
  3. final MyTask myTask = new MyTask();
  4. for (int i = 1; i <=5; i++) {
  5. final int tmp=i;
  6. new Thread(() -> {
  7. myTask.write(tmp+"",tmp+"");
  8. }, "t"+i).start();
  9. }
  10. try {
  11. Thread.sleep(1000);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. for (int i = 1; i <=5; i++) {
  16. final int tmp=i;
  17. new Thread(() -> {
  18. myTask.read(tmp+"");
  19. }, "t"+i).start();
  20. }
  21. }
  22. }

7、分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为 Segment ,它即类似于HashMap(JDK7与JDK8中 HashMap 的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock( Segment 继承 了 ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

8、偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁 是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁 是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁 是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

转载:https://www.cnblogs.com/qifengshi/p/6831055.html

推荐文章:

JVM 锁分类

JVM中锁优化,偏向锁、自旋锁、锁消除、锁膨胀

Java中的偏向锁,轻量级锁, 重量级锁解析

Java中的锁分类与使用

发表评论

表情:
评论列表 (有 0 条评论,376人围观)

还没有评论,来说两句吧...

相关阅读