Java创建对象过程
class T{
int m=8;
}
T t=new T();
编译成汇编指令:
new #2 <T>
dup
invokespecial #3 <T.<init>>
astore_1
return
当执行到new的时候,程序会申请一块内存,由于成员变量中有一个int的m所以在该内存中有一个变量m而且初始化为0。
当执行到invokespecial的时候会调用构造函数把内存中的m的值该为8,执行到astore_1的时候会把变量t和new申请到的内存建立关联
案例3
private int num=8;
public T01(){
new Thread(()->System.out.println(this.num)).start();
}
public static void main(String[] args) throws IOException {
new T01();
System.in.read();
}
在main线程中new T01();的时候会有一个局部变量this和new申请的内存空间建立关联,当 new T01();编译成汇编指令执行到invokespecial的时候就会开启一个线程并执行。
这是this指向的是new刚申请的内存空间中的num,而刚申请的内存空间中的num的初始化值为0,那么打印出来的值可能是0。
即当invokespecial指令和astore_1指令执行顺序发送乱序的是就会出现this溢出问题,所以建议在构造函数中创建线程但不能在构造函数中开启该线程。
案例4
本案例中使用单例模式开始理解DCL(Double Check Lock),为了方便理解先从简单案例开始不断的优化
public class T01 {
private static final T01 INSTANCE=new T01();
private T01(){}
public static T01 getInstance(){
return INSTANCE;
}
public static void main(String[] args){
T01 t1=T01.getInstance();
T01 t2=T01.getInstance();
System.out.println(t1==t2);
}
}
存在的问题:不管该对象你有没有使用,它都存在于内存空间中。即占用内存空间
优化(判断对象的引用是否为空):
public class T01 {
private static T01 INSTANCE=new T01();
private T01(){}
public static T01 getInstance(){
if(INSTANCE==null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new T01();
}
return INSTANCE;
}
public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(T01.getInstance().hashCode());
}
}).start();
}
}
}
存在问题:当该程序处于多线程环境中的时候就会导致你获取到的对象不是同一个
优化(给getInstance函数使用对象锁):
public class T01 {
private static T01 INSTANCE=new T01();
private T01(){}
public static synchronized T01 getInstance(){
if(INSTANCE==null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new T01();
}
return INSTANCE;
}
public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(T01.getInstance().hashCode());
}
}).start();
}
}
}
存在问题:由于在gatInstance函数中添加了对象锁,那么只能等到该线程执行完getInstance函数的时候,第二个线程才能调用该函数。
这样子可以保证了并发编程的原子性,但是如果是getInstance函数体中的业务逻辑代码过长那么就会导致第二个线程等待时间过长(即该对象锁的粒度太大)
优化(判断对象的引用是否为空后给本类添加对象锁):
public class T01 {
private static T01 INSTANCE=new T01();
private T01(){}
public static T01 getInstance(){
//业务代码
//.......
if(INSTANCE==null){
synchronized (T01.class){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new T01();
}
}
return INSTANCE;
}
public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(T01.getInstance().hashCode());
}
}).start();
}
}
}
存在问题:在多线程并发环境中不能保证getInstance函数获取到的对象都是同一个
优化(双重检查锁(Double Check Lock)):
public class T01 {
private static T01 INSTANCE=new T01();
private T01(){}
public static T01 getInstance(){
//业务代码
//.......
if(INSTANCE==null){
synchronized (T01.class){
if(INSTANCE==null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new T01();
}
}
}
return INSTANCE;
}
public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(T01.getInstance().hashCode());
}
}).start();
}
}
}
1、给本类添加对象锁前后两次判断INSTANCE是否为空,第一层判断是为了提高效率,即当有一个线程new出来对象后第二个线程就不用竞争第一个线程的对象锁。
2、结合前面几个案例,由于Java创建对象过程中会成以下5条汇编指令
new #2 <T>
dup
invokespecial #3 <T.<init>>
astore_1
return
DCL存在的问题:当invokespecial和astore_1发送指令重排序的时候就会导致在没调用本类中的构造函数时就会将一个变量和new申请的内存空间给绑定。
由于new申请的内存空间默认给本类中的成员变量初始化为0或者null。则此时通过getInstance函数获取的对象就会无法获取真正的数据。
并发编程中阻止指令重排序
1、并发编程中阻止指令重排序分CPU层面和JVM层面来解决这个问题。
2、指令重排序的原理:以我们在公共厕所排队等待为例,当你需要上厕所的时候刚好公共厕所中的所有厕所都有人了,你只能等到其中一个人上完厕所后你才可以使用该厕所。问题就在于只要有一个人上完厕所这个条件满足就可以了,不需要关心到底是哪个厕所,指令重排序的原理与类似。
3、那么为了让指定的人去使用指定的厕所那么可以在这些厕所之间加个屏障就可以了
CPU层面
内存屏障是一个特殊指令,当看到这种指令的时候,前面的必须执行完,后面的才能执行
Intel:ifence sfence mfence(CPU特有指令)
JVM层面
1、所有实现JVM规范的虚拟机,必须实现四个屏障:LoadLoad LoadStore StoreLoad StoreStore(Load可以理解为读,Store可以理解为写)
LoadLoad屏障:对于这样的语句load1;LoadLoad;Load2,在Load2及后续读取操作的数据被访问前,保证Load1要读取的数据必须读取完毕
StoreStore屏障:对于这样的语句Store1,StoreStore,Store2,在Store2及后续写入操作执行前;保证Store1的写入操作对其他处理器可见
StoreLoad屏障:对于这样的语句Load1;LoadStore;Store2;在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕
StoreLoad屏障:对于这样的语句Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store的写入对所有处理器可见
2、volatile修饰的内容,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序
volatile实现原理理解:
1、当对Volatie修饰的变量写入的时候,在JVM中会在该变量前后添加StoreStore、StoreLoad。即当的对该变量进行写入的时候前面所有的写入操作必须完成,才能对该变量进行写入。后面的所有读操作要等对该变量写入完成后才能开始读
2、当对Volatile修饰的变量读取的时候,在JVM中会在该变量前后添加LoadLoad、LoadStore。即当对该变量进行读取的时候,我必须读取完别人才能读或者写