Java常见的锁,java几种锁

  Java常见的锁,java几种锁

  

  如何解决写爬虫IP受阻的问题?立即使用。

  锁的分类介绍

  乐观锁与悲观锁

  锁的宏观分类是乐观锁和悲观锁。乐观锁悲观锁不是指任何特定的锁(Java中特定锁的实现名称叫做乐观锁或悲观锁),而是指并发情况下的两种不同的策略。

  乐观锁很乐观。他每次去拿数据,都以为别人不会修改。所以不会被锁住。但是,如果要更新数据,那么在更新之前,会检查其他人是否在读取和更新之间修改了数据。如果已经修改,重新读取,再次尝试更新,重复以上步骤,直到更新成功(当然也允许更新失败的线程放弃更新操作)。

  悲观锁很悲观。每次去拿数据,都觉得别人会修改。所以每次我得到数据,我就锁定它。

  这样其他人在获取数据的时候就会被阻塞,直到悲观锁被释放,想要获取数据的线程才会获得锁,然后获取数据。

  悲观锁阻塞事务,乐观锁回滚并重试。各有利弊,没有好坏之分。他们只适应不同的场景。例如,乐观锁定适用于写入很少的情况,即冲突确实很少,这样可以节省锁定的开销,增加系统的整体吞吐量。但如果冲突频繁,上层应用会反复尝试,会降低性能,所以这种场景悲观锁比较合适。

  总结:乐观锁定适合写冲突少的场景;但是写的比较多,冲突比较多的场景适合使用悲观锁。

  乐观锁的基础 --- CAS

  在乐观锁的实现中,我们必须了解一个概念:CAS。

  CAS是什么?比较和交换,即比较和替换,或比较和设置。

  比较:读取一个值A,在更新为B之前检查原始值是否为A(没有被其他线程修改过,这里忽略ABA问题)。

  替换:如果是,更新A到B,结束。如果没有,就不会更新。

  以上两步都是原子操作,可以理解为瞬间完成,在CPU看来是一步到位的操作。

  使用CAS,您可以实现乐观锁定:

  公共类OptimisticLockSample{

  公共无效测试(){

  int data=123//共享数据

  //更新数据的线程将执行以下操作

  for(;) {

  int oldData=data

  int new data=do something(old data);

  //下面是模拟的CAS更新操作,尝试更新数据的值

  if (data==oldData) { //比较

  data=newData//交换

  打破;//完成

  }否则{

  //什么都不敢,再试一次。

  }

  }

  }

  /**

  *

  *显然,test()中的代码根本不是原子的,只是展示了下载CAS的过程。

  *因为真正的CAS利用CPU指令。

  *

  * */

  }在Java中,CAS也是通过native方法实现的。

  公共最终类不安全{

  .

  public final native boolean compareAndSwapObject(对象var1,long var2,对象var4,对象

  var 5);

  public final native boolean compareAndSwapInt(Object var 1,long var2,int var4,int var 5);

  public final native boolean compareAndSwapLong(Object var 1,long var2,long var4,long var 6);

  .

  }上面写了一个简单直观的乐观锁定的实现(确切的说是乐观锁定过程),它允许多个线程同时读取(因为根本没有锁定操作)。如果数据被更新,

  并且只有一个线程可以成功更新数据,导致其他线程需要回滚并重试。利用CAS的CPU指令,从硬件层面保证原子性,从而达到类似锁的效果。

  从乐观锁定的整个过程可以看出,没有锁定和解锁操作,所以乐观锁定策略也叫无锁编程。换句话说,乐观锁定实际上不是一个“锁”,

  这只是一个循环重试的CAS算法。

  相关:《java开发教程》

  自旋锁

  同步和锁定接口

  在Java中实现锁定有两种方法:一种是使用synchronized关键字,另一种是使用lock接口的实现类。

  我在一篇文章里看到一个很好的对比,很生动。synchronized这个关键词就像一个自动变速器,可以满足所有的驾驶需求。

  但是如果你想做更高级的操作,比如玩漂移或者各种高级骚操作,那么你就需要手动挡,这是锁接口的实现类。

  而synchronized在各版本Java的各种优化之后变得非常高效。只是没有锁接口的实现类用起来方便。

  synchronized 锁升级过程就是其优化的核心:偏向锁 - 轻量级锁 - 重量级锁

  类别测试{

  私有静态最终对象Object=new Object();

  公共无效测试(){

  同步(对象){

  //做点什么

  }

  }

  }使用synchronized关键字锁一个代码块时,起初锁对象(即上面代码中的对象)不是重量级锁,而是锁偏置。

  偏向锁的字面意思是偏向第一个获取它的线程的锁。线程执行完同步代码块后,不会主动释放偏置锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否是自己(持有锁的线程ID存储在对象头中),如果是,则正常执行。既然之前没有发布,这里就不需要重新锁定了。如果一个线程从头到尾都在使用锁,很明显几乎没有开销,而且性能非常高,有利于锁。

  一旦第二个线程加入锁竞争,偏向锁就被转换成轻量级锁(自旋锁)。锁争用:如果多个线程轮流获取一个锁,但每次都顺利获取,则不存在锁争用。只有当一个线程获取锁时,发现锁已经被占用,需要等待其释放,这就意味着存在锁竞争。

  在轻量级锁状态下继续锁竞争,没有抢到锁的线程执行spin操作,也就是在循环中不断判断是否可以获取锁。获取锁的操作是通过CAS操作修改对象头中的锁标志位。首先,比较当前锁定标志位是否处于释放状态,如果是,则将其设置为锁定状态。比较和设置是原子操作,由JVM级别保证。即使当前线程持有锁,该线程也会将当前锁的持有者信息更改为其自身。

  如果获取锁的线程需要很长时间的操作,比如会进行复杂的计算和大量数据的网络传输;那么等待锁的其他线程将进入一个长自旋操作,这是非常消耗资源的。其实这个时间就相当于只有一个线程在有效工作,而其他线程什么都做不了,在白白浪费CPU。这种现象叫做忙等。(忙着等待).因此,如果多个线程使用独占锁,但是没有锁竞争,或者有轻微的锁竞争,那么synchronized就是轻量级锁,允许短时繁忙。这是一个选对的思路,短期的忙碌等等。以换取线程在用户模式和内核模式之间切换的开销。

  显然,忙碌等待是有限度的(JVM有一个计数器记录旋转次数,默认允许循环10次,可以通过虚拟机参数改变)。如果锁争用情况严重,

  达到某个最大旋转次数的线程会将轻量级锁升级为重量级锁(锁标志位仍然通过CAS进行修改,但持有锁的线程ID不会被修改)。当后续线程试图获取锁,发现被占用的锁是一个重量级锁,就直接挂起自己(而不是像上面说的忙,也就是不打转),等待释放锁的线程醒来。在JDK1.6之前,synchronized直接添加了重量级锁。现在很明显,经过一系列优化,性能明显提升。

  在JVM中,同步锁只能按照偏置锁、轻量级锁、重量级锁的顺序逐步升级(还有一个过程叫锁膨胀),不允许降级。

  可重入锁(递归锁)

  重入锁字面意思是‘可以重入的锁’,也就是允许同一个线程多次获取同一个锁。比如递归函数中有锁操作,那么递归函数中的锁是否会阻塞自身?

  如果不是,那么这个锁就叫做可重入锁(可重入锁也因此被称为递归锁)。

  Java中用Reentrant命名的锁是可重入锁,JDK提供的所有现成的锁实现类,包括synchronized关键字锁,都是可重入的。

  如果你真的需要一个不可重入的锁,那么你需要自己实现它。去网上搜一下。有很多,自己实现很简单。

  如果不是可重入锁,会造成递归函数死锁,所以Java里所有的锁基本都是可重入锁,不可重入锁的意义不是很大。暂时还没想到什么场景会用到;

  注:想到需要非重锁场景的朋友可以留言一起讨论。

  下图显示了锁的相关实现类:

  公平锁和非公平锁

  如果多个线程申请一个公平锁,那么当获得锁的线程释放锁时,先申请的线程先获得锁是公平的。如果是不公平锁,后面申请的线程可能会先获取锁,是随机获取还是其他方式获取,取决于实现算法。

  对于ReentrantLock类,构造函数可以指定锁是否公平,默认为不公平。因为在大多数情况下,不公平锁的吞吐量要大于公平锁的吞吐量,如果没有特殊要求,优先选择不公平锁。

  至于同步锁,只能是不公平锁,没有办法让它成为公平锁。这也是ReentrantLock优于synchronized lock的一个优点,synchronized lock更加灵活。

  以下是ReentrantLock构造函数代码:

  /**

  *使用创建{@code ReentrantLock}的实例

  *考虑到公平政策。

  *

  * @param fair {@code true}此锁是否应使用公平排序策略

  */

  public ReentrantLock(布尔公平){

  同步=公平?new FairSync():新的NonfairSync();

  }ReentrantLock实现了FairSync和NonfairSync两个内部类,实现公平锁和不公平锁。

  可中断锁

  它的字面意思是“可以响应中断的锁”。

  首先,我们需要明白的是什么是打断。Java没有提供任何可以直接中断线程的方法,只有中断机制。那么中断机制是什么呢?

  线程A向线程B发送请求‘请停止运行’,这是调用Thread.interrupt()的方法(当然线程B本身也可以向自身发送中断请求,

  即thread.currentthread()。interrupt()),但线程B不会立即停止运行,而是会选择在合适的时间点以自己的方式响应中断,或者直接忽略这个中断。也就是说,Java的中断不能直接终止线程,而是将状态设置为响应中断的状态,需要中断的线程自行决定如何处理。就像读书,老师让学生晚上复习功课,但是学生是否复习,怎么复习,完全取决于学生自己。

  回到锁分析,如果线程A持有锁,线程B等待锁被获取。因为线程A持有锁的时间太长了,所以线程B不想再等了。我们可以中断线程b。

  或者自己中断另一个线程中的B。这是中间的锁。

  在Java中,synchronized Lock是一个可中断锁,Lock的实现类都是可中断锁。可以看出,JDK自己实现的锁更加灵活,这也是为什么有了同步锁之后,还会有那么多锁的实现类。

  锁接口的相关定义:

  公共接口锁{

  void lock();

  void lockInterruptibly()引发InterruptedException

  布尔tryLock();

  布尔tryLock(long time,TimeUnit单位)抛出InterruptedException

  void unlock();

  condition new condition();

  }其中lockInterruptibly将获取一个可中断锁。

  共享锁

  从字面上看,多个线程可以共享一个锁。通常,在读取数据时使用共享锁。例如,我们可以允许10个线程同时读取一个共享数据。这时,我们可以用10个凭证设置一个共享锁。

  在Java中,也有特定的共享锁实现类,比如Semaphore。

  互斥锁

  它的字面意思是线程之间的互斥锁,这意味着一个锁只能由一个线程拥有。

  在Java中,ReentrantLock和synchronized lock是互斥锁。

  读写锁

  读写锁实际上是一对锁,一个读锁(共享锁)和一个写锁(互斥锁,排他锁)。

  在Java中,ReadWriteLock接口只指定了两个方法,一个是返回读锁,一个是返回写锁。

  公共接口读写锁{

  /**

  *返回用于读取的锁。

  *

  * @返回用于读取的锁

  */

  lock read lock();

  /**

  *返回用于写入的锁。

  *

  * @返回用于写入的锁

  */

  lock writeLock();

  }文章前面讲过【乐观锁定策略】(#乐观锁定的基础- CAS)。所有线程都可以随时读取,只在写入前判断值是否被更改。

  读写锁实际上做同样的事情,但是策略略有不同。在许多情况下,线程知道在读取数据后是否要更改数据。那为什么不锁的时候说清楚?

  这个呢?如果我读取值来更新它(SQL for update就是这个意思),那么写锁是在锁被锁定的时候直接添加的。当我持有写锁时,其他线程。

  无论是读还是写,都需要等待;如果读取数据只是为了前端显示,那么锁定时会显式添加一个读锁。如果其他线程也想添加一个读锁,它们可以直接获得它而无需等待(读锁计数器加1)。

  虽然读写锁感觉有点像乐观锁,但读写锁是一种悲观锁策略。因为读写锁在更新前并不判断值是否被修改过,而是决定锁之前应该使用读锁还是写锁。乐观锁定指的是无锁编程。

  JDK内部提供了一个读写锁接口的独特实现类,即ReentrantReadWriteLock。从名字可以看出,锁提供了读写锁,也是重入锁。

  总结

  Java中使用的各种锁基本都是悲观锁,那么Java中有乐观锁吗?结果是肯定的,即java.util.concurrent.atomic下面的原子类都是通过乐观锁实现的。如下所示:

  public final int getAndAddInt(Object var 1,long var2,int var4) {

  int var5

  做{

  var5=this.getIntVolatile(var1,var 2);

  } while(!this.compareAndSwapInt(var1,var2,var5,var 5 var 4));

  返回var5

  }通过上面的源代码可以发现,CAS一直循环下去,直到成功。

  参数介绍

  -XX:-UseBiasedLocking=false关闭偏置锁。

  JDK1.6

  -XX:使用Spinning打开旋转锁。

  -XX:PreBlockSpin=10设置旋转次数。

  JDK1.7之后,这个参数被移除,由JVM控制。本文转自:Java各种锁的https://blog.tommyyang.cn/2019/08/13/-Introduction史上最全-2019/以上是检查Java各种锁的详细内容。请多关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: