前言

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

学习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. apache 单独生成模块

    apache 单独生成模块 一般这种模块都是后期自己去生成的,比如一般在安装apache时都会--enable-so  ,允许动态加载模块. 这个模块你可以这样去生成. 1.下载一个与当前使用的apa ...

  2. uprobes issue with oracle 12c

    https://mahmoudhatem.wordpress.com/2017/03/21/uprobes-issue-with-oracle-12c/

  3. ASIHTTPRequest框架使用总结系列之阿堂教程4(下载数据)

    从本篇开始,阿堂准备进一步介绍ASIHTTPRequest框架下载数据和上传数据的实际应用.        为了实现多线程并发请求网络能力,ASIHTTPRequest被设计成 NSOperation ...

  4. 避免"松鼠症"

    转载: http://www.cnblogs.com/freeflying/p/7725385.html ted演讲: http://www.bilibili.com/video/av294900/

  5. 利用【深度网络】高效提取feature

    extracting features from a learned model, and add some new features yourself.

  6. mac 上python编译报错No module named MySQLdb

    mac 上python编译报错No module named MySQLdb You installed python You did brew install mysql You did expor ...

  7. [转载]Elasticsearch Java API总汇

    from: http://blog.csdn.net/changong28/article/details/38445805#comments 3.1 集群的连接 3.1.1 作为Elasticsea ...

  8. urllib urllib2

    #-*-coding:utf-8-*- import urllib import urllib2 import cookielib ##urllib url="http://www.qq.c ...

  9. C2:抽象工厂 Abstract Factory

    提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类. 应用场景: 一系列相互依赖的对象有不同的具体实现.提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合 UM ...

  10. mobx 小结

    1.@observable 是一种让数据的变化可以被观察的方法 //@observable data 注册一个数据,这个数据将会成为一个可mobx监测的数据 2.decorator 修饰器只能修饰 类 ...