各种需要考虑线程安全的情况
1.访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
2.所有依赖时序的操作,即使每一 步操作都是线程安全的,还是存在并发
3.不同的数据之间存在捆绑关系的时候
4.我们使用其他类的时候,如果对方没有声明自己是线程安全的
为什么多线程会带来性能问题
1.上下文切换
2.线程间的协作内存同步
jvm内存结构:
字节码文件,通过类加载器,会在灰色区域(jvm分配的5个区域)诞生出他所需要的很多东西,绿色部分是所以线程所共享的,黄色是线程私有的。
堆是占用内存最多的区域,它里面包含着所有new或者其他指令创建出来的实例对象,也包括数组,数组也是对象,如果对象不再引用会被垃圾回收。
方法区主要加载的是static静态变量,一些类信息,常量池,以及一些永久引用
虚拟机栈也叫java栈里面保存着基本数据类型,以及对象的引用,特点编译的时候就确定了大小,运行时刻不会改变。
本地方法栈保存的是一些native方法
程序计数器离我们较远,主要保存当前线程行号数,下调指令分支循环等
什么是java对象模型
翻译成文字就是,java对象模式要表达的意思就是这个对象的存储结构一个对象是如何在jvm中存储的,如上图,它会首先创建一个instanceClass,放在方法区中,当实例化的时候,会在栈中保存这个类的引用,会在堆中新建实例对象,每个对象包含对象头和实例数据两个部分。
什么是java内存模型
Java内存模型大致分为主内存和工作内存,而这个工作内存呢是为了屏蔽存储设备与处理器的运算能力之间有几个数量级的差距,从而引入的尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
而java内存模型的目的是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
什么是重排序:
重排序就是代码的执行顺序发生了改变。
如上图a =3 b =2 a = a+1,原本的三步操作,会被cpu或者编译器优化成右边的两步省了a的读取和写入,提升了效率,但是代码的顺序发生了改变
public class Test10 {
static int a = 0,b = 0,x = 0,y = 0;
public static void main(String[] args) {
int i = 0;
// CountDownLatch latch = new CountDownLatch(1);
for(;;) {
i++;
//保证每次for循环值都是从0 开始
a = 0;
b = 0;
x = 0;
y = 0;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
//为什么要休眠呢,模拟两个线程并发情况下同时进入下面的代码
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(10);
//为什么要休眠呢,模拟两个线程并发情况下同时进入下面的代码
// latch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
b = 1;
y = a;
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
String result = "第"+i+"次("+x+","+y+")";
if(x == 0 && y == 0) { //(x == 0 && y==0) (x == 1 && y==0) (x == 0 && y==1) (x == 1 && y==1)
System.out.println(result);
break;
}else {
System.out.println(result);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
从以上代码可以得出 x y 会得到4种不同情况的值分别是
1.(0,1) 2.(1,0) 3.(1,1) 4.(0,0)
出现第一种情况是 线程一先执行完 x = b 此时b为0 所以 x = 0 再执行线程二 y = a a已经被线程一赋值为1
出现第二种情况是 与第一种相反。
出现第三种情况是 线程一 和线程二休眠结束 同时进入,可以是线程1先将 b 赋值为1 同时 线程2又将a赋值为1,所以此时 x y 都为 1.
那么为什么会出现第4中情况 都为 0呢 ,这应该是不可能呀。。,这就涉及到指令重排序了。
出现 0,0情况 是因为先执行了 y = a; a=1; x=b,b=1 ,代码顺序发生了变化。
可见习问题演示
public class Test11 {
int a = 1;
int b = 2;
public void set() {
a = 3;
b = a;
}
public static void main(String[] args) {
while(true) {
Test11 test11 = new Test11();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test11.set();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test11.print();
}
}).start();
}
}
public void print() {
System.out.println("a:"+a+" b:"+b);
}
}
如上面代码在多线程环境下会出现 4种情况分别是:
1.a =1,b =2
2.a =3,b = 3
3.a = 3,b =2
4.a = 1 ,b = 3
前三种都是比较好理解的,出现最后一种概率比较低我打印了几万次也就出现了两次,这种就是可见性问题导致的,而解决这个问题很简单,只需要a,b前面加上voliate关键字即可
参考 java内存模型