并发编程的本质
并发编程在一定的意义上其实就是多线程编程,那么多线程编程中主要有哪些,
多线程编程中会涉及到同步操作,线程互斥操作,大的任务进行拆解并发处理(分工);
所以并发编程的实质就是为了解决我们的业务需求,从而尽可能的压榨CPU的性能,达到性能的最大化;
可以说在现在的大多数应用程序中,多线程的身影无处不在,单线程的应用已经成为过去时,如果非要说多线程的应用场景是什么,我可以说多线程的应用场景无处不在,适用于任何的场景下。
JMM模型
JMM模型就是JAVA的内存模型,但是这里我更喜欢叫JAVA的线程内存模型,
我们都知道我们的JVM的内存模型中有堆,元空间还有虚拟机栈,虚拟机栈由执行引擎执行时会在虚拟机栈中创建栈桢来保存我们的变量信息和操作信息,所以JMM模型和虚拟机就有很大的关系,
当我们创建一个对象时,比如new User()和 new Thread()这两者有什么区别呢?
相同点: 都是在堆区创建一个java对象,
不同点: 我们都知道Thread是java的线程对象,通过它可以启动一个线程去执行我们的任务,而User只是一个普通的java对象,那么他们两者有什么区别呢?
当我们创建一个线程Thread,其实不是JAVA开辟了一个线程,Thread底层启动线程和线程的操作是调用的native本地方法的,所以其实创建线程是交给了内核去
做的,JVM本身是不具备调度CPU的权限的 ,是由内核去调度CPU的,比如看下图:
说白了就是JVM不具备调用CPU的权限,是交给了操作系统去执行的,实现从用户态向内核态的转变。
我们都知道JAVA应用程序可以在windows、linux、unix等操作系统上运行,但是我们编写的多线程程序只有一份,也没有在程序中区分操作系统而执行不同的代码块,所以java应用程序创建线程交给操作系统内核的时候这个时候是屏蔽了不同操作系统的差异的,我们可以把它抽象出来,(抽象出来就是我们不管操作系统的类型,我写的这个多线程在不同的操作系统上都能执行,并且都能执行成功而达到预期。 这点很重要 jmm 为了屏蔽各种硬件
和操作系统对内存访问的差异性而规定的)
并发编程带来的风险
并发编程是好,并发编程会带来性能的提升,但是它就真的好,无脑使用吗?
答案是不是的!
并发编程还是要根据实际的业务场景和熟练的程度来使用,否则就会带来一定的风险,那么会带来哪些风险?
1.性能的问题:
就是说我们的业务场景是否需要启动多线程来执行我们的这些任务,这个要根据数据量和处理的任务复杂度来考虑,比如有个需求可以用单线程执行的,你非要使用多线程来执行,那么性能不一定会提升,反而会降低系统性能,为什么呢?因为多线程是要考虑到线程间的上下文切换带来的性能损耗的;是不是系统出现了一些问题,会有人经常给你说,这段程序操作的原因是因为多线程的上下文切换带来的性能开销导致了一些问题;而单线程是一直占用cpu的执行权限的,它可以一直执行,直到执行完成,所有是否采用多线程还是要根据要处理的任务的复杂度以及数据量的大小,多线程在这种情况下就不一定会达到性能提升的效果,我们来看下多线程的上下文切换的图:
所以要根据实际的情况考虑是否采用多线程,多线程是好,可以尽可能的压榨CPU的性能,但是多线程会有线程的上下文切换,所以这个要考虑到性能的开销来觉得是否采用多线程。
2.线程的活跃性问题:
线程的活跃性问题有饥饿,死锁和活锁
- 饥饿:就是在多线程的环境下,比如有些线程被调了线程的优先级非常低,那么有可能这个线程永远都不会被调度,永远处于饥饿的状态,类似于死锁一样,永远不会被调度;所以这种情况下,要特别注意这种情况的发生,也就是说线程的优先级是可以影响到线程获取CPU执行周期,一个线程启动了永远得不到CPU的执行时间周期,那么就会被活活饿死。
- 死锁:死锁这个比较好理解,就是Thrad1锁了a对象,依赖b对象,而Thrad2锁了b对象,依赖a对象,那么就会出现相互依赖,永远无法释放对象锁,就会出现死锁
public class T0915 {
final static Object a = new Object();
final static Object b = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开始执行...");
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 获得a对象执行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+" 依赖b对象执行...");
}
}
},"Thread1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开始执行...");
synchronized (b){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 获得b对象执行...");
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 依赖a对象执行...");
}
}
},"Thread2").start();
}
}
- 活锁:
活锁是什么意思呢?活锁就是说你获取了锁没有意义,就是说你获取了锁没有干正事儿,比如两个线程,第一个线程获取了锁,发现需要让步给其他锁,其他锁获取了锁,也让给了其他锁,这样一来一往,其实大家都没有干正事儿
public class LiveLockTest {
/**
* 定义一个勺子,ower 表示这个勺子的拥有者
*/
static class Spoon {
Diner owner;
public Spoon(Diner diner) {
this.owner = diner;
}
public String getOwnerName() {
return owner.getName();
}
public void setOwner(Diner diner) {
this.owner = diner;
}
//表示正在用餐
public void use() {
System.out.println(owner.getName() + " 用这个勺子吃饭.");
}
}
/**
* 定义一个晚餐类
*/
static class Diner {
private boolean isHungry;
//用餐者的名字
private String name;
public Diner(boolean isHungry, String name) {
this.isHungry = isHungry;
this.name = name;
}
//和某人吃饭
public void eatWith(Diner diner, Spoon sharedSpoon) {
try {
synchronized (sharedSpoon) {
while (isHungry) {
//当前用餐者和勺子拥有者不是同一个人,则进行等待
while (!sharedSpoon.getOwnerName().equals(name)) {
sharedSpoon.wait();
}
if (diner.isHungry()) {
System.out.println(diner.getName()
+ " 饿了," + name + "把勺子给他.");
sharedSpoon.setOwner(diner);
sharedSpoon.notifyAll();
} else {
//用餐
sharedSpoon.use();
sharedSpoon.setOwner(diner);
isHungry = false;
}
Thread.sleep(500);
}
}
} catch (InterruptedException e) {
System.out.println(name + " is interrupted.");
}
}
public boolean isHungry() {
return isHungry;
}
public void setHungry(boolean hungry) {
isHungry = hungry;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) {
final Diner ant = new Diner(true, "ant");
final Diner monkey = new Diner(true, "monkey");
final Spoon sharedSpoon = new Spoon(monkey);
Thread h = new Thread(()->ant.eatWith(monkey, sharedSpoon));
h.start();
Thread w = new Thread(()->monkey.eatWith(ant, sharedSpoon));
w.start();
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// h.interrupt();
// w.interrupt();
}
}
3.线程的数据安全问题:
线程数据安全问题就是比较常见而且也是多线程编程的一个老话题了,
针对线程安全问题,一般通过锁来控制线程的数据安全,锁机制在java多线程开发过程中大体分为两种,内置锁和JUC锁:
内置锁:使用Synchornized,synchronized是java的关键字,如果简简单单的通过看底层源码的方式是没有办法看到的,需要通过汇编指令来看才能理解这个锁的机制,
synchronized锁的对象,不是代码块,这个概念一定要弄清楚,因为经常会听到说锁代码块,锁方法,其实它是锁的对象;
synchronized锁是不需要程序员释放的,它本身会自己释放锁,底层有一个锁升级的过程,依次为自适应锁(自旋锁)、偏向锁、轻量级锁、重量级锁,
synchronized是基于object monitor的机制来实现的,默认是非公平锁。
JUC包下面的锁:JUC下面的锁比较常用的有ReentrantLock和LockSupport,这个锁是JDK的已有的实现,
JUC锁有独占锁、共享锁、公平锁、非公平锁以及读写锁;
JUC锁都有一个特征如果你手动加锁了,记得要手动释放,jvm是不会给你释放锁的,一般我们会在finally中去释放锁,保证整个锁能够释放。
不管什么语言,并发的编程都是在高级的部分,可见并发有多难,因为并发的涉及的知识太广,不单单是操作系统的知识,还有计算机的组成的知识等等。说到底,这些年硬件的不断的发展,但是一直有一个核心的矛盾在:CPU、内存、I/O设备的三者的速度的差异。这就是所有的并发的源头
为了那么解决这三者的差异生产的解决办法如下:
-
CPU增加了缓存,以均衡与内存的差异;
-
操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异;
-
编译程序优化指令执行次序,使得缓存能够得到更加合理的利用。
但是 以上的解决办法都会导致相应的问题。 由此并发的三大问题来源我们已经找到了
并发问题出现的三大源头
2.1缓存导致可见性问题
- 单核:所有的线程都是在一颗CPU上执行,CPU缓存与内存的数据一致性容易解决。因为所有线程都是同一个CPU的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。
多核:每颗CPU都有自己的缓存,这时CPU缓存与内存的数据一致性就没有那么容易解决了,当多个线程在不同的CPU上执行时,这些线程操作的是不同的CPU缓存(CPU的解决的方案:MESI协议)
可见性问题深入分析
思考:上面例子中为什么多线程对共享变量的操作存在可见性问题?
我们主要看load 方法和 flag变量 的编码方式
threadB修改flag,但是threadA 不能跳出 while循环的情况有以下两种
/**
* @author Fox
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*/
public class VisibilityTest {
private boolean flag = true;
private int i = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
int i = 0;
while (flag) {
i++;
//TODO 业务逻辑
}
System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
1、flag属性定义如下 :
private boolean flag = true;
2、load方法如下
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
int i = 0;
while (flag) {
//TODO 业务逻辑
}
System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
}
执行结果为: thread A 在while 循环里面执行 未终止
threadB修改flag,threadA能跳出 while循环的情况有以下:
1、使用volatile关键字修饰变量,保证变量的可见性
package bat.ke.qq.com.learnjuc.volatiledemo;
/**
* 源码学院-Fox
* 只为培养BAT程序员而生
* http://bat.ke.qq.com
* 往期视频加群:516212256 暗号:6
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*
*/
public class VisibilityTest {
// JMM模型 java线程内存模型
// 可见性 为什么? lock addl $0x0,(%rsp) 触发缓存一致性协议
private volatile boolean flag = true;
private int i = 0;
public void refresh(){
flag = false;
System.out.println(Thread.currentThread().getName()+"修改flag");
}
public void load(){
System.out.println(Thread.currentThread().getName()+"开始执行.....");
int i=0;
while (flag){
i++;
}
System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i);
}
public static void main(String[] args){
VisibilityTest test = new VisibilityTest();
new Thread(() -> test.load(), "threadA").start();
try {
Thread.sleep(2000);
new Thread(()->test.refresh(),"threadB").start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
1、flag属性定义如下 :
private volatile boolean flag = true;
2、load 方法
public void load(){
System.out.println(Thread.currentThread().getName()+"开始执行.....");
while (flag){
i++;
}
System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i);
}
执行结果: thread A 跳出while 循环
2、使用内存屏障保证可见性
package bat.ke.qq.com.learnjuc.volatiledemo;
/**
* 源码学院-Fox
* 只为培养BAT程序员而生
* http://bat.ke.qq.com
* 往期视频加群:516212256 暗号:6
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*
*/
public class VisibilityTest {
private boolean flag = true;
private int i = 0;
public void refresh(){
flag = false;
System.out.println(Thread.currentThread().getName()+"修改flag");
}
public void load(){
System.out.println(Thread.currentThread().getName()+"开始执行.....");
while (flag){
i++;
// 使用内存屏障保证flag的可见性
UnsafeFactory.getUnsafe().storeFence();
}
System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i);
}
public static void main(String[] args){
VisibilityTest test = new VisibilityTest();
new Thread(() -> test.load(), "threadA").start();
try {
Thread.sleep(2000);
new Thread(()->test.refresh(),"threadB").start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
1、flag属性定义如下 :
private boolean flag = true;
2、load 方法
public void load(){
System.out.println(Thread.currentThread().getName()+"开始执行.....");
while (flag){
i++;
// 使用内存屏障保证flag的可见性
UnsafeFactory.getUnsafe().storeFence();
}
System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i);
}
执行结果: thread A 跳出while 循环
3、使用yeild方法释放时间片 ,与上下文切换有关会重新去加载主内存的值
package bat.ke.qq.com.learnjuc.volatiledemo;/** * 源码学院-Fox * 只为培养BAT程序员而生 * http://bat.ke.qq.com * 往期视频加群:516212256 暗号:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { private boolean flag = true; private int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; Thread.yield(); } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、flag属性定义如下 :
private boolean flag = true;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; Thread.yield(); } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); }
执行结果: thread A 跳出while 循环
4、 使用System.out.println("====="); 打印语句可以跳出while循环
synchroinzed 可以保证可见性,为什么可以保证可见性是因为 synchroinzed的底层会调用storefence 这个内存屏障
package bat.ke.qq.com.learnjuc.volatiledemo;/** * 源码学院-Fox * 只为培养BAT程序员而生 * http://bat.ke.qq.com * 往期视频加群:516212256 暗号:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { private boolean flag = true; private int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; System.out.println("====="); } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、flag属性定义如下 :
private boolean flag = true;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); int i=0; while (flag){ i++; System.out.println("====="); } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); }
执行结果: thread A 跳出while 循环
5、使用LockSupport.unpark(Thread.currentThread());
public class VisibilityTest { private boolean flag = true; private int count = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ count++; LockSupport.unpark(Thread.currentThread()); } System.out.println(Thread.currentThread().getName()+"跳出循环: count="+ count); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、flag属性定义如下 :
private boolean flag = true;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ count++; LockSupport.unpark(Thread.currentThread()); } System.out.println(Thread.currentThread().getName()+"跳出循环: count="+ count);}
6、i 关键字加上volatile关键字
public class VisibilityTest { private boolean flag = true; private volatile int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、int 属性定义如下 :
private volatile int i= 0;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出循环: count="+ count);}
执行结果;
7、使用Integer修饰i
package bat.ke.qq.com.learnjuc.volatiledemo;import java.util.concurrent.locks.LockSupport;/** * 源码学院-Fox * 只为培养BAT程序员而生 * http://bat.ke.qq.com * 往期视频加群:516212256 暗号:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { // JMM模型 java线程内存模型 // 可见性 为什么? lock addl $0x0,(%rsp) 触发缓存一致性协议// private volatile boolean flag = true; private boolean flag = true; private Integer i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、int 属性定义如下 :
private Integer i= 0;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i);}
执行结果;
8、 使用showWait方法
package bat.ke.qq.com.learnjuc.volatiledemo;import java.util.concurrent.locks.LockSupport;/** * 源码学院-Fox * 只为培养BAT程序员而生 * http://bat.ke.qq.com * 往期视频加群:516212256 暗号:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { // JMM模型 java线程内存模型 // 可见性 为什么? lock addl $0x0,(%rsp) 触发缓存一致性协议// private volatile boolean flag = true; private boolean flag = true; private int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行.....");// int i=0; while (flag){ i++; // TODO 不能的情况 // 1. 不能 迷 为什么? 取决于缓存是否失效(过期) // shortWait(10000); // 10微秒 // TODO 能的情况: // 使用内存屏障保证flag的可见性// UnsafeFactory.getUnsafe().storeFence();// Thread.yield(); // 1.sleep 让出cpu时间片 /*try { Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); }*/ // 2. synchronized 可见性保证 内存屏障// System.out.println("====="); // 3. 我们发现i++执行时间短,人为的延长执行时间 ,延长1000微妙 等于1 ms 毫秒 shortWait(1000000);// LockSupport.unpark(Thread.currentThread()); } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、int 属性定义如下 :
private int i= 0;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"开始执行....."); while (flag){ i++; // 模拟程序的执行时间。。。 // 我们发现i++执行时间短,人为的延长执行时间 ,延长1000微妙 等于1 ms 毫秒 shortWait(1000000); } System.out.println(Thread.currentThread().getName()+"跳出循环: i="+ i);}
执行结果;
Java内存模型(JMM)
JMM定义
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,
JMM规范了Java虚拟机与计算机内存是如何协同工作的:
- 规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,
- 以及在必须时如何同步的访问共享变量。
**JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,
JMM即线程通信是围绕原子性、有序性、可见性展开的。
JMM(Java内存模型 )是一个共享内存模型 ,它与 线程之间通信有关系。
从根本上来说,线程间的通信有两种方式:
-
一种是在消息传递的并发模型里,线程之间通过发送消息来进行通信(没有公共状态) ----适用于协程
-
一种是在共享内存的并发模型中,线程之间通过读-写共享内存来实现通信(公共状态)--适用于java线程之间的通信并发
这么说有点不明白,那么 我们举个例子说明一下上面的两种方式:
demo:线程A 与线程B 之间想要交互数据,线程A把 共享变量i改成5 ,线程B想要读到共享变量i 的值
方式1: 在消息传递的并发模型里,线程之间通过发送消息来进行通信(没有公共状态) -----适用于协程
就是 线程A把 共享变量i改成5 ,直接发送消息 告诉线程B ,线程B 收到消息 可以直接拿到线程A修改过后的i值
方式2: 在共享内存的并发模型中,线程之间通过读-写共享内存来实现通信(公共状态)
但是很可惜协程是通过 上述方式1消息传递的方式 ,Java的并发采用的是共享内存模型,所以Java线程之间的通信是基于共享内存实现的
具体来讲Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。如果线程A与B之间要通信的话,必须经历下面两个步骤:
线程A 与线程B 之间想要交互数据,,必须经历下面两个步骤:
- 线程A把本地内存中更新过的共享变量刷新到主内存中去。
- 线程B到住内存中去读取线程A之前更新过的共享变量。
大家要理解这个jmm线程模型一定要站在线程与线程之间如何交互的角度去理解
jmm是一个抽象的概念,本地内存和 主内存不是真实存在的,操作的空间也是逻辑空间!!
并不是说有个硬件区域叫主内存、有个硬件区域叫做本地内存 ,后面讲处理器架构会提到这块。
站在Java内存角度的模型 , 我们开始分析 , threadA 为什么可以跳出循环, 为什么threadA 不可以跳出循环。。。
这边还要说一个东西就是 就是关于主内存与工作内存之间的具体交互协议:
即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成, 虚拟机实现时必须保证这8种操作都是原子的、不可分割的(对于long和double类型的变量来说,load、store、read跟write在某些平台上允许例外)。
内存交互操作
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
主内存、工作内存交互流程如下:
一:启动线程A,因为现在处理器采用多核,所以线程A 中 run方法的代码由java执行引擎放在cpu core1 的寄存器内执行
cpu core1 寄存器需要执行while(flag) ,其中falg是全局变量
① 线程A使用read指令,将主内存中的falg的变量值传输到线程A的工作内存中
- read(读取):作用于主内存变量,把一个变量值从主内存读取接着传输到线程的工作内存中,以便随后的load动作使用
② 在线程A的工作内存中,使用load指令把 从主内存中得到的falg的变量值 放入工作内存的变量副本 (此时我们可以看到线程A的工作内存,有一份变量副本)
- load(载入):作用于工作内存的变量,它把read操作把从主内存中得到的变量值放入工作内存的变量副本中。
注意: 线程A的工作内存中是:
先使用read指令:获取到变量值true之后 然后使用load指令 :将true赋值给变量副本 flag
③ 线程A有属于自己的程序计数器,通过改变这个pc计数器来选取下一条需要执行的字节码指令 , 当pc计数器指向while(flag)指令时
执行引擎会将我们的我们的while(true)指令 指令加载到cpu的寄存器 中 执行
注意:flag 读取的是线程A本地内存的flag
即 使用use指令将工作内存中flag副本值传递给执行引擎,执行引擎将其交给cpu执行,cpu执行while(true)指令
- use(使用):作用于工作内存的变量,,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作把工作内存中的一个变量值传递给执行引擎,然后执行引擎将齐指令交给cpu执行
二:2s过后,线程B启动,
问: 线程B 启动会在CPU CORE1 上执行吗??
答:不会
这边要涉及到操作系统的知识了:
Windows 采用基于优先级的、抢占调度算法来调度线程。
Windows 调度程序 (用于处理调度的 Windows 内核部分称为调度程序) 确保具有最高优先级的线程总是在运行的。
- 调度程序选择运行的线程会一直运行,直到被更高优先级的线程所抢占,或终止,或调用阻塞系统调用(如 I/O)。
- 低优先级线程运行时,更高优先级的实时线程变成就绪,那么低优先级线程就被抢占。这种抢占使得实时线程在需要使用 CPU 时优先得到使用。(实时线程
优先级高) ,
而我们的线程A 执行 while(true),线程A是一个高优先级的线程, 能保证线程A就算调用Thread.yeild 方法 暂时释放cpu的执行权,也会在不久的将来重新获取cpu的执行权,继续执行while(true) 指令 。。 所以线程B 此时 会使用 cpu core2
问: 如果os的核数有限的话,高优先级线程执行while(true) 容易造成死锁问题 ?
答: 优先级比较低的线程会因为优先级低而抢占不到cpu的资源造成死锁,
比如说有3个线程 : 有一个 while(true)的高优先级线程A , 有两个优先级低的的线程B、C 获取同一把锁lock
线程B 最先启动, 并上锁!!,线程C 启动 阻塞在lock锁上,,
此时线程A启动,线程会一直占用cpu 的执行权不释放 , 那么线程B 一直持有锁, 线程C 想要获取lock锁从而一直阻塞在lock 锁上 (线程B 释放锁才能解除线程A的阻塞,线程B 又一直获取不到cpu执行权,所以一直持有lock锁)
通过上面的讲解我们知道线程B 在cpu core2 上执行 。。。。
线程B也是和线程A 一样,从主内存中read flag ,然后 load flag 生成flag 副本,然后 使用use 指令, cpu将 修改flag为false ,
④⑤⑥ 参考上面的①②③步骤
三: ThreadB 更新flag 共享变量
⑦ cpu core2中 寄存器的值需要赋值给工作内存 :
cpu把flag的变量值交给执行引擎,然后cpu执行assign指令 把一个从执行引擎接收到的值赋值给工作内存的变量
- assign(赋值):作用于工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作,它把一个从执行引擎接收到的值赋值给工作内存的变量,
⑧ 线程B的工作内存会把flag变量副本值传输给主内存
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
⑨ 将flag变量副本值赋值给主内存中的flag变量
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
问:线程B 更新完本地内存的变量副本flag 为 false的 , 会马上使用store、write指令把本地内存flag 的变量值刷新回主内存吗?
答: 不会, 只有在某些条件执行了的情况下,才会立马刷新 。。
前致知识: jmm中说的本地内存一般都是使用硬件层面cpu的缓存, jmm中说的主内存一般使用的就是 硬件层面的主内存
os对缓存淘汰(jmm中的工作内存)有一些淘汰策略,在缓存淘汰(也就是缓存失效)之前才会把缓存的数据同步主内存 (也就是同步jmm的主内存)
至于这个某些条件 后面说
问: 线程B 消亡之前 会使用store、write指令把本地内存副本值刷新回主内存吗?
答: 是的,线程B快要结束了,就表示flag的数据不再使用了,那么本地内存中副本变量即将失效!!
那么线程B结束之前肯定会把 缓存的数据同步到主内存 (工作内存 同步到主内存)
问: 那什么时候线程A的本地内存失效呢,这样子线程A就可以去拿主内存的最新flag=false的数据,从而跳出 while循环了!!
现在是因为while(true) 本地内存会一直存在,不会失效。
那么正常程序在使用缓存的时候,缓存一定会有使用时间的,不可能一直存在 。。因为我们的缓存空间很小,当缓存空间不够使用的时候,一定会基于某种算法淘汰掉一些无用的缓存, 如果while(true){ xxxx} , xxx执行的逻辑超过50ms ,那么变量副本flag 肯定会被淘汰,
上面的 8、线程A使用showWait 就是 因为这个原因 而跳出 while 循环的 。。。
你可以修改showwait(1000) 之后 ,那么缓存不会淘汰 ,,
于是我们需要采取一些策略去淘汰缓存;
保证可见性问题 转换成缓存淘汰问题:
1、 使用 Thread.yeild 方法 ,
这个方法 会释放时间片, 意味着 有个cpu上下问切换的概念,,
切换前 while(true) 保存线程, 切换后 还原线程 ,会去加载线程的上下文 。。。线程上下文切换会导致缓存失效
Thread1 执行 x=5 , y=6 ,z= x+ y , 执行刀 y= 6后 cpu切换到线程 2
线程2 执行完后 ,有切换回 线程1 ,线程1 继续执行 z= x+y
??1、 x、y的值为 5、6 是从哪里取得
2、 线程1 为什么知道程序继续从 z=x+y的地方执行
有一个叫做pc 寄存器,(硬件层面) ,可以知道执行位置
x,y 的值哪里来???, thea1 切换之前
会保存上下文信息:如果x、y的值为写操作,那么就会把 xy的值重新刷回到主存中的某个地址 ,如果是读操作 则直接清除缓存即可
之后 thea1 切换后来后,会重新从主存中加载数据 (加载上下文)
于是 thread 读到最新的flag数据 工作副本数据
线程上下文切换 时间为5-10 ms , 线程上下文切换 很耗费性能的 !!!
上下文存在哪里跟硬件架构有关系 。。。。
之后还会再讲的 ,,
程序在计算机上是如何执行的 。。。。
x=6, y= x+5
讲了一下 计算机架构程序是怎样运行的
vlotaile 为什么可以跳出循环。。
Java关键字,要去看jvm源码。。。 c
看openjdk的源码 !!!
volatile在hotspot的实现:
字节码解释器实现
JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对简单且容易理解,缺点是执行慢。
bytecodeInterpreter.cpp
storeLoad这是有个内存屏障,注意这个jmm层面的内存屏障, 不是处理器层面的内存屏障(fence)
jmm : storestore loadload storeload 。。
在linux系统x86中的实现
orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }inline void OrderAccess::fence() { if (os::is_MP()) {// 是否是多核的处理器架构 // always use locked addl since mfence is sometimes expensive#ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");#else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");#endif }}
jvm 可以在不同的处理器架构下运行,linux 、winodw 虚拟机 跨平台可以给我们屏蔽掉底层的不同的差异。
不同的处理器架构有不同的实现,,
我们看的是x86架构的实现,调用了fence()风法
lock; addl $0,0(%%rsp) 是一个汇编层面的指令,,, lock前缀指令,, 是storeload的实现,
lock前缀指令不是处理器架构中内存屏障的指令(mfence),
因为x86 架构认为lock前缀指令的性能比mfence 内存屏障的处理机架构指令要好一些,所以总是喜欢用lock 替换
知道lock前缀指令的作用就可以知道如何valtoile 为何 能够保证可见性!!!
一下就干到汇编,此时就跟硬件有关
lock指令的作用,, 重排序先不管,,,
第三点:3. LOCK前缀指令会 等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。
什么意识呢 ???
如何用了lock 指令会保证 ,lock指令是涉及到硬件的
我们都知道jmm 中的本地内存,主内存是逻辑空间,逻辑空间最终会映射到物理空间,
jmm会尽可能的保证 保证工作内存的数据映射到cpu缓存 【cache】中,保证了主内存的数据映射到内存中。。
于是可见性可以保证了。。
lock指令不是一个cpu处理器中不是一个内存屏障指令,但是它有内存屏障的效果,还能让处理器缓存中的其他副本失效。。
缓存失效从主内存中加载flag
lock前缀指令能让缓存失效???为什么?? 于硬件有关之后讲
另外一种方式: jvm 做了优化把常用的方式用模板解释器实现:
模板解释器实现
模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。
templateTable_x86_64.cpp
// 负责执行putfield或putstatic指令void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) { // ... // Check for volatile store __ testl(rdx, rdx); __ jcc(Assembler::zero, notVolatile); putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags); // x86只生效哦storeLoad volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad | Assembler::StoreStore)); __ jmp(Done); __ bind(notVolatile); putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags); __ bind(Done); }void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits order_constraint) { // Helper function to insert a is-volatile test and memory barrier if (os::is_MP()) { // Not needed on single CPU __ membar(order_constraint); }}
assembler_x86.hpp
// Serializes memory and blows flags void membar(Membar_mask_bits order_constraint) { // We only have to handle StoreLoad // x86平台只需要处理StoreLoad if (order_constraint & StoreLoad) { int offset = -VM_Version::L1_line_size(); if (offset < -128) { offset = -128; } // 下面这两句插入了一条lock前缀指令: lock addl $0, $0(%rsp) lock(); // lock前缀指令 addl(Address(rsp, offset), 0); // addl $0, $0(%rsp) } }
下面这两句插入了一条lock前缀指令: lock addl $0, $0(%rsp)
lock(); // lock前缀指令 :
lock addl $0, $0(%rsp) 再次验证了 都会执行汇编指令
addl(Address(rsp, offset), 0); // addl $0, $0(%rsp) 在rsp是我们的64位寄存器,在我们的栈顶加入0 无意义
汇编层面volatile的实现 :
一定要有hsdis-amd65.ddl
添加下面的jvm参数查看之前可见性Demo的汇编指令
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
找到putfield flag 即可:
验证了可见性使用了lock前缀指令
前端编译: jvm指令
后端编译: 汇编指令
汇编指令最后会转换成机器指令。。供计算机使用
UnsafeFactory.getUnsafe().storeFence();
x86 的 storefence -》 底层就是调用了
sout
底层用了Synchroinzed :x86 也是调用UnsafeFactory.getUnsafe().storeFence();
那么 storefence 内存屏障---》 lock指令
park: x86 也是调用UnsafeFactory.getUnsafe().storeFence();
sleep --->调用parlk ->x86 -》x86 也是调用UnsafeFactory.getUnsafe().storeFence();
Integer: 于关键字 final 有关
value值 是一个final 关键字修饰, jvm 对final做了优化 保证了可见性
java可见性如何保证: 方式规律由两种
可见性的问题:
1、 利用jmm 层面storeLoad ===》 x86 使用lock替代了mfence
2、利用线程上下文切换 ,缓存失效。。 Thread.yeild
等待唤醒机制: park unpark
linux底层用的是 pthrea_cond_timewait
从硬件层面分析Lock前缀指令
操作的规则:
java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,即不允许加载或同步工作到一半。
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后,必须吧改变化同步回主内存。
- 不允许一个线程无原因地(无assign操作)把数据从工作内存同步到主内存中。
- 一个新的变量只能在主内存中诞生。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,,多次lock之后必须要执行相同次数的unlock操作,变量才会解锁。
- 如果对一个对象进行lock操作,那会清空工作内存变量中的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock,就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁住的变量。
- 对一个变量执行unlock操作之前,必须将此变量同步回主内存中(执行store、write)。
有如上8种内存访问操作以及规则限定,再加上对volatile的一些特殊规定,就已经完全确定了java程序中哪些内存访问操作是在并发下安全的。
对于volatile的特殊规则:
volatile有两个特性:1、对所有线程可见;2、防止指令重排;我们接下来说明一下这两个特性。
可见性,是指当一条线程修改了某个volatile变量的值,新值对于其它线程来说是可以立即知道的。而普通变量无法做到这点。但这里有个误区,由于volatile对所有线程立即可见,对volatile的写操作会立即反应到其它线程,因此基于volatile的变量的运算在并发下是安全的。这是错误的,原因是volatile所谓的其它线程立即知道,是其它线程在使用的时候会读读内存然后load到自己工作内存,如果这时候其它线程进行了修改,本线程的volatile变量状态会被置为无效,会重新读取,但如果本线程的变量已经被读入执行栈帧,那么是不会重新读取的;那么两个线程都把本地工作内存内容写入主存的时候就会发生覆盖问题,导致并发错误。
防止指令重排,重排序优化是机器级的操作,也就是硬件级别的操作。重排序会打乱代码顺序执行,但会保证在执行过程中所有依赖赋值结果的地方都能获取到正确的结果,因此在一个线程的方法执行过程中无法感知到重排的操作影响,这也是“线程内表现为串行”的由来。volatile的屏蔽重排序在jdk1.5后才被修复。原理是volatile生成的汇编代码多了一条带lock前缀的空操作的命令,而根据IA32手册规定,这个lock前缀会使得本cpu的缓存写入内存,而写入动作也会引起别的cpu或者别的内核无效化,这相当于对cpu缓存中的变量做了一次store跟write的操作,所以通过这样一个操作,可以让变量对其它cpu立即可见(因为状态被置为无效,用的话必须重新读取)。
另外,java内存模型对volatile变量有三条特殊规则:
a、每次使用变量之前都必须先从主内存刷新最新的值,用于保证能看见其它线程对变量的修改;
b、每次对变量修改后都必须立刻同步到主内存中,用于保证其它线程可以看到自己的修改;
c、两个变量都是volatile的,将数据同步到内存的时候,先读的先写;
long跟double变量的特殊规则
对于64位的数据类型long跟double,java内存模型定义了一条相对宽泛的规定:允许虚拟机将没有被volatile修饰的64位数据操作分为两次32位的操作来进行。也就是允许虚拟机不保证64位数据load、store、read跟write这4个操作的原子性,这就是long跟double的非原子性协定。如果真的这样,当多个线程共享一个并未声明为volatile的long或者double类型的变量,并同时对他们进行读取修改,那么某些线程可能会读到一些既非初始值也不是其他线程修改值的代表了“半个变量”的数据。
不过这种读到“半个变量”的情况非常罕见,因为java内存模型虽然允许实现为非原子的但“强烈建议”将其实现为原子操作,实际开发中,所有商用虚拟机都将其实现为原子操作,因此,这点我们并不需要担心。
Java虚拟机-pc寄存器执行过程理解 https://blog.csdn.net/Alphr/article/details/105665051
问题?在某个时间点本地内存会把数据刷回主内存?与硬件有关
可见性问题是因为数据在缓存中的更新不能及时的通知其它线程。
二期;https://blog.csdn.net/qq_36434742/article/details/106722869
-
专题一--并发编程专题
-
提取码:wtel
Effective Java 第三版——78. 同步访问共享的可变数据
3期: https://blog.csdn.net/scjava/article/details/108673512?spm=1001.2014.3001.5501
并发编程之JMM模型&Volatile底层原理 https://blog.csdn.net/scjava/article/details/108673512
【并发编程系列3】volatile内存屏障及实现原理分析(JMM和MESI)
https://blog.csdn.net/zwx900102/article/details/106306915/?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-1.no_search_link&spm=1001.2101.3001.4242
4期: 4. synchronized详解](https://www.cnblogs.com/ITPower/p/13620573.html) https://note.youdao.com/ynoteshare/index.html?id=ee257490aa10fc87bb8c3823ed1e5421&type=note&_time=1632375637449
https://www.cnblogs.com/IcanFixIt/p/10633368.html
VIP并发编程之JMM&volatile详解
使用工具看汇编指令,是目前最好的教程说缓存一致性的。。。
二期锁:
jol openjdk ,下过来可以看到。。
64bits - > 8 个字节
32bits ->4个字节
128bits -> 12个字节
锁是给对象上锁:
其实就是给对象头的最后两位上锁
00、01、10、11 表示四种状态
五种状态通过2个bits表示
无锁:前56个表示hashcode 、 1一个未使用 4个表示年龄 1个是否是偏向锁的标志 最后两个表示锁
ptr_to_lock_record 下节课说
不延迟了一上来就是偏向锁,之后会升级
偏向锁有缺点,偏向锁要消除锁
13-java内存模型
二期关于 hb 八大原则的!!https://vip.tulingxueyuan.cn/detail/v_60336242e4b035d3cdba00b0/3?from=p_603394e5e4b035d3cdba188e&type=6
①sleep方法给其他线程运行机会时不考虑线程的优先级,因此会给低线程优先级运行的机会,而yield方法只会给相同优先级或者更高优先级线程运行的机会
②线程执行sleep()方法后转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程可能在进入可执行状态后马上又被执行
③sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常
④sleep()方法比yield()方法(跟操作系统相关)有更好的可移植性
————————————————
版权声明:本文为CSDN博主「不羁朔风」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36071795/article/details/83890281
JMM与硬件内存架构的关系
Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。
对于硬件,所有的线程栈和堆都分布在主内存(这个主内存 叫做 RAM - main memory 是硬件中的叫法 )中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如上图所示,Java内存模型和计算机硬件内存架构是一个交叉关系 。。 这个叙述是不是有问题呀 ?? 晚上问一下老师