想给出一个线程安全的定义是比较复杂的,但是我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境该有的结果,则说明这个程序是线程安全的。
示例:
/**
* 线程不安全示例
*/
public class Exe_01 {
private static int num=50000;
public static void main(String[] args) throws InterruptedException {
//new对象
Counter counter=new Counter();
//创建两个线程
Thread t1=new Thread(() ->{
for (int i = 0; i < num; i++) {
//自增操作
counter.add();
}
});
Thread t2=new Thread(() ->{
for (int i = 0; i < num; i++) {
//自增操作
counter.add();
}
});
//启动线程
t1.start();
t2.start();
//等待线程结束
t1.join();
t2.join();
//获取自增结果
System.out.println("自增结果"+counter.count);
}
}
class Counter{
int count=0;
public void add(){
count++;
}
}
上述就是线程不安全的现象
1、线程的抢占式执行(无法避免)
会出现线程的执行顺序并不是我们代码里期望的顺序,抢占式执行是造成线程不安全的主要原因,这是我们无法解决的,完全是由CPU随机调度。和机器配置也有关系,CPU核心数不同。
2、多个线程修改了同一个变量
多个线程修改不同的变量,不会出现线程安全的问题。
一个线程修改读取或修改同一个变量,不会出现线程安全问题。
多个线程修改了同一个变量,就会出现上面的运行结果,出现相同情况,出现问题。
3、原子性
要么全都执行,要么全都不执行
上述代码中的count++,对应的是多条CPU指令
*从内存或寄存器中把count值读出来 --LOAD;
*执行自增操作 --ADD;
*把计算结果写回内存或寄存器 --STORE;
像上述的count++操作,本质上就是一个“非原子性”的操作,这种操作就会导致线程不安全的现象,而原子性操作就不会产生“线程不安全”的困扰。
4、内存可见性
JMM(Java Memory Modle) Java内存模型
每一个工作内存都是独立的,相互之间不可以访问。内存可见性就是要保证一个线程改变了一个变量的值可以让其他线程感知到。
上面的例子中,由于没有对count++做任何处理,所以它不能在多个线程之间实现内存可见性。
java内存模型(JMM):
java虚拟机规范中定义了java的内存模型。目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台都能达到一致的并发效果。
1、所有线程不能直接修改主存中的共享变量。
2、如果要修改内存中的变量,那么就需要把这个变量从主内存中复制到自己的工作内存,修改完成后,再刷新回主内存。
3、各个线程之间不能相互通信,做到内存隔离级别的线程隔离。
4、通过共享内存实现线程间的相互通信,就叫做内存的可见性。
5、指令重排序
发生指令重排序的条件:
1、结果必须正确。
2、重排序的操作逻辑上互不影响
造成线程不安全的原因:
1、线程的抢占式执行
2、多个线程修改了同一个变量
3、原子性
4、有序性
5、内存可见性
1、线程的抢占式执行
CPU调度方式问题,硬件层面,解决不了。
2、多个线程修改了同一个变量
并发编程,目的就是要提高代码运行效率,需求势必是存在的而且是需要满足。
3、原子性
指令在CPU上执行的,怎么才能让CPU实现原子性,是可以解决的。
4、有序性
Java中编译器开发大佬们,给我们提供了一些关键字来修饰代码或变量,让它们不执行指令重排序。
5、内存可见性
通过分析,进程之间内存也存在隔离的,而且存在内存间通信的机制,那么线程之间也有这种机制。
通过以上分析只要满足条件3,4,5中的一两条就可以避免了线程不安全的问题,而3,4,5又正式JMM的主要特性,所以可以通过JAVA层面手段解决线程安全问题。
通过加锁的方式来满足3,4,5中的原子性
示例:
public class Exe_02 {
private static int num=50000;
public static void main(String[] args) throws InterruptedException {
Counter01 counter01=new Counter01();
Thread t1=new Thread(() ->{
for (int i = 0; i < num; i++) {
counter01.add();
}
});
Thread t2=new Thread(() ->{
for (int i = 0; i < num; i++) {
counter01.add();
}
});
//启动线程
t1.start();
t2.start();
//等待线程结束
t1.join();
t2.join();
//打印预期结果10w
System.out.println("count结果:"+counter01.count);
}
}
class Counter01{
public int count=0;
public synchronized void add(){
count++;
}
}
*synchronized保证原子性
*sychronized不保证有序性(不会禁止指令重排序)
*sychronized保证内存可见性
保证原子性不是说所有的指令不执行完不释放锁,并不是说所有指令必须一次性执行完,在执行的过程中也有可能被PCU调度走,但是在CPU调度的时间内,并不释放锁,所有来获取锁的其它线程的时间内,依然是被锁定状态。