Home Tags Posts tagged with "锁"

0 13

查阅资料时常常看到线程安全和线程不安全的字样,特此记录二者的详细定义,同时讨论线程安全我们有必要了解什么是原子操作,什么是原子类以及同步,非同步和阻塞非阻塞的区别

 

 

1.同步与异步

编辑

同步和异步 [1]  关注的是消息通信机制 (synchronous communication/ asynchronous communication)。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

2.阻塞与非阻塞

编辑

阻塞和非阻塞 [2]  关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

3.结合两概念分析

编辑

  • 进程通信 [3]  层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。
  • 发送方阻塞/非阻塞(同步/异步)和接收方的阻塞/非阻塞(同步/异步) 是互不影响的。
  • 在 IO 系统调用层面( IO system call )层面, 非阻塞 IO 系统调用  异步 IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )
  • 非阻塞系统调用(non-blocking I/O system call 与 asynchronous I/O system call) 的存在可以用来实现线程级别的 I/O 并发, 与通过多进程实现的 I/O 并发相比可以减少内存消耗以及进程切换的开销。

 

原子操作:

原子操作(atomic operation)是不需要synchronized”,这是多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切 [1]  换到另一个线程)

定义

如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
将整个操作视作一个整体是原子性的核心特征。

特性

原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是” 原子操作”,因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl (递减指令)为例,这是一个典型的”读-改-写”过程,涉及两次内存访问。设想在不同CPU运行的两个进程都在递减某个计数值,可能发生的情况是:
⒈ CPU A(CPU A上所运行的进程,以下同)从内存单元把当前计数值⑵装载进它的寄存器中;
⒉ CPU B从内存单元把当前计数值⑵装载进它的寄存器中。
⒊ CPU A在它的寄存器中将计数值递减为1;
⒋ CPU B在它的寄存器中将计数值递减为1;
⒌ CPU A把修改后的计数值⑴写回内存单元。
⒍ CPU B把修改后的计数值⑴写回内存单元。
我们看到,内存里的计数值应该是0,然而它却是1。如果该计数值是一个共享资源的引用计数,每个进程都在递减后把该值与0进行比较,从而确定是否需要释放该共享资源。这时,两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它–两个进程都推断出:计数值是1,共享资源仍然在被使用。

线程安全:

 

一、线程安全

是指你的代码所在的进程中有多个线程同时运行,而这些线程可能会同时运行这段代码,如果每次运行的代码结

果和单线程运行的结果是一样的,而且其他变量的值和预期的也是一样的,那么就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,

也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行

写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

二、非线程安全

是不提供代码数据访问保护,可能出现多个线程先后访问更改数据造成所得的数据是脏数据。*(脏数据是表示一个数据已经

被修改,但是还没有保存或进一步的处理。例如你操作数据库修改某一字段内容,在你修改了但还没commit时,另一线程在读取这

数据,他读取的就是你修改前的数据,但事实上你已经修改了,这就是脏数据了。)*在多个线程同时访问同一个对象时会发生数据

错误 不完整等情况时 那就是线程不安全,不会发生上续错误时是线程安全的,一般采用锁机制。

三、线程安全出现的根本原因

存在两个或者两个以上的线程对象共享同一个资源;

多线程操作共享资源代码有多个语句。

存在竞争的线程不安全,不存在竞争的线程就是安全的

 

 

存在线程安全问题必须满足三个条件:

1.有共享变量

2.处在多线程环境下

3.共享变量有修改操作。

所谓解决线程安全问题无非就是将操作原子化,正如楼上各位所说的加sychronized,或者加lock什么的,只要将操作原子化就能避免线程安全的问题。但加锁会有性能问题。

所以在多线程情况下,优先考虑能否不用共享,优先使用局部变量代替共享的全局变量。

只能用共享变量的时候优先使用原子类,诸如AtomicInteger尔尔。

没有原子类,可以自己创造自己的原子类。

如以上方法都不能奏效,再考虑使用sychro,lock之类尔尔。

别一上来就sychro,不仅low,而且显的不够专业

 

四、怎么实现线程安全

1、synchronized
同步代码块、同步方法

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完

整性,使用方法一般是加在方法上。

public class ThreadDemo {

int count = 0; // 记录方法的命中次数

public synchronized void threadMethod(int j) {

count++ ;

int i = 1;

j = j + i;
}
}

这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

2.Lock
Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块

五、分别在什么情况下使用

线程安全是多线程操作同一个对象不会有问题;

非线程安全是指多线程操作同一个对象可能会出现问题;

线程安全会导致性能的降低。

如果是多个线程操作同一个对象,那么使用线程安全的Vector;否则,就使用效率更高的ArrayList。

PS(非线程安全并非不安全)

非线程安全并不是多线程环境下就不能使用。

 

六、集合线程安全实例

1.Vector
Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它给几乎所有的public方法都加上了synchronized关键字。由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用

2.HashTable
HashTable和HashMap类似,不同点是HashTable是线程安全的,它给几乎所有public方法都加上了synchronized关键字,还有一个不同点是HashTable的K,V都不能是null,但HashMap可以,它现在也因为性能原因被弃用了

3.Collections包装方法
Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());
Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

4.java.util.concurrent包中的集合
4.1ConcurrentHashMap
ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率

4.2CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

5.除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到

 

转载整理自:

————————————————
版权声明:本文为CSDN博主「写代码的彭于晏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lixiaobuaa/article/details/79689338

————————————————
版权声明:本文为CSDN博主「CodeHunter_qcy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43390235/article/details/90897699

作者:花溪的小石头
链接:https://www.zhihu.com/question/49855966/answer/298161490
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 13

一个锁可以同时是悲观锁可重入锁公平锁可中断锁等等,就像一个人可以是男人、医生、健身爱好者、并不矛盾;

 

synchronized与Lock

Java加锁的方式有两种:

1.使用synchronized关键字

2.用Lock接口的实现类

synchronized关键字是自动挡,可以满足一切日常驾驶需求;但是如果我们想玩飘逸或其他骚操作(刀片超车),就需要手动挡—各种Lock的实现类

所以如果我们只是想要简单加个锁,对性能也没什么特别的要求,用synchronized关键字就够了;自Java 5之后,才在java.util.concurrent.locks包下有了另外一种方式来实现锁,那就是Lock;也就是说,synchronized是Java语言内置的关键字,而Lock是一个接口,这个接口的实现类在代码层面实现了锁的功能;

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

ReentrantLockReadLockWriteLockLock接口最重要的三个实现类对应“可重入锁”“读锁”“写锁”,后面会讲其用途;

ReadWriteLock其实是一个工厂接口,而ReentranReadWriteLock是ReadWriterLock的实现类,它包含两个静态内部类,即ReadLock和WriteLock,这两个静态内部类又实现了Lock接口;

 

各种锁分类的概念、synchronized、各种Lock实现类之间的区别与联系;

 

一、悲观锁和乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略

悲观锁(Pessimistic Lock),很悲观,往坏的方向想,即每次去拿数据的时候都认为别人会修改,因此在每次拿数据的时候都会上锁,这样别人想拿数据就会被挡住,直到悲观锁被释放;

乐观锁(Optimistic Lock),很乐观,往好的方向想,每次去拿数据的时候都认为别人不会修改,所以不会上锁!不会上锁!不会上锁!但是如果想要更新数据,会在更新前检查在读取至更新这段时间别人有没有修改过这个数据,如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(也允许更新失败的线程放弃操作)

乐观锁阻塞事务,悲观锁回滚重试,它们各有优缺点,不能固执的认为一种一定好于另一种;乐观锁适用于写操作比较少的情况下即冲突真的很少发生的时候,这样可以省去锁的开销,加到了系统的整个吞吐量;但如果经常发生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下使用悲观锁就比较合适;

 

二、乐观锁的基础—CAS

说到乐观锁,就必须提到一个概念:CAS

什么是CAS?Compare-and-Swap,即比较并替换,也叫Compare-and-Set的,比较并设置。

1.比较:读取了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动);

2.设置:如果是,将A更新为B,结束。如果不是,则什么也不做;

上面的两步操作具有原子性那个,理解为瞬间完成,在CPU看来就是一步操作;

通过CAS实现一个乐观锁;

data = 123; // 共享数据

/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
    oldValue = data; // 保存原始数据
    newValue = doSomething(oldValue); 

    // 下面的部分为CAS操作,尝试更新data的值
    if (data == oldValue) { // 比较
        data = newValue; // 设置
        flag = false; // 结束
    } else {
	// 啥也不干,循环重试
    }
}
/* 
   很明显,这样的代码根本不是原子性的,
   因为真正的CAS利用了CPU指令,
   这里只是为了展示执行流程,本意是一样的。
*/

这是一个简单直观的乐观锁实现,它允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。

Java中真正的CAS操作调用的native方法

因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

三、自旋锁

有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。

刚刚的乐观锁就有类似的无限循环操作,那么它是自旋锁吗?

感谢评论区养猫的虾的指正。

不是。尽管自旋与 while(true) 的操作是一样的,但还是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋

然而在JDK中并没有自旋锁(SpinLock)这个类,那什么才是自旋锁呢?读完下个小节就知道了。

四、synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利没有发生阻塞,那么就不存在锁竞争只有当某线程尝试获取锁的时候,发现该锁已经被占用只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。(补充:可以降级,但是条件比较苛刻)

感谢评论区酷帅俊靓美的问题:
偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
还有人对此有疑惑,我之前确实没有描述清楚,但如果要展开讲,涉及到太多新概念,可以新开一篇了。更何况有些太底层的东西,我没读过源码,没有自信说自己一定是对的。其实在升级为轻量级锁之前,虚拟机会让线程A尽快在安全点挂起,然后在它的栈中“伪造”一些信息,让线程A在被唤醒之后,认为自己一直持有的是轻量级锁。如果线程A之前正在同步代码块中,那么线程B自旋等待即可。如果线程A之前不在同步代码块中,它会在被唤醒后检查到这一情况并立即释放锁,让线程B可以拿到。这部分内容我之前也没有深入研究过,如果有说的不对的,请多多指教啊!

五、可重入锁(递归锁)

可重入锁的字面意思是“可以重新进入的锁,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)

Java里只要以Reetrant开头命名的锁都是可重入锁,而且JDK提供的所有线程的Lock实现类,包括synochronized关键字锁都是可重入的如果你需要不可重入锁只能自己去实现。

六、公平锁、非公平锁

如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。

对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。

ReentrantLock构造器可以指定为公平或非公平

对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁

七、可中断锁

可中断锁,字面意思是“可以响应中断的锁”。

这里的关键是理解什么是中断Java没有提供任何直接中断某线程的方法只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。[2]

回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁

在Java中,synchronized就是不可中断锁Lock的实现类都是可中断锁可以简单看下Lock接口。

/* Lock接口 */
public interface Lock {

    void lock(); // 拿不到锁就一直等,拿到马上返回。

    void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

    boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

    void unlock();

    Condition newCondition();
}

八、读写锁、共享锁、互斥锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。

看下Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。

记得之前的乐观锁策略吗?所有线程随时都可以读,仅在写之前判断值有没有被更改。

读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。

虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略因为读写锁并没有更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁乐观锁特指无锁编程,如果仍有疑惑可以再回到第一、二小节,看一下什么是“乐观锁”。

JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock提供了一些便于外界监控其内部工作状态的方法,这里就不一一展开。

九、回到悲观锁和乐观锁

这篇文章经历过一次修改,我之前认为偏向锁和轻量级锁是乐观锁,重量级锁和Lock实现类为悲观锁,网上很多资料对这些概念的表述也很模糊,各执一词。

先抛出我的结论:

我们在Java里使用的各种锁几乎全都是悲观锁synchronized偏向锁、轻量级锁重量级锁全是悲观锁JDK提供的Lock实现类全是悲观锁其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。

那JDK并发包里到底有没有乐观锁呢?

有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。

原子类AtomicInteger的自增方法为乐观锁策略

为什么网上有些资料认为偏向锁、轻量级锁是乐观锁?理由是它们底层用到了CAS?或者是把“乐观/悲观”与“轻量/重量”搞混了?其实,线程在抢占这些锁的时候,确实是循环+CAS的操作,感觉好像是乐观锁。但问题的关键是,我们说一个锁是悲观锁还是乐观锁,总是应该站在应用层看它们是如何锁住应用数据的,而不是站在底层看抢占锁的过程。如果一个线程尝试获取锁时发现已经被占用它是否继续读取数据,等后续要更新时再决定要不要重试?对于偏向锁轻量级锁来说,显然答案是否定的。无论是挂起还是忙等,对应用数据的读取操作都被“挡住”了。从这个角度看,它们确实是悲观锁。

退一步讲,也没有必要在这些术语上狠钻牛角尖,最重要的是理解它们的运行机制。想写得尽量简单一些,却发现洋洋洒洒近万字,只讲了个皮毛。深知自己水平有限,不敢保证完全正确,只能说路漫漫其修远兮,望指正。

参考

  1. ^这里存在一个问题,就是一个值从A变为B,又从B变回了A。这种情况下,CAS可能会认为值没有发生过变化,但实际上是有变化的。对此,并发包下有AtomicStampedReference提供根据版本号判断的实现。ABA问题。
  2. ^Java中断机制: https://www.cnblogs.com/jiangzhaowei/p/7209949.html