并发
Ⅰ 进程与线程
1、程序、进程与线程比较
- 程序是含有指令和数据的文件,被存储在磁盘或其他数据存储设备中,也就是说程序是静态的代码。
- 进程是程序的一次执行过程,是系统运行的基本单位(资源分配的最小单位),因此进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。每个进程还占有某些系统资源如cpu时间、内存空间、文件、输入输出设备的使用权等。换句话说,当程序在执行时,将会被操作系统载入内存中。线程与进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一段程序内几乎同时执行一个以上的程序段。
- 线程与进程相似,但线程是一个比进程更小的执行单位(最小调度的单位)。一个进程在其执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源。所以系统在产生一个线程,或者是在各个线程之间切换时,负担要比进程小的多,也正因为如此,线程也被称为轻量级进程。
2、并行与并发
- 并发(concurrent)是同一时间应对多件事情的能力(本质还是串行)
- 并行(parallel)是同一时间动手做多件事情的能力
Ⅱ Java线程
1、创建线程的方式
① 直接使用Thread(继承Thread类)
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
② 使用Runnable 配合Thread(实现Runnable接口)
- 继承Thread把线程和任务合并在了一起,实现Callable把线程和任务分开
- 用Runnable更容易与线程池等高级API配合
- 实现Runnable让任务类脱离了Thread单继承体系,更灵活
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable,"t2" );
// 启动线程
t.start();
//或者使用lambda表达式
Thread t = new Thread(()->{
//要执行的任务···
},"t2");
t.start();
③实现Callable接口(处理有返回结果的情况)
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
④利用线程池(后面讲到)
2、常用方法
①比较start和run:
- 直接调用run是在主线程中执行run方法,没有启动新线程
- 使用start是启动新线程,通过新线程间接执行了run方法
②sleep与yield:
- sleep 方法是属于 Thread 类中的,sleep 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会
- yield和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会
③ join方法(后面可以加上时间参数)
join方法用于实现同步操作。比如在主线程中开启线程 t,然后调用t.join()
,主线程要在线程 t 执行完毕之后才开始执行。
④ interrupt 方法(打断sleep、wait、join的线程)
打断 sleep 的线程会清空打断状态(即调用isInterrupted()
显示false),而打断正常运行的线程则不会清空打断状态(状态为true)。
打断park线程不会清空打断状态,如果打断标记已经是true,则park会失效。可以使用Thread.interrupted()
清除打断状态
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
});
t1.start();
sleep(1);
t1.interrupt();
}
输出:
21:13:48.783 [Thread-0] c.TestInterrupt - park...
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.812 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
3、主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。垃圾回收器线程就是一种守护线程。
//t1为线程
t1.setDaemon(true);//设置t1为守护线程
t1.start();
4、线程的状态
①从操作系统层面来描述
② 从Java API层面,根据Thread.State
枚举来划分为六种状态
5、两阶段终止模式
见后文设计模式总结
Ⅲ 共享模型之管程
1、临界区及竟态条件
- 多个线程访问共享资源时,读取共享资源没有问题,但是多线程对共享资源读写操作发生指令交错就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,这段代码块为临界区
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测称之为发生了竟态条件
2、synchronized (对象锁)解决方案
可以给代码块加锁(常见情况),也可以给方法加锁(如果是在静态方法上加锁相当于锁住了这个类)。
-
在普通方法上加锁
class Number{ public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } public void c() { log.debug("3"); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()->{ n1.a(); }).start(); new Thread(()->{ n1.b(); }).start(); //上两个线程的a()和b()不能同时执行 new Thread(()->{ n1.c(); }).start(); } //输出结果:3 1s 12或者23 1s 1或者32 1s 1
-
在静态方法上加锁
class Number{ public static synchronized void a() { sleep(1); log.debug("1"); } public static synchronized void b() { log.debug("2"); } } public static void main(String[] args) { //虽然new了两个对象,但是由于锁是加在静态方法上的,锁的是这个类,因此下面两个线程的方法还是不能同时执行,没有锁的会被阻塞 Number n1 = new Number(); Number n2 = new Number(); new Thread(()->{ n1.a(); }).start(); new Thread(()->{ n2.b(); }).start(); } //答案:1s后12或者2 1s后1
3、变量的线程安全分析
①成员变量和静态变量
- 如果他们没有共享则线程安全
- 如果被共享但只有读操作,则线程安全;若被共享了还有读写操作,则这段代码是属于临界区,线程不安全
②局部变量
局部变量是线程安全的
-
局部变量引用的对象不一定线程安全。如果该对象没有逃离方法的作用范围,则线程安全;如果逃离了方法的作用范围,则需要考虑线程安全
//例1 class A { ArrayList<String> list = new ArrayList<>(); public void method1 (int n) { //arr是局部变量,但是它的引用list是成员变量,因此可能会有线程安全问题 ArrayList<String> arr = new ArrayList<>(); arr = list; ······· } } //例2 class ThreadUnsafe { ArrayList<String> list = new ArrayList<>(); public void method1(int loopNumber) { for (int i = 0; i < loopNumber; i++) { // { 临界区, 会产生竞态条件 method2(); method3(); // } 临界区 } } private void method2() { list.add("1"); } private void method3() { list.remove(0); } } //解决方法,可以把list移动到method1里面去,作为局部变量
image-20201215152132284
4、Monitor概念
32位虚拟机的Java对象头:
64位虚拟机的Mark Word:
5、Monitor原理
Monitor 即为监视器或管程。每个Java对象都可以关联一个Monitor对象,若使用synchronized给对象上锁后(重量级锁),该对象头的Mark Word中就被设置指向Monitor对象的指针。(Monitor ≠ 锁对象 )
Monitor的结构:
注:EntryList
中为阻塞队列,一旦锁被释放,将在阻塞队列中选取一个线程进行执行。而WaitSet
中线程在没有被notify()
之前不会被执行,即cpu不会在WaitSet
中选择线程执行。
6、synchronized原理
synchronized的底层是使用操作系统的mutex lock
实现的。JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter
和monitorexit
指令实现的,monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行monitorenter
指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1(可重入锁);相应地,在执行monitorexit
指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
注意两点:
- synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
①轻量级锁(乐观锁)
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
-
加锁过程
-
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
image-20201215162946927 -
让锁记录中 Object reference 指向锁对象,并尝试用
cas
替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录image-20201215163049688 -
如果
cas
替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下image-20201215163140698 -
如果
cas
失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,若当前只有一个等待线程,则可通过自旋稍微等待一下,可能另一个线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条
Lock Record
作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一;当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用
cas
将 Mark Word 的值恢复给对象头,若成功则解锁成功,若失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
-
-
解锁过程
(1)通过
CAS
操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
② 偏向锁(乐观锁)
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS
操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS
将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS
。以后只要不发生竞争,这个对象就归该线程所有
偏向锁获取:
- (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
- (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
- (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
- (4)如果CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。当到达全局安全点(
safepoint
)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着(因为可能持有偏向锁的线程已经执行完毕,但是该线程并不会主动去释放偏向锁),如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。 - (5)执行同步代码。
偏向锁释放:
如上步骤(4)。偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
③ 偏向锁、轻量级锁、重量级锁之间的转换
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
- 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的
ThreadID
改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS再进行操作。 - 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
- 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
④ 其他锁优化
-
锁消除
锁消除即删除不必要的加锁操作。虚拟机即时编辑器在运行时,对一些“代码上要求同步,但是被检测到不可能存在共享数据竞争”的锁进行消除。
-
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } } //这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
-
自旋锁与自适应锁
- 自旋锁:让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器额执行时间),看持有锁的线程是否会很快释放锁。
-
自适应的自旋锁:
JDK1.6
引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。 - 自旋锁使用场景:从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的
⑤ 总结
synchronized特点:保证内存可见性、操作原子性
synchronized影响性能的原因:
- 1、加锁解锁操作需要额外操作;
- 2、互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
synchronized锁:对象头中的Mark Word根据锁标志位的不同而被复用
- 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较
ThreadID
。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存活,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁 - 轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
- 重量级锁:指向互斥量(
mutex
),底层通过操作系统的mutex lock
实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。
- 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较
7、wait/notify 原理
必须获取到对象的锁才能执行下面的方法:
-
obj.wait()
让进入object监视器的线程到waitSet
等待 -
obj.wait(long n)
有限等待或者被notify()
-
obj.notify()
在object上正在waitSet
等待的线程中挑一个唤醒 -
obj.notify()
object上正在waitSet
等待的线程全部唤醒
比较wait()
和sleep()
:
-
sleep()
是Thread
类中的方法,wait()
是Object
中的方法 -
sleep
不需要强制和synchronized
配合使用,但wait
需要和synchronized
一起用 - sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
8、保护性暂停模式、生产者消费者
9、Park/Unpark
它们是LockSupport
类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
Park/Unpark和wait/notify比较
- wait,notify 和
notifyAll
必须配合 Object Monitor 一起使用,而 park,Unpark
不必 - park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
Park/Unpark 原理
10、死锁、活锁、饥饿
11、ReentrantLock
它也是可重入锁,相对于synchronized它的特点:
- 可中断 (
lock.lockInterruptibly()
) - 可以设置超时时间(例如
lock.tryLock(1,TimeUnit.SECONDS)
) - 可以设置为公平锁
-
支持多个条件变量(
waitset = lock.newCondition()
,用waitset.await()/signal()
)
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
12、同步模式之顺序控制
Ⅳ 共享模型之内存
1、Java内存模型
-
原子性:保证指令不会受到线程上下文切换的影响
image-20201216091413816 -
可见性:保证指令不会受 cpu 缓存的影响
static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while(run){ // .... } }); t.start(); sleep(1); run = false; // 线程t不会如预想的停下来 }
image-20201216091804799CPU缓存:
image-20201216092509909image-20201216092524957
解决方法:可以使用volatile
关键字,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。注意只能保证看到最新值不能解决指令交错的问题
如果在前面示例的死循环中加入 System.out.println()
会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?因为sout
就是加锁的语句,解锁前会刷新cpu缓存
- 有序性:保证指令不会受 cpu 指令并行优化的影响
2、模式:两阶段终止和Balking,线程安全单例模式
3、volatile 原理
① 内存屏障
内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:
1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
- 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
- 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
Java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
LoadLoad屏障:对于这样的语句
Load1
; LoadLoad;Load2
,在Load2
及后续读取操作要读取的数据被访问前,保证Load1
要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1
; StoreStore;Store2
,在Store2
及后续写入操作执行前,保证Store1
的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1
; LoadStore;Store2
,在Store2
及后续写入操作被刷出前,保证Load1
要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1
; StoreLoad;Load2
,在Load2
及后续所有读取操作执行前,保证Store1
的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile
的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作后插入LoadLoad屏障,并在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
②与synchronized比较
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以volatile 性能肯定比 synchronized 关键字要好。但是volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
Ⅴ 共享模型之无锁
1、CAS(基于乐观锁的思想)
CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性);CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】
CAS体现的是无锁并发、无阻塞并发