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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。