-
V表示要更新的变量
-
E表示预期值
-
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下:
3、代码实例
以AtomicInteger的修改为例查看使用CAS时如何无锁并安全的修改某个值的:
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
先获得要修改的原值prev和要改成的新值next,当使用CAS替换新值不成功时,自旋,重新获得原值和新值再试一次直到成功为止。
这段代码中可以发现两个问题:
-
CAS操作必须是原子性的,即操作中间无法被打断。
-
获取原值时要保证这个原值对本线程可见。
CAS其实是调用了JNI,使用本地方法来保证原子性。
JNI(Java native interface),通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。
4、CAS带来的问题
(1)ABA问题
CAS操作的流程为:
-
读取原值。
-
通过原子操作比较和替换。
虽然比较和替换是原子性的,但是读取原值和比较替换这两步不是原子性的,期间原值可能被其它线程修改。
ABA问题有些时候对系统不会产生问题,但是有些时候却也是致命的。
ABA问题的解决方法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。
(2)自旋次数过多
CAS操作在不成功时会重新读取内存值并自旋尝试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。
(3)只能保证一个变量的原子性
当对一个变量操作时,CAS可以保证原子性,但同时操作多个变量时CAS就无能为力了。
可以封装成对象,再对对象进行CAS操作,或者直接加锁。
四、多线程锁的升级原理是什么?
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。
1、无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其它修改失败的线程会不断重试直到修改成功。
无锁总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,CAS是无锁技术的关键。
2、偏向锁
对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁。
如果线程处于活动状态,升级为轻量级锁的状态
3、轻量级锁
轻量级锁是指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从er提升性能。
当前只有
【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定次数时,轻量级锁边会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访时,轻量级锁也会升级为重量级锁。
注:自旋是什么?
自旋(spinlock)是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
4、重量级锁
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
5、锁状态对比
偏向锁 | 轻量级锁 | 重量级锁 | |
使用场景 | 只有一个线程进入同步块 | 虽然很多线程,但没有冲突,线程进入时间错开因而并未争抢锁 | 发生了锁争抢的情况,多条线程进入同步块争用锁 |
本质 | 取消同步操作 | CAS操作代替互斥同步 | 互斥同步 |
优点 | 不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId) | 不会阻塞 | 不会空耗CPU |
缺点 | 适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗 | 长时间获取不到锁空耗CPU | 阻塞,上下文切换,重量级操作,消耗操作系统资源 |
6、锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,别切不会被其它线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
五、Synchronized的特性
1、可重入性
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;
可重入的好处:
(1)可以避免死锁;
(2)可以让我们更好的封装代码;
synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
2、不可中断性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;
synchronized 属于不可被中断;
Lock lock方法是不可中断的;
Lock tryLock方法是可中断的;
六、Synchronized保证了原子性、可见性、有序性
1、Synchronized保证原子性
public class Test {
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0;i<1000;i++){
number++;
}
};
List list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("number = " + number);
}
}
Synchronized保证原子性
Runnable increment = () -> {
for (int i = 0;i<1000;i++){
synchronized (obj){
number++;
}
}
};
2、Synchronized保证可见性
public class Test1 {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag = false;
System.out.println(“线程修改了变量的值为false”);
}).start();
}
}
volatile即可解决这个问题!
public static volatile boolean flag = true;
Synchronized保证可见性
Synchronized保证可见性的原理,执行Synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。
3、Synchronized保证有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
七、synchronized的三种应用方式
1、修饰普通方法
作用于当前方法加锁,进入同步代码前要获得当前实例的锁。
(1)synchronized保证线程安全
(2)synchronized什么情况下无法保证线程安全
(3)这时synchronized修饰在静态方法上,就可以解决这个问题了。
start()、run()、join()的区别:
start():线程不会立即启动。相当于是在就绪队列里面;
run():启动线程;
join():主要作用是同步,它可以使得线程之间的并行执行变为串行执行。
join方法的作用:
在A线程中调用了B线程的join方法,表示只有当B线程执行完毕后,A线程才能继续执行。注意调用的join方法是没有传参的,join方法其实可以传递一个参数给它,如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
2、修饰静态方法
作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。
3、修饰同步代码块
指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁。
除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
从代码可以看出,将synchronized作用域一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其它线程正持有该对象锁,那么新到的线程必须等待,这样也就保证了每次只有一个线程执行i++操作。当然除了使用instance作为对象外,还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下: