前言

本文需要具备一定的多线程基础才能更好的理解。

学习java多线程时,最头疼的知识点之一就是java中的锁了,什么互斥锁、排它锁、自旋锁、死锁、活锁等等,细分的话可以罗列出20种左右的锁,光是看着这些名字就足以让人望而却步了,更别说一个个去理解它们的含义了。其实我要在这里告诉大家,我们看到的其实只是假象,其实根本没有这么多锁,或者这样说,这里边有很多锁其实就是一个东西,当我们从不同的侧重点去看的时候,它们就会衍生出不同的名字。本文就是着重将这些锁进行分门别类的总结,另外,本文不着重阐述锁的实现原理,大家有兴趣可以自行去查找,资料有很多,本文着重让大家理解这些锁的概。好了,废话不多说,进入正题。

一、由ReentrantLock和synchronized实现的一系列锁

jdk1.5的java.util.concurrent并发包中的Lock接口和1.5之前的synchronized或许是我们最常用的同步方式。这两种同步方式特别是Lock的ReentrantLock实现,经常拿来进行比较,其实他们有很多相似之处,其实它们在实现同步的思想上大致相同,只不过在一些细节的策略上(诸如抛出异常是否自动释放锁)有所不同。前边说过了,本文着重讲锁的实现思想和不同锁的概念与分类,不对实现原理的细节深究。

因此我在下面介绍第一类锁的时候经常讲他们放在一起来说。我们先来说一下Lock接口的实现之一ReentrantLock。当我们想要创建ReentrantLock实例的时候,jdk为我们提供两种重载的构造函数,如下:

     /**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
} /**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock

fair是什么意思?公平的意思,没错,这就是我们要说的第一种锁。

1.从其它等待中的线程是否按顺序获取锁的角度划分--公平锁与非公平锁

我先做个形象比喻,比如现在有一个餐厅,一次最多只允许一个持有钥匙的人进入用餐,那么其他没拿到钥匙的人就要在门口等着,等里面那个人吃完了,他出来他把钥匙扔地上,后边拿到钥匙的人才能进入餐厅用餐。

  • 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。所用公平锁就好像在餐厅的门口安装了一个排队的护栏,谁先来的谁就站的靠前,无法进行插队,当餐厅中的人用餐结束后会把钥匙交给排在最前边的那个人,以此类推。公平锁的好处是,可以保证每个排队的人都有饭吃,先到先吃后到后吃。但是弊端是,要额外安装排队装置。
  • 非公平锁:理解了公平锁,非公平锁就很好理解了,它无非就是不用排队,当餐厅里的人出来后将钥匙往地上一扔,谁抢到算谁的。但是这样就造成了一个问题,那些身强体壮的人可能总是会先抢到钥匙,而那些身体瘦小的人可能一直抢不到,这就有可能将一直抢不到钥匙,最后导致需要很长时间才能拿到钥匙甚至一直拿不到直至饿死。

公平锁与非公平所的总结:

(1) 公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
(2) 在java中,公平锁可以通过new ReentrantLock(true)来实现;非公平锁可以通过new ReentrantLock(false)或者默认构造函数new ReentrantLock()实现。
(3)synchronized是非公平锁,并且它无法实现公平锁。

2.从能否有多个线程持有同一把锁的角度划分--互斥锁

互斥锁的概念非常简单,也就是我们常说的同步,即一次最多只能有一个线程持有的锁,当一个线程持有该锁的时候其它线程无法进入上锁的区域。

在Java中synchronized就是互斥锁,从宏观概念来讲,互斥锁就是通过悲观锁的理念引出来的,而非互斥锁则是通过乐观锁的概念引申的。
即:

互斥锁--悲观锁

非互斥锁--乐观锁

3.从一个线程能否递归获取自己的锁的角度划分--重入锁(递归锁)

我们知道,一条线程若想进入一个被上锁的区域,首先要判断这个区域的锁是否已经被某条线程所持有。如果锁正在被持有那么线程将等待锁的释放,但是这就引发了一个问题,我们来看这样一段简单的代码:

 public class ReentrantDemo {
private Lock mLock; public ReentrantDemo(Lock mLock) {
this.mLock = mLock;
} public void outer() {
mLock.lock();
inner();
mLock.unlock();
} public void inner() {
mLock.lock();
// do something
mLock.unlock();
}
}

ReentrantDemo

当线程A调用outer()方法的时候,会进入使用传进来mlock实例来进行mlock.lock()加锁,此时outer()方法中的这片区域的锁mlock就被线程A持有了,当线程B想要调用outer()方法时会先判断,发现这个mlock这把锁被其它线程持有了,因此进入等待状态。我们现在不考虑线程B,单说线程A,线程A进入outer()方法后,它还要调用inner()方法,并且inner()方法中使用的也是mlock()这把锁,于是接下来有趣的事情就来了。按正常步骤来说,线程A先判断mlock这把锁是否已经被持有了,判断后发现这把锁确实被持有了,但是可笑的是,是A自己持有的。那你说A能否在加了mlock锁的outer()方法中调用加了mlock锁的inner方法呢?答案是如果我们使用的是可重入锁,那么递归调用自己持有的那把锁的时候,是允许进入的。

可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法A(方法A递归)。
    不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。

下面这段代码演示了不可重入锁:

 public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
} public synchronized void unlock(){
isLocked = false;
notify();
}
}

不可重入锁

可以看到,当isLocked被设置为true后,在线程调用unlock()解锁之前不管线程是否已经获得锁,都只能wait()。

4.从编译器优化的角度划分--锁消除和锁粗化

锁消除和锁粗化,是编译器在编译代码阶段,对一些没有必要的、不会引起安全问题的同步代码取消同步(锁消除)或者对那些多次执行同步的代码且它们可以可并到一次同步的代码(锁粗化)进行的优化手段,从而提高程序的执行效率。

锁消除

对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。

来看这样一个方法

     public String concatString(String s1, String s2, String s3)
{
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

concatString方法

     public synchronized StringBuffer append(StringBuffer sb) {
super.append(sb);
return this;
}

StringBuffer源码(append)

可见append的方法使用synchronized进行同步,我们知道对象的实例总是存在于堆中被多有线程共享,即使在局部方法中创建的实例依然存在于堆中,但是对该实例的引用是线程私有的,对其他线程不可见。即上边代码中虽然StringBuffer的实例是共享数据,但是对该实例的引用确实每条线程内部私有的。不同的线程引用的是堆中存在的不同的StringBuffer实例,它们互不影响互不可见。也就是说在concatString()方法中涉及了同步操作。但是可以观察到sb对象它的作用域被限制在方法的内部,也就是sb对象不会“逃逸”出去,其他线程无法访问。因此,虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了

锁粗化

原则上,我们在编写代码的时候,总是要将同步块的作用范围限制的尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁禁止,那等待的线程也能尽快拿到锁。大部分情况下,这些都是正确的。

但是,如果一系列的联系操作都是同一个对象反复加上和解锁,甚至加锁操作是出现在循环体中的,那么即使没有线程竞争,频繁地进行互斥同步操作也导致不必要的性能损耗。

举个案例,类似上面锁消除的concatString()方法。如果StringBuffer sb = new StringBuffer();定义在方法体之外,那么就会有线程竞争,但是每个append()操作都对同一个对象反复加锁解锁,那么虚拟机探测到有这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个append()操作之前和最后一个append()操作之后,这样的一个锁范围扩展的操作就称之为锁粗化。

5.在不同的位置使用synchronized--类锁和对象锁

这是最常见的锁了,synchronized作为锁来使用的时候,无非就只能出现在两个地方(其实还能修饰变量,但作用是保证可见性,这里讨论锁,故不阐述):代码块、方法(一般方法、静态方法)

由于可以使用不同的类型来作为锁,因此分成了类锁和对象锁。

  • 类锁:使用字节码文件(即.class)作为锁。如静态同步函数(使用本类的.class),同步代码块中使用.class。
  • 对象锁:使用对象作为锁。如同步函数(使用本类实例,即this),同步代码块中是用引用的对象。

下面代码涵盖了所有synchronized的使用方式:

 public class Demo {
public Object obj = new Object(); public static synchronized void method1() { //1.静态同步函数,使用本类字节码做类锁(即Demo.class)
} public void method2() {
synchronized (Demo.class) { //同步代码块,使用字节码做类锁
}
} public synchronized void method3() { //同步函数,使用本类对象实例即this做对象锁
} public void method4() {
synchronized (this) { //同步代码块,使用本类对象实例即this做对象锁
}
} public void method5() {
synchronized (obj) { //同步代码块,使用共享数据obj实例做对象锁。
}
}
}

二、从锁的设计理念来分类--悲观锁、乐观锁

如果将锁在宏观上进行大的分类,那么所只有两类,即悲观锁和乐观锁。

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁

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

java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

使用场景:就是利用各种锁。

乐观锁

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

但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

使用场景:是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

乐观锁的实现思想--CAS(Compare and Swap)无锁

CAS并不是一种实际的锁,它仅仅是实现乐观锁的一种思想,java中的乐观锁(如自旋锁)基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

关于CAS的原理,有兴趣可以参看JAVA CAS实现原理与使用。另外,在Java中,java.util.concurrent.atomic包下的原子类也都是基于CAS实现的。

前两节的结构图

三、数据库中常用到的锁--共享锁、排它锁

共享锁和排它锁多用于数据库中的事物操作,主要针对读和写的操作。而在Java中,对这组概念通过ReentrantReadWriteLock进行了实现,它的理念和数据库中共享锁与排它锁的理念几乎一致,

即一条线程进行读的时候,允许其他线程进入上锁的区域中进行读操作;当一条线程进行写操作的时候,不允许其他线程进入进行任何操作。即读+读可以存在,读+写、写+写均不允许存在

  • 共享锁:也称读锁或S锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
  • 排它锁:也称独占锁、写锁或X锁。如果事务T对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。

使用场景:

排它锁:Synchronized、ReentrantLock等
共享锁:ReadWriteLock其读锁是共享锁(其写锁是排它锁)等

四、对锁的不同效率进行的分类--偏向锁、轻量级锁和重量级锁

由于不同的锁的实现原理不同,故它们的效率肯定也会不尽相同,那么我们在不同的应用场景下究竟该选择何种锁呢?

基于这个问题,锁被分成了偏向锁、轻量级锁和重量级锁以便应对不同的应用场景。

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现

具体请参考:java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁

五、由于并发问题产生的锁--死锁、活锁

死锁

1.什么是死锁

所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。下面我通过一些实例来说明死锁现象。

先看生活中的一个实例,2个人一起吃饭但是只有一双筷子,2人轮流吃(同时拥有2只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2个人都同时占用一个资源,等待另一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也无法继续吃饭。

在计算机系统中也存在类似的情况。例如,某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

2.死锁形成的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生:

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。

活锁

活锁和死锁在表现上是一样的两个线程都没有任何进展,但是区别在于:死锁,两个线程都处于阻塞状态,说白了就是它不会再做任何动作,我们通过查看线程状态是可以分辨出来的。而活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功。举个生动的例子的话,两个人都没有停下来等对方让路,而是都有很有礼貌的给对方让路,但是两个人都在不断朝路的同一个方向移动,这样只是在做无用功,还是不能让对方通过。

六、分段锁

分段锁也是一种锁思想,对数据分段加锁已提高并发效率,比如jdk8之前的ConcurrentHashMap 的分段锁(Segment),jdk8后采用CAS+synchronized。通过hashCode计算到索引后对数据分段加锁
详见:ConcurrentHashMap的锁分段技术

转自:

https://blog.csdn.net/renwei289443/article/details/79540809

https://blog.csdn.net/zqz_zqz/article/details/70233767

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

https://blog.csdn.net/yansong_8686/article/details/50664351

Java中的各种锁--分类总结的更多相关文章

  1. Java中常见的锁分类以及对应特点

    对于 Java 锁的分类没有严格意义的规则,我们常说的分类一般都是依据锁的特性.锁的设计.锁的状态等进行归纳整理的,所以常见的分类如下: 公平锁和非公平锁:公平锁是多线程按照锁申请的顺序获取锁,非公平 ...

  2. Java中15种锁的分类综合总结

    本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...

  3. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...

  4. Java中15种锁的介绍

    作者:搜云库技术团队 原文:https://segmentfault.com/a/1190000017766364 1. Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观 ...

  5. 分门别类总结Java中的各种锁,让你彻底记住

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

  6. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  7. Java 中的各种锁和 CAS + 面试题

    Java 中的各种锁和 CAS + 面试题 如果说快速理解多线程有什么捷径的话,那本文介绍的各种锁无疑是其中之一,它不但为我们开发多线程程序提供理论支持,还是面试中经常被问到的核心面试题之一.因此下面 ...

  8. 一文带你看懂Java中的Lock锁底层AQS到底是如何实现的

    前言 相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题.那你是不是很好奇,这些Lock锁api是如何实现的呢?本 ...

  9. 在 Java 中高效使用锁的技巧--转载

    竞争锁是造成多线程应用程序性能瓶颈的主要原因 区分竞争锁和非竞争锁对性能的影响非常重要.如果一个锁自始至终只被一个线程使用,那么 JVM 有能力优化它带来的绝大部分损耗.如果一个锁被多个线程使用过,但 ...

随机推荐

  1. Python学习笔记——基本数据结构

    列表list List是python的一个内置动态数组对象,它的基本使用方式如下: shoplist = ['apple', 'mango', 'carrot', 'banana'] print 'I ...

  2. C语言的运算符、位操作

    + - * / (加 减 乘 除) > >= < <= (大于 大于等于 小于 小于等于) == != (测试等于 测试不等于) && || ! (逻辑与 逻辑 ...

  3. dragon-book-exercise-answers

    https://github.com/fool2fish/dragon-book-exercise-answers

  4. linux下内存释放问题

    参考: http://blog.sina.com.cn/s/blog_3cba7ec10100gk4k.html http://blog.sina.com.cn/s/blog_3cba7ec10100 ...

  5. python 时间 相关

    http://www.jb51.net/article/47957.htm 不管何时何地,只要我们编程时遇到了跟时间有关的问题,都要想到 datetime 和 time 标准库模块,今天我们就用它内部 ...

  6. 阿里蚂蚁的前端ant-design

    蚂蚁国内镜像: http://ant-design.gitee.io/components/date-picker-cn/ 阿里的设计平台:https://design.alipay.com/deve ...

  7. python软件工程知识

    软件工程知识 3.1 程序设计过程中,常用伪代码来"思考"一个程序,在将伪代码程序转换成python程序. 3.2 所有python程序都可以给予6类控制结构来创建(顺序,if, ...

  8. ECSHOP筛选属性修改title标题

    发现百度蜘蛛爬行网站时会爬行属性链接,而且会进行收录.可是ecshop系统制作的网站,在分类页点击属性筛选出产品时,网页title不会改变.这样就会造成大量title一样的页面,不利于优化.为此,在网 ...

  9. 2017.5.1 java动态代理总结

    参考来自:http://www.cnblogs.com/jqyp/archive/2010/08/20/1805041.html 1.代理模式 代理类和委托类有相同接口. 代理类负责为委托类:预处理消 ...

  10. 2017.4.28 SSM框架搭建与配置

    1.项目结构 2.配置文件 对配置文件进行总结: pom.xml web.xml -> 配置web相关 -> 读取application*.xml 5 -> 读取logback.xm ...