文章目录
什么是线程安全?
在单线程下安全,在多线程情况下不安全的事件。
为啥会出现线程不安全?
因为线程之间并发执行是无序的(抢占式执行)。
线程不安全的原因
1.多个线程修改同一个变量
2.抢占式执行
3.修改操作不是原子的(不可拆分,++操作就不是,对应单个cpu指令是原子的)
4.内存可见效
5.指令重排序
++操作的不安全问题!
class Count{
public int count=0;
public void add(){
count++;
}
public int getCount(){
return count;
}
}
public class Test10 {
public static void main(String[] args) throws InterruptedException {
Count count=new Count();
Thread t=new Thread(()->{
for(int i=0;i<50000;i++) {
count.add();
}
});
t.start();
for(int j=0;j<50000;j++){
count.add();
}
System.out.println(count.getCount());
}
}
上述结果不是确定的,为什么呢?
因为++操作在cpu上是分为三条指令,分别为:从count的值读到寄存器->在+1操作->最后再给到count;我们知道线程之间是并发的,假设当主线程在读操作(值读到寄存器),而t线程已经完成前两步操作;此时t返回给count,count变为了1,但是主线程后脚也完成了,它也是把count变为了1,这样看我们发现明明执行了两次却只有一次的效果,但是你不要想那最少可能执行一次最少count结果也是50000,这样的想法是不对的,我们举个例子当我们主线程正在执行第一次读操作由于特殊原因主线程在阻塞),而线程t已经执行了50000次完整三次操作,此时到最后也只是1(停止阻塞了,主线程继续执行,在他的视角此时count还是0呢,所以最后为1)。
加锁与解锁
锁:能起到保证“原子性”的效果,锁的核心操作有两个,加锁和解锁,一旦某个线程加锁以后,其他线程也想加锁,就不能直接加上需要阻塞等待,等到加锁的线程释放锁(解锁)之后才能拿到锁进行上锁操作。
但是假如1号线程拿到锁后,2,3号线程都在阻塞等待,此时谁能拿到锁不确定;这时就发生抢占式执行。
如何加锁?
使用synchronized关键字进行加锁操作,执行完后自动解锁。
加锁与解锁的过程
加锁时:清空工作内存,读取主内存最新值。
解锁时:把共享变量最新值,刷新到主内存。
public void add(){
synchronized (this){//锁对象,在针对哪个对象加锁,如果两个线程,针对同一个对象加锁,此时会发生“锁竞争”,参数写什么都行(Object实例,内置类型不行),就相当于是一个吉祥物。
count++;
}
}
//此时就保证++操作是原子的了,此时我们结果就是100000
synchronized public void add(){//此时默认以this为锁对象
count++;
}
synchronized public static void test(){ //此时修饰静态方法,此时不是给this加锁,而是给类对象加锁
;
}
public static void test(){
synchronized (Count.class){//类对象加锁
;
}
}
Join和加锁的区别
join:是让两个线程完整的进行串行
加锁:两个线程某个小部分串行,大部分都是并发的。
什么是静态方法,什么是类对象
静态方法:属于类的,不依靠实例就可以调用,不能调用其他非静态方法,不能使用this,super关键字。
类对象(对象的图纸):java源代码文件->.class文件,JVM就可以执行.class,JVM要想执行这个.class就要把文件内容读到内存中(类加载),类对象,就可以表示这个.class文件的内容,描述了类的详细属性:类名字、属性、方法、继承哪个类、实现哪个接口。
内存可见性
内存可见性:内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。当一个多线程环境下,编译器对于代码优化,产生误判,从而引起bug。
此时从内存读取数据到cpu寄存器(flag),cmp比较寄存器里的值是否是0;
编译器就发现每次flag结果都一样,它就做了优化把每次读操作优化掉了,只执行一次读操作(相当于重复利用第一次读取的值),后续循环中只有cmp判断。
编译器优化:保证程序结果不变的前提下;单线程下非常准确,多线程下就不一定了。
可见性问题的解决方案
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。
这里有两种方案:加锁 和 使用 volatile 关键字
Volatile
被volatile修饰的变量,编译器就禁止上述优化,保证每次都是从内存重新读取数据。
volatile不保证“原子性” ,保证“内存可见性”
volatile 适用一个线程读,一个线程写的情况
synchronized 则是多个线程写
指令重排序
volatile 还有一个效果,禁止指令重排序,指令重排序,也是编译器优化的策略,调整代码执行的顺序,前提保证逻辑不变。
这个场景加锁和volatile都能解决。