对[锁]的相关名词解释
公平锁/非公平锁
基本概念: 公平锁(Fair)
:是指按照线程申请的顺序获取锁。加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。 非公平锁(Nonfair)
:是指不是按照线程申请的顺序获取锁,有可能后申请的线程反而先获取到锁,假如先来的线程一直获取不到锁,会造成锁饥饿现象。即加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。
在Java的ReentrantLock中可以通过构造方法指定是否为公平锁,默认的lock()方法为非公平锁,非公平锁的优点在于吞吐量大。
synchronized关键字无法指定为公平锁,一直都是非公平锁。
看公平锁和非公平锁的实例:
公平锁:
package com.study;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
public class FairOrNonfair {
private ReentrantLock lock;
public FairOrNonfair(boolean isFair){
super();
lock = new ReentrantLock(isFair);
}
public void serviceMethod(){
try{
lock.lock();
System.out.println("ThreadName="
+Thread.currentThread().getName() + "获得锁定");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args){
final FairOrNonfair fairOrNonfair = new FairOrNonfair(true);
Thread thread = new Thread(){
@Override
public void run(){
System.out.println("我进来了"+Thread.currentThread().getName());
fairOrNonfair.serviceMethod();
}
};
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0 ; i < 5 ; i ++){
exec.execute(thread);
}
exec.shutdown();
}
}
结果:
我进来了pool-1-thread-2
我进来了pool-1-thread-5
我进来了pool-1-thread-4
我进来了pool-1-thread-3
我进来了pool-1-thread-1
ThreadName=pool-1-thread-2获得锁定
ThreadName=pool-1-thread-5获得锁定
ThreadName=pool-1-thread-4获得锁定
ThreadName=pool-1-thread-3获得锁定
ThreadName=pool-1-thread-1获得锁定
非公平锁:
package com.study;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
public class FairOrNonfair {
private ReentrantLock lock;
public FairOrNonfair(boolean isFair){
super();
lock = new ReentrantLock(isFair);
}
public void serviceMethod(){
try{
lock.lock();
System.out.println("ThreadName="
+Thread.currentThread().getName() + "获得锁定");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args){
final FairOrNonfair fairOrNonfair = new FairOrNonfair(false);
Thread thread = new Thread(){
@Override
public void run(){
System.out.println("我进来了"+Thread.currentThread().getName());
fairOrNonfair.serviceMethod();
}
};
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0 ; i < 5 ; i ++){
exec.execute(thread);
}
exec.shutdown();
}
}
我进来了pool-1-thread-2
我进来了pool-1-thread-5
我进来了pool-1-thread-4
我进来了pool-1-thread-3
我进来了pool-1-thread-1
ThreadName=pool-1-thread-2获得锁定
ThreadName=pool-1-thread-4获得锁定
ThreadName=pool-1-thread-5获得锁定
ThreadName=pool-1-thread-3获得锁定
ThreadName=pool-1-thread-1获得锁定
从结果可以看出 打印是有序的,排队在前面的线程直接获取锁。这就是公平锁。
而将非公平锁的结果也很显而易见,线程5先进来,但是线程4却先得到了锁。
看一下部分源码:
//定义成final型的成员变量,在构造方法中进行初始化
private final Sync sync;
//无参数默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//根据参数初始化为公平锁或者非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
线程在非公平锁模式下的吞吐量比公平锁模式下高,原因如下: 非公平锁模式下,当线程释放锁之后,快速的通过Fast通道再次获取锁,就算当前sync队列中有排队等待的线程也会被忽略。这种模式,可以保证进入和退出锁的吞吐量,但是sync队列中过早排队的线程会一直处于阻塞状态,造成“饥饿”场景。而公平性锁,就是在调用中顾及当前sync队列中的等待节点(废弃了Fast通道),也就是任意请求都需要按照sync队列中既有的顺序进行,先到先得。这样很好的确保了公平性,吞吐量就没有非公平的锁高了。
可重入锁
可重入锁,是指一个线程获取锁之后再尝试获取锁时会自动获取锁,可重入锁的优点是避免死锁。也就是说可重入锁指的是在一个线程中可以多次获取同一把锁。比如一个线程在执行一个带锁的方法,但是在该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。
另一个释义:广义上的可重入锁指的是可重复可递归调用的锁(因此也叫递归锁),指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。——来源
ReentrantLock和sychronized都是可重入锁。
下面看两个例子: 例1
package com.study.reentrantlock;
public class MyReentrantLock implements Runnable{
public synchronized void get() {
System.out.println(Thread.currentThread().getId());
set();
}
public synchronized void set() {
System.out.println(Thread.currentThread().getId());
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
MyReentrantLock myLock = new MyReentrantLock();
new Thread(myLock).start();
new Thread(myLock).start();
new Thread(myLock).start();
}
}
打印结果:
12
12
14
14
13
13
例2
package com.study.reentrantlock;
import sun.awt.windows.ThemeReader;
import java.util.concurrent.locks.ReentrantLock;
public class MyReentrantLock2 implements Runnable {
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
System.out.println(Thread.currentThread().getId());
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println(Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run(){
get();
}
public static void main(String[] args) {
MyReentrantLock2 mrl2 = new MyReentrantLock2();
new Thread(mrl2).start();
new Thread(mrl2).start();
new Thread(mrl2).start();
}
}
打印结果:
12
12
13
13
14
14
从其打印结果可见,结果都是正确的,即同一个线程id被连续输出了两次。
独享锁/共享锁
独享锁,是指锁一次只能被一个线程持有。
共享锁,是指锁一次可以被多个线程持有。从字面来看也即是允许多个线程共同访问资源。
ReentrantLock和synchronized都是独享锁,ReadWriteLock的读锁是共享锁,写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
AQS
抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
concurrent包的实现结构如上图所示,AQS、非阻塞数据结构和原子变量类等基础类都是基于volatile变量的读/写和CAS实现,而像Lock、同步器、阻塞队列、Executor和并发容器等高层类又是基于基础类实现
互斥锁/读写锁
与独享锁/共享锁的概念差不多,是独享锁/共享锁的具体实现。
互斥锁:在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作。加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。 如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被变成就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
ReentrantLock和synchronized都是互斥锁
ReadWriteLock是读写锁
读写锁特点: 1)多个读者可以同时进行读。 2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行) 3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
同时读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态。
互斥锁特点: 一次只能一个线程拥有互斥锁,其他线程只有等待。即某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个线程之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也是一种互斥)!
乐观锁/悲观锁
悲观锁,是指认为对于同一个数据的并发操作必然会发生修改,即使不会发生修改也这么认为,所以一定要加锁。 乐观锁,是指认为对于同一个数据的并发操作不一定会发生修改,在更新数据的时候,尝试去更新数据,如果失败就不断尝试。 悲观锁适用于写操作多的场景,乐观锁适用于读操作多的场景。
详见 https://homxuwang.github.io/2019/06/11/%E4%B9%90%E8%A7%82%E9%94%81%E4%B8%8E%E6%82%B2%E8%A7%82%E9%94%81/
自旋锁
自旋锁,是指尝试获取锁的线程不会阻塞,而是循环的方式不断尝试,这样的好处是减少线程的上下文切换带来的开锁,提高性能,缺点是循环会消耗CPU,也有可能导致死锁。
基本作用是用于线程(进程)之间的同步。与普通锁不同的是,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起(阻塞);试想下,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程B可以不放弃CPU时间片,而是在“原地”忙等,直到锁的持有者释放了该锁,这就是自旋锁的原理,可见自旋锁是一种非阻塞锁。
关于两个缺点:
- 过多消耗CPU:如果锁的当前持有长时间不释放该锁,那么等待着将长时间占据cpu时间片,导致CPU资源浪费,因此可以设置一定的时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片段阻塞。
- 死锁问题:如果有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁时,检测到锁已被占用(即被自己占用了),那么这时候线程会等待自己释放该锁,而不能继续执行,这样就发生了死锁问题。所以在递归程序中使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
实现原理:如果自旋锁被另外一个线程对象持有,那么当前获取锁的线程将陷入while循环等待,直到那个持有自旋锁的线程对象释放它所持有的自旋锁,那么那些想要获取该自旋锁的线程对象 将会有一个获得该自旋锁。等待的时候,并不释放cpu时间片,相比synchronized wait()操作,减小了释放,重新获取的消耗。 该自旋锁适用于,当前线程竞争不强烈的时候使用。
分段锁
分段锁,是一种锁的设计思路,它细化了锁的粒度,主要运用在ConcurrentHashMap中,实现高效的并发操作,当操作不需要更新整个数组时,就只锁数组中的一项就可以了。
说的简单一点就是:容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。
当然,任何技术必有其劣势,与独占锁相比,维护多个锁来实现独占访问将更加困难而且开销更加大。
参考
https://juejin.im/post/5cdac52ce51d456e55623bfc
https://blog.csdn.net/IsResultXaL/article/details/53334750
https://blog.csdn.net/jiang13479/article/details/80679794
http://ifeve.com/java_lock_see4/
https://zhuanlan.zhihu.com/p/54551800
https://blog.csdn.net/tanga842428/article/details/52765037
https://www.jianshu.com/p/87ac733fda80
https://blog.csdn.net/sunp823/article/details/49886051
https://blog.csdn.net/liushengbaoblog/article/details/39227863
https://blog.csdn.net/u010853261/article/details/54314486