Java并发-锁相关基础

锁的概念

锁是在资源中的,是要访问资源(如对象实例,Class类实例,属性变量,代码块等)的一部分,线程只有获取该资源的锁才有权访问这个资源,这个就是锁机制。

互斥锁:解决原子性问题

并发编程有3个源头性问题:缓存导致的可见性问题,编译优化导致的有序性问题,以及线程切换导致的原子性问题。解决可见性问题和有序性问题的方法是按需禁用缓存和编译优化,Java的内存模型就是一种按需禁用缓存和编译优化的规则,它规定了 JVM 如何提供相关的方法,这些已经在Java内存模型与Hppens-Before规则进行了描述。

在这里插入图片描述
总结:锁主要是用来解决原子性问题, 是要保证中间状态对外不见。

java中锁的分类

java中的锁机制
参考URL: https://blog.csdn.net/cuichunchi/article/details/88532582
【躲不过的Java “锁事”】一文扫除对Java各种锁的困扰!
参考URL: https://blog.csdn.net/YangCheney/article/details/106679763

在java中的锁分为以下(其实就是按照锁的特性和设计来划分):

1、公平锁/非公平锁
2、可重入锁
3、独享锁/共享锁
4、互斥锁/读写锁
5、乐观锁/悲观锁
6、分段锁
7、偏向锁/轻量级锁/重量级锁
8、自旋锁(java.util.concurrent包下的几乎都是利用锁)

从底层角度看常见的锁也就两种:Synchronized和Lock接口以及ReadWriteLock接口(读写锁)

在这里插入图片描述

乐观锁 VS 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

根据从上面的概念描述我们可以发现:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

自旋锁 VS 适应性自旋锁

自旋锁与适应性自旋锁
参考URL: https://blog.csdn.net/yjn1995/article/details/98884799

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作源码中的do…while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直到成功。

自旋锁本身是有缺点的,它不能代替阻塞。**自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。**所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

  • 自适应自旋锁
    自旋锁在Java1.6中改为默认开启,并引入了自适应的自旋锁。
    自适应意味着自旋的次数不在固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态共同决定。

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

这四种锁是指锁的状态,专门针对synchronized的。

在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。 这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

整体的锁状态升级流程如下:
在这里插入图片描述综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

公平锁 VS 非公平锁

**公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。**公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

什么时候我们会用到可重入锁呢?

demo

public class Demo1 {
    public synchronized void functionA(){
        System.out.println("iAmFunctionA");
        functionB();
    }
    public synchronized void functionB(){
        System.out.println("iAmFunctionB");
    }
}

functionA()和functionB()都是同步方法,当线程进入funcitonA()会获得该类的对象锁,这个锁"new Demo1()",在functionA()对方法functionB()做了调用,但是functionB()也是同步的,因此该线程需要再次获得该对象锁(new Demo1())。其他线程是无法获该对象锁的。

这就是可重入锁。

总结:可重入锁的作用就是为了避免死锁,java中synchronized和ReentrantLock都是可重入锁。

重入锁的实现原理

通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁

独享锁 VS 共享锁

ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

锁和受保护资源的关系

互斥锁,解决原子性问题
参考URL: jianshu.com/p/71c7b9b7d3a3

锁可以保护一个或者多个资源。我们可以用一个范围较大的锁,比如说 X.class 保护多个相关的资源;也可以用不同的锁对被保护资源进行精细化管理,这就叫细粒度锁。

  1. 一把锁保护一个资源
    class SafeCalc {
      long value = 0L;
      long get() {
        return value;
      }
      synchronized void addOne() {
        value += 1;
      }
    }
    

这是一段想解决 count += 1 问题的代码,我们对 addOne() 使用 synchronized 加上互斥锁,可以保证其原子性。**根据 Happens-before 管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁,也可以保证其可见性。**即使是 1000 个线程同时执行 addOne() 也可以保证 value 增加 1000。

所谓“对一个锁解锁 Happens-Before 后续对这个锁的加锁”,指的是前一个线程的解锁操作对后一个线程的加锁操作可见,综合 Happens-Before 的传递性原则,我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。

但我们无法保证 get() 的可见性,管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而 get() 方法并没有加锁操作,所以可见性没法保证。所以我们给 get() 也加上锁:

class SafeCalc {
  long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized void addOne() {
    value += 1;
  }
}

此时 get() 和 addOne() 都持有 this 这把锁,此时 get() 和 addOne() 是互斥的,并且保证了可见性。
在这里插入图片描述2. 两把锁保护不同资源的问题

在这里插入图片描述
此时 get() 和 addOne() 分别持有不同的锁,get() 和 addOne() 不互斥,也就不能保证可见性,就会导致并发问题。

  1. 一把锁保护多个资源

现在要写一个银行转账的方法,用户 A 给用户 B 转账,将其转换成代码:

class Account {
  private int balance;
  // 转账
  synchronized void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

用户 A 给用户 B 转账 100,要保证 A 的余额减少 100,B 的余额增加 100。由于转账操作可以是并发的,所以要保证转账操作没有并发问题。比如说 A 的余额只有 100,两个线程分别执行 A 给 B 转账 100,A 给 C 转账 100,这两个线程有可能同时从内存中读取到 A 的余额是 100,这就产生了并发问题。

解决这个问题的第一反应,就是给 transfer(Account target, int amt) 加上 synchronized。这样做真的对么?transfer() 此时有两个需要被保护的资源 target.balance 和 this.balance 即别人钱和自己的钱,但我们使用的锁是 this 锁,如下图所示:
在这里插入图片描述自己的锁 this 能保护自己的 this.balance 但是无法保护别人的 target.balance,就像我的锁不能即保护我家的东西,又保护你家的东西一样。

所以我们需要一把锁的范围更大一点,让它能够覆盖到所有的被保护资源,比如说传入同一个对象作为锁:

class Account {
  private Object lock;
  private int balance;
  private Account();
  // 创建Account时传入同一个lock对象
  public Account(Object lock) {
    this.lock = lock;
  } 
  // 转账
  void transfer(Account target, int amt){
    // 此处检查所有对象共享的锁
    synchronized(lock) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}

或者使用类锁 Accout.class,由于 Accoutn.class 是在 Java 虚拟机加载 Account 类时创建的,所以 Account.class 是所有 Account 对象共享且唯一的一把锁。

class Account {
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    synchronized(Account.class) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  } 
}

Accout.class 就可以同时保护两个不同对象的临界区资源:

在这里插入图片描述

4种常用Java线程锁的特点,性能比较、使用场景

4种常用Java线程锁的特点,性能比较、使用场景
参考URL: https://youzhixueyuan.com/4-kinds-of-java-thread-locks.html

参考

互斥锁,解决原子性问题
参考URL: jianshu.com/p/71c7b9b7d3a3

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西京刀客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值