【问题的产生】:
程序真的是按照顺序执行的吗?
/**
* 本程序跟可见性无关,曾经有同学用单核也发现了这一点
*/
import java.util.concurrent.CountDownLatch;
public class T01_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
for (long i = 0; i < Long.MAX_VALUE; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(2);
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
latch.countDown();
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
latch.countDown();
}
});
one.start();
other.start();
latch.await();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
}
}
}
}
【最终输出】:
【图解程序 】:
//上图是8种可能出现的执行情况。你会发现——出现(0,0)的情况就意味着一定是线程内部执行的时候调换了顺序。
【 乱序的原因 】:
//简单说 , 就是为了提高效率。
【下图】:
第一条CPU指令是去内存读数据,等待数据的返回;
第二条CPU指令是本地寄存器自增。
【分析】:
//CPU的速度要比内存的速度快100倍。如果必须要按顺序执行的话,在等待返回过程需要大量的等待。
【什么情况下,两条指令可以交换顺序呢? 】:
如果前后指令之间有依赖关系 , 后续指令必须依赖前面的指令,那么是无法交换顺序的。——如果前后两条指令不存在依赖关系,则是可以进行交换的。
【 乱序存在的条件 】:
as——if——serial
不影响单线程的最终一致性。
不影响单线程的最终一致性时,各指令可以交换顺序。
【如】:
x=1; y=1。
x=a; y=a。
//上述两组指令都不影响最终结果。(打乱执行顺序的话)
【但如下就不行】:
x=1; x++。
【程序理解可见性和有序性】:
《Java并发编程实践》 中的一个例子。
package T04_YouXuXing;
public class T02_NoVisibility {
private static boolean ready = false;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number); //这里的打印有可能会是0.
}
}
public static void main(String[] args) throws Exception {
Thread t = new ReaderThread();
t.start();
number = 42; //没有前后依赖关系。
ready = true; //没有前后依赖关系。
t.join();
}
}
- 思考—上述程序存在什么问题吗?
(1)可见性问题
ready设置为true后不会马上停止,但也有可能马上停止。
要在number上加volatile关键字。——保证可见性。
(2)有序性问题
number = 42; //没有前后依赖关系。
ready = true; //没有前后依赖关系。
//这两个指令有可能第二个先执行——此时就会输出0 , 因为还没有执行到设置值为42的那一步。
【线程的半初始化状态】:
【对象的创建过程】:
0——申请内存;
4——特殊调用,特殊调用了T的init方法即默认的构造方法。
7——建立关联,和t变量建立关联。
【安全性】:
当我们new出一个对象 , 里面成员变量m的值是多少呢?——其实是和上一个使用这一块区域的程序有关系( C语言 )。
//Java中int类型的默认值是 0。
//这一句执行完,m=0 , 这是对象的半初始化状态。
//执行完这一句指令后——m的值才会变为8。
t变量和内存区域建立关联。
【 this对象逸出 】:
package T04_YouXuXing;
public class T03_ThisEscape {
private int num = 8;
public T03_ThisEscape() {
new Thread(() -> System.out.println(this.num) //这里有可能输出中间状态0.
).start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
System.in.read(); //进入阻塞
}
}
//——这里是有可能出现问题的。
【 num为何可能输出中间状态呢? 】:
this存在于局部变量表里面。
//这两条指令是有可能换顺序的。这样就会先建立关联,关联完之后才调用构造方法 , 结果在调用构造方法的过程当中,新建了一个线程去输出当前的num值——此时因为是先关联的,所以为0 , 就先输出0了。
即——先进行了关联 , 关联完之后再调用的构造方法。
【防止this逸出现象】:
可以在构造方法里NEW线程,但是!!!!!!!————不要让它在那里启动。
什么时候启动?——单独写一个方法。
【 修改程序 】:
package T04_YouXuXing;
public class T03_ThisEscape2 {
private int num = 8;
Thread t;
public T03_ThisEscape2() {
t = new Thread(() -> System.out.println(this.num) );
}
public void start(){
t.start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape2();
System.in.read(); //进入阻塞
}
}
【 美团面试题 】:
关于Object o = new Object( )
【1–请解释一下对象的创建过程(半初始化)】:
创建对象是有一个半初始化过程的。Java里半初始化过程是赋默认值的,C++等语言是赋内存上遗留下来的值。
【2–加问DCL与volatile问题(指令重排)】:
DCL单例是否要加volatile , 这里面主要涉及指令重排序的问题。
volatile有两大作用——线程可见&禁止重排。as_if_serial这种机制是为了提高利用率。
单线程中as_if_serial结果幂等 , 执行指令可以随意排序。——但是,这种程序一旦到了多线程中就会出现问题!!!!!!
如果不想让其排序的话 , 怎么办?——使用volatile可以禁止重排~~~! ! !
【DCL】:
饿汉式单例代码:
package T04_YouXuXing.T05_singleton;
/*
饿汉式
类加载到内存之后,就实例化一个单例,JVM保证线程安全;
唯一缺点————不管用到与否,类装载时就完成实例化;
* */
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01(){};
public static Mgr01 getInstance(){ return INSTANCE; }
public void m(){
System.out.println("m");
}
public static void main(String[] args){
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1==m2);
}
}
【最终输出】:
True
懒汉式代码:
package T04_YouXuXing.T05_singleton;
/*
* 懒汉式写法
* 【缺点】:多线程访问的时候,你不能够保证一致性,你不能够保证NEW出来的都是同一个对象;
* */
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03(){ }
public static Mgr03 getInstance(){
if (INSTANCE == null){ //两个线程接连判断都为NULL(还没来得及NEW出来),这时就会新建两个。
try{
Thread.sleep(1); //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m(){
System.out.println(" m ");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(
()-> System.out.println( Mgr03.getInstance().hashCode() )
).start();
}
}
}
【最终输出】:
【修正程序】:
//只在上述程序的基础上加synchronized
public static synchronized Mgr03_02 getInstance(){
if (INSTANCE == null){ //两个线程接连判断都为NULL(还没来得及NEW出来),这时就会新建两个。
try{
Thread.sleep(1); //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new Mgr03_02();
}
return INSTANCE;
}
//但是仍然有问题——锁的粒度太粗了~ ~ ~ ! ! !
【继续修改】:
public static Mgr03_03 getInstance(){
//业务代码
if (INSTANCE == null){ //两个线程接连判断都为NULL(还没来得及NEW出来),这时就会新建两个。
//妄图通过缩减同步代码块的方式提高效率,然后不可行。
synchronized(Mgr03_03.class) {
try {
Thread.sleep(1); //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03_03();
}
}
return INSTANCE;
}
//还是无法保证一致性~~~!!!!!!前一个线程刚释放锁,后一个线程就拿到锁了,然后就立即NEW了一个对象。
【DCL机制】:
package T04_YouXuXing.T05_singleton;
// DCL Double Check Lock
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06(){}
public static Mgr06 getInstance(){
//业务代码省略
if (INSTANCE == null){ //Double Check Lock
synchronized(Mgr06.class) {
if (INSTANCE==null) {
try {
Thread.sleep(1); //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m(){
System.out.println(" m ");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(
()-> System.out.println( Mgr06.getInstance().hashCode() )
).start();
}
}
}
【提高效率的细节】:
if (INSTANCE == null){ //Double Check Lock——————这一行可以提高效率
synchronized(Mgr06.class) {
if (INSTANCE==null) {
第一个判断INSTANCE是否==null 是非常有必要的,如果去掉的话,那么所有的线程二话不说一上来全都在抢锁,竞争锁消耗的资源是非常高的。
【DCL和volatile的问题】:
【结论】:
DCL必须要加上volatile
private static volatile Mgr06 INSTANCE; //JIT
【Q】:不加的话会怎么样呢???
//第一个线程进来,刚刚完成半初始化,这个时候发生了指令重排序。
//刚执行完连接的时候,外面就有一个线程进入了IF判断!!!(这个时候虽然没有完成初始化,但是已经赋上了默认值,外面的线程就会拿到半初始化状态的对象!!!)
【总结】:
thread1刚刚执行完连接那一步,这个时候thread2进入了IF判断,拿到刚半初始化完(没有完全初始化,没有调用构造方法)的对象。
【3–对象在内存中的存储布局(对象与数组的存储不同)】:
普通对象
mrakword——标记字。
class pointer——当你new出一个对象来 , 你这个对象是属于哪一个class的呢? ? ?
padding——64位虚拟机的话,就是8字节对齐( 将前面三块markword、class pointer、instance data补到能被8整除的字节 ),就和用集装箱装东西一样,用8字节比较规整。
【UseCompressedClassPointers】:
使用压缩类指针;
【UseCompressedOops】:
OrdinaryObjectPointer
使用压缩的普通对象指针。
【类指针】:
//这个类指针默认是启动压缩的 , 压缩完是4byte 。
【普通对象指针】:
String s , s就是一个指针,默认这两个的压缩都是开启的。
【数组类型】:
//唯一区别是多了4字节的数组长度;
【4–对象头具体包括什么(markword classpointer)synchronized锁信息】:
对象头主要包括markword 和 class_pointer两部分,class pointer是默认开压缩的4个字节 , 如果不开压缩就是8个字节。
【 研究一个对象内存布局的工具 】:
org.openjdk.jol
全称——JavaObjectLayout
//parseInstance——解析这个对象 , 然后变成可以打印的。
Instance size:16 bytes ———一共有16个字节构成。
//指向Object.class
空间的丢失4个字节。空间丢失就是为了补齐的意思。
【多加一个变量然后进行测试】:
markword ——8 ;
classpointer——4;
instance data——4;
8+4+4=16 , 所以已经是8的倍数了 , 没有必要再去补齐。
//最后的4个字节是m变量。
【再进行一例测试】:
//最后补了4个字节。
【markword中主要包括什么呢 ? 】:
最主要的是包含锁信息 , 其他的都是次要的。
锁信息、GC信息、IdentityHashCode 。
【5–对象怎么定位?(直接 间接)】
句柄方式
直接指针
//定位就是指——如何通过 t 来找到 T 。
【6–对象怎么分配?(栈上—线程本地—Eden—Old )】:
首先会尝试在栈上分配 , 如果能在栈上分配就分配在栈上;分配在栈上的好处就是——一旦弹出,它的生命周期就结束了。
【 什么样的对象能够在栈上分配呢 ? 】:
逃逸分析;
标量替换;
如果个儿超级大——扔到老年代。
如果不大不小——往TLAB中分配。TLAB全称是——Thread Local Allocation Buffer( 线程本地分配缓冲区 ) , E代表伊甸区 ,AGE是年龄 。
【解释线程本地分配】:
当我们往一个内存区域里NEW对象的时候 , 总是要分配空间的。多个线程往同一个内存空间里分配对象的时候,必须要经过同步。有线程的协调,就会有效率上的损失。
【线程本地分配缓冲区】:
当一个线程启动的时候 , 为这个线程在伊甸区里分配一个小小的空间,这个空间是线程所独享的 , 如果线程NEW了任何对象,就往对应的空间里扔,往自己的兜里扔东西的话,就不需要进行争抢了。
【7–Object o = new Object( ) 在内存中占用多少字节? 】:
1)有没有压缩class pointer 。
2) 有没有压缩oops 。
3) 看内存是不是32G以下或者以上 。
【8–为什么hotspot不使用C++对象来代表Java对象?】:
因为C++中有一个 vtbl 的指针 , 它指向的是虚方法表 , 虚方法表是用来实现多态的。Java中的是oop-class二元机制。
【9–Class对象是在堆还是在方法区?】:
O.class是给反射用的;
OOM溢出实际上是方法区里溢出了:
【阶段小结】:
【happens—before原则】:
【CPU级别】:
只要你不影响单线程的一致性,指令随便换。
【JVM级别】:
对Java的哪些指令不可以互换做了一些规定。
【CPU用屏障指令阻止乱序】:
【CPU汇编指令一级】:
内存屏障
所谓的内存屏障就是一条特殊的指令 ,当看到这种指令的时候,前面的指令和后面的指令不可以换顺序。
内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行
intel : lfence(读) sfence(写) mfence(读和写)(CPU特有指令)
//我们的JVM并不是靠这种底层指令来实现的 , 它并没有去针对不同的CPU从而使用不同CPU的内存屏障指令,不是。
【JVM要求实现的四种屏障】:
【JVM层级的内存屏障】:
所有要求实现JVM的Java虚拟机 , 都应该实现自己的JVM级别的内存屏障 , 你的JVM实现应该有这四条的屏障,不论你底层采用什么汇编语言来实现。JVM层级必须得有能实现这四个屏障的效果。
Load叫 “读” , Store叫 “写”。
【LoadLoad】:
//两条读之令中间夹了一个指令——LoadLoad , 那么上面的Load指令就不能和下面的Load指令交换顺序。其他的三个屏障可以类比理解。
【用volatile禁止指令重排】:
//StoreStoreBarrier——在我写之前,以前所有的写指令必须先完成,别人写完我才能写。
后面还一个指令:
//StoreLoadBarrier——等我写完别人才能读。
LoadLoadBarrier——等我读完,别人才能读;
LoadStoreBarrier——等我读完,别人才能写。
【volatile在hotspot中的实现】:
volatile的底层实现
volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序
1: volatile i
2: ACC_VOLATILE
3: JVM的内存屏障
屏障两边的指令不可以重排!保障有序!
happends-before
as - if - serial
4:hotspot实现
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
orderaccess_linux_x86.inline.hpp
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"); //这条指令的核心是lock而非后面的add。
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。
lock指令是比较特殊的,后面必须要跟其他的指令——表示当我执行后面的指令的时候,对总线/缓存 进行锁定,后面的这个指令不能是空指令,不能是NOP , addl $0,0(%%rsp)即给寄存器加了一个0,相当于是空操作。
//两颗CPU访问同一块内存的话 , 我会将总线进行锁定或者我会把对应的缓存行进行锁定(一个叫总线锁 ,一个叫缓存锁)。