目录
1 .线程安全则主要体现在三个方面:
1.1 原子性
1.1.1 Java 实现原子性的两种方式:
1.2 可见性
1.3有序性
1.3.1 重排序
1.3.2 指令重排序
1.3.3 存储子系统重排序
1.3.4 貌似串型语义
1.3.5 保证内存的访问顺序
接下来就对这三个特性进一步说明:
- 锁的缺点:
- 多线程编程中 我们⼀般会使⽤各种锁来确保对共享资源的访问和操作,需要共享的数据要对它的访问串⾏化。
修改共享数据的操作必须以原⼦操作的形式出现才能保证不被其它线程破坏相应数据。
锁这种机制⽆法彻底避免以下⼏点问题:
1、锁引起线程的阻塞,对于没有能占⽤到锁的线程或者进程将会⼀直等待锁的占有者释放资源后才能继续。
2、申请和释放锁的操作增加了很多访问共享资源的消耗。
3、锁不能很好的避免编程开发者设计实现的程序出现死锁或者活锁可能
4、优先级反转和所护送怪现象
5、难以调试
在多线程环境中,一个线程对某一个共享变量进行更新之后,后续其它线程可能无法立即读到这个更新的结果,这就是线程安全中的另一种形式:可见性(visibility)。
package stu.my.cdn.threadsafe;
/**
* 测试线程的可见性
*/
public class Test02 {
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
new Thread(myTask).start();
Thread.sleep(6000);
// 主线程 1 秒后取消执行
myTask.cancel();
/*
可能会出现以下情况:
在 main 线程中调用了 myTask.cancel() 方法,把myTask的 toCancel 修改为 true
可能存在子线程看不到 main 线程对 toCancel做的修改,在子线程中 toCancel 变量的值一直为 false
导致子线程看不到 main 线程对 toCancel 变量更新的原因可能为:
1)JIT即时编译器,可能会对 run 方法中的 while循环进行优化为:
if(!toCancel){
while(true){
if(doSomething())
}
}
2) 可能与计算机的存储系统有关,假设分别有两个 cpu 内核运行 main 线程与子线程,
一个 cpu 内核无法立即读取另一个cpu 内核中的数据。(运行在子线程的cpu无法立即读取
main 线程cpu的数据)
*/
}
static class MyTask implements Runnable{
private boolean toCancel = false;
@Override
public void run() {
while(!toCancel){
if(doSomething()) break;
}
if(toCancel){
System.out.println("任务被取消");
}else{
System.out.println("任务正常执行");
}
}
private boolean doSomething() {
System.out.println("执行了某个任务");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
}
public void cancel(){
toCancel = true;
System.out.println("收到 取消线程的消息");
}
}
}
总的来说,可见性就是,一个线程修改了数据,能让其共享的线程及时的到新数据。
Java 代码的执行过程中,首先进行前端编译,通过编译器将 .java文件 编译成字节码,即 .class文件。
然后,进行后端编译,将字节码编译成机器码,在此过程中,有一个编译器将热点代码编译成本地平台相关的机器码,并进行优化。javac: javac的任务是将Java源代码语言 (.java文件) 先转化成JVM能够识别的一种语言,然后由JVM将JVM语言再转化成当前这个机器能够识别的机器语言,javac的作用简单来说就是通过一些列的流程之后将.java文件转换为.class文件, javac 编译器位于jdk --> bin -->javac
在 Java 中提到“编译”,自然很容易想到 javac 编译器将
*.java
文件编译成为*.class
文件的过程,这里的 javac 编译器称为前端编译器,其他的前端编译器还有诸如 Eclipse JDT 中的增量式编译器 ECJ 等。相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的 Java 程序在运行时基本都是解释执行加编译执行),如 HotSpot 虚拟机自带的 JIT(Just In Time Compiler)编译器(分 Client 端和 Server 端)。另外,有时候还有可能会碰到静态提前编译器(AOT,Ahead Of Time Compiler)直接把*.java
文件编译成本地机器代码,如 GCJ、Excelsior JET 等,这类编译器我们应该比较少遇到。
在源码顺序与程序顺序不一致,或者 程序顺序与执行顺序不一致时的情况下,我们就说发生了指令重排序。
指令重排序是一种动作,确实对指令的顺序做了调整,重排序的对象指令。
javac
编译器一般不会执行指令重排序,而JIT
编译器可能执行指令重排序。处理器也可以执行指令重排序,使得执行顺序与程序顺序不一致。
指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预测的结果。
存储子系统是指写缓冲器与高速缓存:
高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配设计的一个高速缓冲
写缓冲器(Store buffer,Write buffer)用来提高写高速缓冲操作的效率。
即使处理器严格按照程序执行两个内存访问操作,在存储子系统的作用下,其它处理器对这两个操作的感知顺序和程序顺序一致,即使这两个操作的顺序看起来像是发生了变化,这种现象称为存储子系统重排序。
存储子系统重排序并没有真正的对程序执行顺序进行了调整,而是造成一种指令执行顺序被调整的现象。
存储子系统重排序对象是内存操作的结果。
JIT 编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序,给单线程程序造成一种假象----------指令是按照源码的顺序执行的,这种假象称为貌似串型语义。并不能保证多线程环境程序的正确性。
为了保证貌似串型语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的数据才会被重排序。如果两个操作(指令)访问同一个变量,其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data dependency)
如:
x=1; y=x+1; 后一条语句的操作数包含前一条语句的执行结果;
y = x; x = 1 ; 先读取x变量,在更新 x 变量的值。
x = 1; x = 2; 两条语句同时对一个变量进行写操作。
如果不存在数据依赖关系则可能重排序
如:
double price = 45.8;
int quantity = 10;
double sum = price * quantity;
存在控制依赖关系的数据允许重排。一条语句(指令)的执行结果会决定另一条语句(指令)能否执行,这两条数据(指令)存在控制依赖关系(Control Dependency)。如在 if 语句中允许重排,可能存在处理器先执行 if 代码块,在判断 if 条件是否成立。
两者比较:
volatile与sychronized的比较:
1)volatile只能修饰变量,sychronized可以修饰变量、方法,以及代码块;
2)多线程访问volatile修饰的变量时不会发生阻塞,而访问sychronized修饰的内容时,线程会发生阻塞;
3)volatile只能保证可见性,而sychronized保证了原子性,间接保证了可见性,因为它会将本地缓存和主内存的数据同步。
感谢阅读~~~希望对你有帮助~~~