目录
1.悲观锁和乐观锁
1.1 什么是悲观锁和乐观锁?
- 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,
- 悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。
- 这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。
(1)悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
- Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
- 悲观锁(如synchronized和Lock的实现机制)在高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。
(2)乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,实际上,当多个线程同时操作一个共享资源时,只有一个线程会成功,那么失败的线程呢?它们不会像悲观锁一样在操作系统中挂起,而仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。乐观锁适用于多读的应用类型,这样可以提高吞吐量,
- 像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
- 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
- 乐观锁是一种无锁的机制
1.2 两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,
- 像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
- 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
1.3 乐观锁的两种实现方式
乐观锁一般会使用版本号机制或CAS算法实现。
(1)版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:
假设我们要使用sql语句执行更新余额的任务

- 两个事务同时去读到余额都是100
- 然后事务1先进行了更新,余额变成了90,但此时并未提交
- 事务2又进行了更新,余额变成了200,覆盖了90
- 最终两个事务都进行了提交,可以发现以上过程是冲突的,导致最终的余额的错误
而解决上述问题就可以使用版本号机制

- 我们为余额加入版本号
- 当两个事务第一次读到余额的时候,版本号都为1
- 然后事务1更新的时候要判断版本号为1的情况下才去更新,更新后,将版本号加1
- 而第二个事务如果此时执行的话,会发现版本号不是1,所以事务2不会提交
- 事务2进行回滚
- 这样就避免了事务2的操作覆盖了事务1正在操作的数据
(2)CAS
- 即compare and swap(比较与交换),是一种有名的无锁算法。
- 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,
- 也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
关于CAS更详细的讲解,看我的另一篇博客:https://blog.csdn.net/qq_34805255/article/details/99232549
1.4 乐观锁的优缺点
ABA 问题是乐观锁一个常见的问题
(1)优点
- 乐观锁相比悲观锁来说,不存在锁的竞争,所以不会带来死锁、饥饿等活性故障问题,线程间的相互影响也远远比悲观锁要小。
- 更为重要的是,乐观锁没有因竞争造成的系统开销,所以在性能上也是更胜一筹。
(2)缺点
- 1.ABA问题
- 如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
- JDK 1.5 以后的 AtomicStampedReference类就提供了此种能力,其中的
compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志(这个预期标志就是相当于上述的版本号),如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 详细讲解,请看:https://blog.csdn.net/qq_34805255/article/details/99232549
-
2.循环时间长开销大
- 自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
- 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,
- 第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
- 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
-
3.只能保证一个共享变量的原子操作
- CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
- 但是从 JDK 1.5开始,提供了
AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
2.公平锁和非公平锁
2.1 是什么?


- 公平锁:指的是多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
- 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转(即后面申请锁的先得到锁)或者饥饿现象(有的线程长时间得不到锁)
ReetranLock调用默认构造方法是非公平锁,只有在构造方法中传入true才是公平锁
2.2 两者区别
- 公平锁,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
- 非公平锁,非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁的方式
注意:
- 非公平锁的优点在于吞吐量比公平锁大
- 对于synchronized而言,也是一种非公平锁
3.可重入锁(递归锁)
3.1 是什么?
- 可重入锁又名递归锁
- 指的是同一线程外层方法获得锁之后,内层方法仍然能获取该锁的代码,在同一个线程在外层方法获取锁的的时候,在进入内层方法会自动获取锁
- 也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
- ReetranLock、synchronized就是典型的可重入锁
其实关于加锁的内外层方法的调用有如下几种情况:
情况1:内外层方法都加锁
public synchronized void method01(){
//...
method02();
//...
}
public synchronized void method02(){
//...
}
method01在执行method02的时候自动获得它所得到的锁
情况2:内层方法加锁,外层方法不加锁,但是外层方法只调用了加锁了的方法,没有其他代码
public void method01(){
method02();
}
public synchronized void method02(){
//...
}
这种情况,method01即使不加锁,也因为内层它执行的代码加了锁,所以它在执行的时候还是保证了线程同步,相当于加了锁
情况3:内层方法加锁,外层方法不加锁,而且外层方法还执行了其他非同步代码
public void method01(){
//...
method02();
//...
}
public synchronized void method02(){
//...
}
这种情况下,method01线程不安全
情况4:内层方法不加锁,外层方法加锁
public synchronized void method01(){
//...
method02();
//...
}
public void method02(){
//...
method03();
//...
}
public void method03(){
//...
}
这种情况下,由于外层加了锁,所以在它当中执行的代码都是单线程的,所以在它调用method02执行的时候,也是单线程的,加了锁的,保证了线程安全,即便method02再去调用一个非同步方法method03,对method01方法的执行,仍然是线程安全的,因为synchronized对method01加了锁,它就保证了在执行method01方法中任何代码都是加锁的,单线程执行,必须执行完整个方法,才能切换CPU执行权
而可重入锁描述的就是上述的情况1
3.2 代码演示理解
示例代码1:
public class Demo {
/**
* 验证synchronized是重入锁
*
* 如果synchronized是非重入锁的话,当线程调用a()方法时,a方法就获取到this锁了,
* 而这时在a方法中调用b方法,b方法也要获取this锁,这时候因为锁被a方法占着,它就会等待,这时就会产生死锁
*
* 而实际是下述代码能正确执行,所以synchronized是可重入锁,内层代码块可以直接再次获得外层synchronized
* 的锁,即由于a方法获得到了this锁,b方法的调用作为它的内部代码块,可以再次获得该this锁
* 对于a方法中的同步代码块,同理,它也可以自动获取到this锁,即外层获取到,内层就可以使用该锁
*/
public synchronized void a(){
System.out.println("a");
b();
synchronized (this){
System.out.println("a方法中的this锁同步代码块");
}
}
public synchronized void b(){
System.out.println("b");
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
}
}

示例代码2:
public class Demo {
/**
* 当两个线程分别调用一个同步方法时,它们使用同一个锁对象,
* 会不会在一个线程的同步方法还没执行完就去执行另外一个方法?
*
*
* 不能
*/
public synchronized void a(){
System.out.println("a");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b(){
System.out.println("b");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
d.b();
}
}).start();
}
}

示例代码3:
public class Demo {
/**
* 当两个线程分别调用一个同步方法时,它们使用不同锁对象,
* 会不会在一个线程的同步方法还没执行完就去执行另外一个方法?
*
* 可以
*/
public synchronized void a(){
System.out.println("a");
System.out.println(System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b(){
System.out.println("b");
System.out.println(System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Demo d1 = new Demo();
Demo d2 = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d1.a();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
d2.b();
}
}).start();
}
}

总结:
- 当两个线程使用的是不同的对象锁的时候,线程无法被锁住,还是会并发执行
- 当两个线程使用的是同一个对象锁的时候,线程会被锁住,因为只有一个锁,就产生了互斥,一个线程得到锁,另一个只能等待
- 当同一个线程拿到锁时,锁可以被重入,即在拿到该锁的代码块,还可以获取并使用该锁
- 可重入锁最大的作用就是:防止死锁
示例4:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
/**
* 验证ReetranLock是可重入锁
*
* 正常输出,所以是可重入锁
*/
Lock lock = new ReentrantLock();
public void a(){
lock.lock(); //线程可以进入任何一个它已经拥有的锁
System.out.println("a");
b(); //所同步着的代码块
lock.unlock();
}
public void b(){
lock.lock();
System.out.println("b");
lock.unlock();
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
}
}

示例5:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
/**
* 验证ReetranLock是可重入锁
*
* 正常输出,所以是可重入锁
*/
Lock lock = new ReentrantLock();
public void a(){
lock.lock(); //线程可以进入任何一个它已经拥有的锁
lock.lock(); //所同步着的代码块
System.out.println("a");
lock.unlock();
lock.unlock();
}
public static void main(String[] args) {
Demo d = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
d.a();
}
}).start();
}
}

3.3 自己手写一个可重入锁
版本1:
public interface Lock {
//加锁
void lock();
//释放锁
void unlock();
}
public class MyLock implements Lock {
/**
* isLocked相当于一个锁标志,
* 为true的时候,表示有线程拥有锁,
* 为false的时候,表示没有线程拥有该锁
*/
private boolean isLocked = false;
/**
* 在lock和unlock加锁有两个作用:
* 1.为了保证这两个过程的原子性
* 2.wait和notify只能在synchronized方法或代码块中执行
*
*/
@Override
public synchronized void lock() {
/**
* 此处用while不用if的原因:
* 因为线程在wait完(即被notify唤醒以后)以后,会从wait()的下一句执行,
* if的话,整个判断直接结束,当前线程直接去修改了isLocked,
* 当有多个线程同时wait时,使用if的话,多个线程都会执行isLocked=true,这样就多个线程同时拿到了锁
* while的话,每个线程要再次判断是否锁已经被其他线程又再次持有
* 锁被释放后,第一个获得CPU执行权的线程,遇到isLocked=false,直接执行isLocked=true
*/
while(isLocked){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
}
@Override
public synchronized void unlock() {
isLocked = false;
this.notify();
}
}
public class Test {
private MyLock lock = new MyLock();
/**
* 如果加锁成功,这个方法中3条打印语句会一起输出
* 如果没有加锁,它们会交替执行
*/
public void print(){
lock.lock();
System.out.println("------------------------");
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" has acquired lock!");
System.out.println(threadName+" is running!");
System.out.println(threadName+" is realsing lock!");
lock.unlock();
}
public static void main(String[] args) {
Test t = new Test();
new Thread(new Runnable() {
@Override
public void run() {
t.print();
}
},"thread-1").start();
new Thread(new Runnable() {
@Override
public void run() {
t.print();
}
},"thread-2").start();
new Thread(new Runnable() {
@Override
public void run() {
t.print();
}
},"thread-3").start();
}
}

- 上述代码实现了显式锁的功能,但是它并没有实现可重入,以下测试未实现可重入
public class Test02 {
private MyLock lock = new MyLock();
public void a(){
lock.lock();
System.out.println("a方法开始执行了");
b();
lock.unlock();
}
public void b(){
lock.lock();
System.out.println("b方法开始执行了");
lock.unlock();
}
public static void main(String[] args) {
Test02 t2 = new Test02();
new Thread(new Runnable() {
@Override
public void run() {
t2.a();
}
}).start();
}
}

- 可以发现出现了一直的等待,当a方法执行调用lock.lock(),将isLocked设置为true,这样在执行到a方法调用b方法,b方法调用lock.lock()的时候,由于isLocked是true,所以执行wait()一直进行等待
下面对MyLock进行改造实现可重入
版本2:
public class MyLock implements Lock {
/**
* isLocked相当于一个锁标志,
* 为true的时候,表示有线程拥有锁,
* 为false的时候,表示没有线程拥有该锁
*/
private boolean isLocked = false;
//用于记录当前的线程
private Thread lockByThread = null;
//递归(重入)的锁的数量
private int lockCount = 0;
@Override
public synchronized void lock() {
//获取到当前的线程
Thread currentThread = Thread.currentThread();
//当锁已被其他线程占有并且当前线程不是占有锁的线程时,当前线程就需要等待
while(isLocked && currentThread != lockByThread){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lockByThread = Thread.currentThread();
isLocked = true;
lockCount++;
}
@Override
public synchronized void unlock() {
//在当前线程是占有锁的线程的时候,需要进行处理
if(lockByThread == Thread.currentThread()){
//将占有锁的数量-1,表示递归的锁的个数
lockCount --;
//只有当递归的锁的数量为0的时候,表示当前线程应该释放锁
if(lockCount == 0){
isLocked = false;
this.notify();
}
}
//在当前线程是占有锁的线程的时候,不需要进行任何处理
}
}
public class Test03 {
private MyLock lock = new MyLock();
public void a(){
lock.lock();
System.out.println("a方法开始执行了");
b();
lock.unlock();
}
public void b(){
lock.lock();
System.out.println("b方法开始执行了");
c();
lock.unlock();
}
public void c(){
lock.lock();
System.out.println("c方法开始执行了");
lock.unlock();
}
public static void main(String[] args) {
Test03 t2 = new Test03();
new Thread(new Runnable() {
@Override
public void run() {
t2.a();
}
}).start();
}
}

- 可重入即是当前线程拿到锁以后,再次获取锁,会自动拿到同样的锁
- 所以对MyLock新增加了两个字段:
- lockByThread用来保存当前线程
- lockCount用于记录当前线程拿到锁的次数,每当当前线程调用一次lock()就要对该数量加1,调用一次unlock()就对该数量减1,次数为0的时候才可以释放锁,因为此时才代表要配对释放当前线程的第一次lock()
4.自旋锁
- 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
- 生活案例解释:
- 张三要问老师问题,这时老师在打电话,阻塞就是他一直站在讲台上等待老师打完电话,解决自己的问题,而自旋就是下去做一些事情多次再来看老师是否打完电话,直到老师打完
自己手写一个自旋锁:
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
//定义为原子引用保证了操作的原子性
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void mylock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t lock");
long start = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"mylock start当前毫秒数:"+start);
while (!atomicReference.compareAndSet(null,thread)){
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"mylock end当前毫秒数:"+end);
System.out.println(Thread.currentThread().getName()+"自旋时间为:"+(end-start)+"ms");
}
public void myunlock(){
System.out.println(Thread.currentThread().getName()+"myunlock start当前毫秒数:"+System.currentTimeMillis());
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t unLock");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.mylock();
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myunlock();
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.mylock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myunlock();
},"t2").start();
}
}

- 自旋的好处是不用阻塞,但是它循环一直在执行,消耗着CPU,就比如上述案例中,t1线程执行后4s的睡眠期间,CPU会发生切换到t2线程去判断是否可以比较成功,而如果让线程在着循环判断,就会降低CPU的性能,它经常要切换给t2线程去判断

- 如果我将上述的t1的睡眠时间改为500*1000时,意味着这500s,循环一直在执行,CPU要调度它,这还只是一个线程,如果还有很多个线程的话,那意味着这些线程都要进行自旋,CPU除了要执行拿到“锁”线程,CPU还要调度它们,这样整个程序的性能就会因为这些无意义的空转而降低

5.读写锁
读锁,又叫共享锁
写锁,又叫独占锁
独占锁:指该锁一次只能被一个线程所持有,
- 对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程所持有
- 对ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁
- 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的
即多个线程同时读一个资源没有问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写
总结:
- 读-读能共存
- 读-写不能共存
- 写-写不能共存
代码演示:
- 未加任何锁
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//模拟Redis
public class MyCathe {
private volatile Map<String,Object> map = new HashMap<>();
//写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"\t正在写入:"+key);
//暂停一会线程
try {
TimeUnit.MICROSECONDS.sleep(300);
}catch (Exception e){
e.printStackTrace();
}
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t写入完成");
}
//读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"\t正在读取:");
//暂停一会线程
try {
TimeUnit.MICROSECONDS.sleep(300);
}catch (Exception e){
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t读取完成:"+result);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCathe myCathe = new MyCathe();
for (int i = 0; i < 5; i++) {
int tempInt = i;
new Thread(()->{
myCathe.put(tempInt+"",tempInt+"");
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
int tempInt = i;
new Thread(()->{
myCathe.get(tempInt+"");
},String.valueOf(i)).start();
}
}
}

- 可以看到出现了线程安全的问题
使用读写锁来改造程序
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//模拟Redis
public class MyCathe {
private volatile Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//写
public void put(String key,Object value){
reentrantReadWriteLock.writeLock().lock();
//暂停一会线程
try {
System.out.println(Thread.currentThread().getName()+"\t正在写入:"+key);
TimeUnit.MICROSECONDS.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t写入完成");
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
//读
public void get(String key){
reentrantReadWriteLock.readLock().lock();
//暂停一会线程
try {
System.out.println(Thread.currentThread().getName()+"\t正在读取:");
TimeUnit.MICROSECONDS.sleep(300);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t读取完成:"+result);
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantReadWriteLock.readLock().unlock();
}
}
}
测试程序同上

- 读写锁保证了线程安全,并且读写锁分离,读锁保证在读-读的时候并发执行,写锁保证进行写操作的时候是单线程执行,相比传统的synchronized和ReentrantLock提高了读-读的效率
本文详细介绍了Java多线程中的悲观锁与乐观锁的概念、使用场景、优缺点,以及两种锁的实现方式,包括版本号机制和CAS算法。此外,还讨论了公平锁和非公平锁的区别,解释了可重入锁的工作原理,并探讨了自旋锁和读写锁在并发控制中的应用。
582

被折叠的 条评论
为什么被折叠?



