JUC - 多线程之悲观锁乐观锁,读写锁(共享锁独享锁),公平非公平锁,可重入锁,自旋锁,死锁
Posted MinggeQingchun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC - 多线程之悲观锁乐观锁,读写锁(共享锁独享锁),公平非公平锁,可重入锁,自旋锁,死锁相关的知识,希望对你有一定的参考价值。
Java中主要有如下锁
一、悲观锁、乐观锁
悲观锁:当前线程去操作数据的时候,总是认为别的线程会去修改数据,所以每次操作数据的时候都会上锁,别的线程去操作数据的时候就会阻塞,比如synchronized;
乐观锁:当前线程每次去操作数据的时候都认为别人不会修改,更新的时候会判断别人是否会去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,例如cas是乐观锁,但是严格来说并不是锁,通过原子性来保证数据的同步,例如数据库的乐观锁,通过版本控制来实现,cas不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
总结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁高
二、共享锁、独享锁(读写锁、互斥锁)
读写锁是一种技术: 通过ReentrantReadWriteLock类来实现
为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 JVM自己控制的
读锁(共享锁): 允许多个线程获取读锁,同时访问同一个资源
写锁(独享所): 只允许一个线程获取写锁,不允许同时访问同一个资源
共享锁:也叫读锁,可以查看数据,但是不能修改和删除的一种数据锁,加锁后其他的用户可以并发读取,但不能修改、增加、删除数据,该锁可被多个线程持有,用于资源数据共享
独享锁:也叫排它锁、写锁、独占锁、独享锁,该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁都会被阻塞,直到当前线程解锁
如:线程A对data加上排它锁后,则其他线程不能再对data加任何类型的锁,获得互斥锁的线程既能读数据又能修改数据
Java中的读写锁:ReadWriteLock,ReadWriteLock实现类ReentrantReadWriteLock
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock;其实现类 ReentrantReadWriteLock
* 读-读 可以共存!
* 读-写 不能共存!
* 写-写 不能共存!
*/
public class ReadWriteLockTest
public static void main(String[] args)
MyCache myCache = new MyCache();
// 写入
for (int i = 1; i <= 5 ; i++)
final int temp = i;
new Thread(()->
myCache.put(temp + "",temp + "");
,String.valueOf(i)).start();
// 读取
for (int i = 1; i <= 5 ; i++)
final int temp = i;
new Thread(()->
myCache.get(temp + "");
,String.valueOf(i)).start();
class MyCache
private volatile Map<String,Object> map = new HashMap<>();
// 读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock lock = new ReentrantLock();
// 存、写数据,只希望同时只有一个线程写
public void put(String key,Object value)
readWriteLock.writeLock().lock();
try
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入OK");
catch (Exception e)
e.printStackTrace();
finally
readWriteLock.writeLock().unlock();
// 取,读数据,所有线程都可以同时读取
public void get(String key)
readWriteLock.readLock().lock();
try
System.out.println(Thread.currentThread().getName() + "读取" + key);
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
catch (Exception e)
e.printStackTrace();
finally
readWriteLock.readLock().unlock();
输出
1写入1
1写入OK
2写入2
2写入OK
3写入3
3写入OK
4写入4
4写入OK
5写入5
5写入OK
1读取1
2读取2
2读取OK
3读取3
1读取OK
3读取OK
5读取5
5读取OK
4读取4
4读取OK
1-5写入数据时不会被其他线程抢占执行顺序;但是读取数据时就会插队
三、公平非公平锁
公平锁:有多个线程按照申请锁的顺序来获取锁,就是说,如果一个线程组里面,能够保证每个线程都能拿到锁,例如:ReentrantLock(使用的同步队列FIFO)
非公平锁:获取锁的方式是随机的,保证不了每个线程都能拿到锁,会存在有的线程饿死,一直拿不到锁,例如:synchronized,ReentrantLock
总结:非公平锁性能高于公平锁,更能重复利用CPU的时间
四、可重入锁(递归锁)
可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不会产生死锁
不可重入锁:在当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
1、来自知乎的解释:可重入锁指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个他已经拥有锁的所有同步代码块
2、Coffey强的解释:可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁
可重入锁的意义之一在于防止死锁
1、synchronized
/**
* 可重入锁(递归锁)
* 在外层使用锁之后,在内层仍然可以使用,并且不会产生死锁(前提是同一把锁,如同一个类、同一个实例、同一个代码块)
* 1、来自知乎的解释:可重入锁指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个他已经拥有锁的所有同步代码块。
* 2、Coffey强的解释:可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
* synchronized 和 ReentrantLock 都是可重入锁
* 可重入锁的意义之一在于防止死锁
* 不可重入锁:在当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
*/
public class SynchronizedTest
public static void main(String[] args)
Object obj = new Object();
new Thread(() ->
// 第一次加锁
synchronized (obj)
System.out.println(Thread.currentThread().getName() + "线程执行第一层");
// 第二次加锁,此时obj对象处于锁定状态,但是当前线程仍然可以进入,避免死锁
synchronized (obj)
// 抛异常
int a = 10/0;
System.out.println(Thread.currentThread().getName() + "线程执行第二层");
,"t1").start();
new Thread(() ->
// 第一次加锁
synchronized (obj)
System.out.println(Thread.currentThread().getName() + "线程执行第一层");
// 第二次加锁,此时obj对象处于锁定状态,但是当前线程仍然可以进入,避免死锁
synchronized (obj)
System.out.println(Thread.currentThread().getName() + "线程执行第二层");
,"t2").start();
t1线程第二层有异常,使用synchronized关键字,发生异常,会自动释放锁,t2线程正常输出
2、 ReentrantLock
/**
* 可重入锁(递归锁)
* synchronized 和 ReentrantLock 都是可重入锁
* 可重入锁的意义之一在于防止死锁
*/
public class ReentrantLockTest
public static void main(String[] args)
// 非公平锁
Lock lock = new ReentrantLock(false);
new Thread(() ->
// 第一次加锁
lock.lock();
try
System.out.println(Thread.currentThread().getName() + "线程执行第一层");
// 第二次加锁,此时obj对象处于锁定状态,但是当前线程仍然可以进入,避免死锁
lock.lock();
try
// 抛异常
int a = 10/0;
System.out.println(Thread.currentThread().getName() + "线程执行第二层");
finally
lock.unlock();
finally // Lock是显式锁,必须手动关闭锁(忘记关闭锁会导致 死锁)
lock.unlock();
,"t1").start();
new Thread(() ->
// 第一次加锁
lock.lock();
try
System.out.println(Thread.currentThread().getName() + "线程执行第一层");
// 第二次加锁,此时obj对象处于锁定状态,但是当前线程仍然可以进入,避免死锁
lock.lock();
try
System.out.println(Thread.currentThread().getName() + "线程执行第二层");
finally
lock.unlock();
finally // Lock是显式锁,必须手动关闭锁(忘记关闭锁会导致 死锁)
lock.unlock();
,"t2").start();
t1线程第二层有异常,我们的释放锁写在finally里边,不影响t2线程正常输出
注:
1、Lock是显式锁,必须手动关闭锁(手动开启和关闭锁,忘记关闭锁会导致 死锁) synchronized是隐式锁,会自动释放锁,出了作用域自动释放
2、Lock一定要手动关闭释放,且必须在finally从句中释放
ReentrantLock类中源码注释
五、自旋锁
自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁
总结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
六、重量级锁、轻量级锁
重量级锁是一种称谓: synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。
操作系统实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。为了优化synchonized,引入了轻量级锁,偏向锁。
Java中的重量级锁: synchronized
轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。
优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
七、偏向锁
偏向锁是JDK6时加入的一种锁优化机制: 在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。
优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
八、分段锁
分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。
线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全
九、互斥锁、同步锁
互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问。
读-读互斥
读-写互斥
写-读互斥
写-写互斥
Java中的同步锁: synchronized
同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
Java中的同步锁: synchronized
十、死锁
死锁是一种现象:如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。
Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁
十一、锁粗化
锁粗化是一种优化技术: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗
十二、锁消除
锁消除是一种优化技术: 就是把锁干掉。当Java虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除
判断共享数据不会被线程竞争?
利用逃逸分析技术:分析对象的作用域,如果对象在A方法中定义后,被作为参数传递到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。
在堆上的某个数据不会逃逸出去被其他线程访问到,就可以把它当作栈上数据对待,认为它是线程私有的,同步加锁就不需要了
十三、synchronized
synchronized是Java中的关键字:用来修饰方法、对象实例
synchronized属于独占锁、悲观锁、可重入锁、非公平锁
1、作用于实例方法时,锁住的是对象的实例(this);
2、当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁, 会锁所有调用该方法的线程;
3、synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中
每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
十四、Lock和synchronized的区别
Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁
1、Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
2、Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
3、synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
4、Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
5、通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
6、Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
synchronized的优势:
只需要基础的同步功能时,用synchronized
Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的
十五、ReentrantLock 和synchronized的区别
ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁
相同点:
1、主要解决共享变量如何安全访问的问题
2、都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁
3、保证了线程安全的两大特性:可见性、原子性
不同点:
1、ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
2、ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
3、ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
4、ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
5、ReentrantLock 通过 Condition 可以绑定多个条件
可参考
Java中的锁有哪些?_zhangjia_happy的博客-CSDN博客_java锁的类型有哪些
以上是关于JUC - 多线程之悲观锁乐观锁,读写锁(共享锁独享锁),公平非公平锁,可重入锁,自旋锁,死锁的主要内容,如果未能解决你的问题,请参考以下文章
一篇文章读懂java中所有的锁(包括乐观锁/互斥锁/读写锁/分段锁)