线程安全
1. Java内存模型
Java虚拟机规范中试图定义一种Java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
java内存模型的主要目标是定义程序中各个变量的访问规则,就是在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。
JMM模型图:
每个线程都有自己的工作内存,而JMM规定了所有的变量都存储在主内存中,线程只能使用主内存中变量的副本。
多线程在JMM中的执行过程是:线程的工作内存中存储了被该线程使用到的变量的主内存副本,线程对在工作内存中对变量进行操作(读写),然后将更改后的变量值同步到主内存中。
线程之间无法直接访问对方工作内存中的变量,(也就是不能获悉其他线程对变量操作完最后得到的结果),线程变量值的传递需要通过主内存来完成。
2. JMM中的线程不安全:
根据以上的线程在JMM中的执行过程可以发现,当多个线程并发时会因为线程之间不能互相访问工作内存变量而导致数据被破坏的情况上面我们说到,主内存是共享的,所有线程都可以访问;而线程之间工作内存是不可见的,线程间的变量值传要通过主内存来完成。举个例子说,A,B两个线程都去主内存拿了变量m=1,A线程读了变量后执行了m++,此时A线程的工作内存中m为2,但是A线程还没有把m=2同步到主内存让其他线程更新m的值,B线程就对m进行了m++的操作,那么B线程的工作内存中m=2,两个线程执行完之后输出m的值为2;但这并不符合预期,两个线程操作的是共享变量,结果应该是3才对吧(同一个变量执行了两次加法操作)。这就是数据被破坏了,也因此引出了线程不安全问题。
JMM是围绕着在并发过程中如何处理原子性,可见性和有序性这三个特征来建立的。而原子性,可见性和有序性就是保证线程安全的必备条件。如何保证线程安全也是从这三个点考虑。
2.1 原子性:
对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。
2.2 可见性:
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。仍然是通过共享内存来同步变量值,线程工作内存之间依然不能互相访问。
2.3 有序性:
程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。(线程内表现为串行,在一个线程中观察另一个线程,所有的操作都是无序的(指令重排,工作内存和主内存同步延迟))
3. 什么是线程安全
3.1 线程安全定义:
一个对象可以安全的被多个线程同时使用那它就是线程安全的。
3.2 线程安全性的分类
3.2.1 不可变:
不可变对象一定是线程安全的。不可变对象被正确构建出来,其外部的可见状态永远都不会改变,也不会看到它在多个线程中处于不一致的状态。
怎样才能保证不可变: 如果共享数据是一个基本数据类型,那在定义时用final关键字修饰;如果共享数据是一个对象,那就要保证对象的行为对其状态不会产生任何影响。例如不可变对象String,当我们调用它的substring(),replace()和contact()方法时都不会影响它原来的值而是返回一个新构造的字符串,对象的行为没有影响到它的状态。
保证对象行为不影响状态的方法有很多,最简单的是把对象中带有状态的变量都声明为final。
java 中符合不可变要求的类型有,String,枚举,Number的部分子类,Integer,BigInteger等。
3.2.2 绝对线程安全:
线程安全的代码本身封装了必要的正确性保障手段(互斥同步),当多线程访问一个对象时,不用考虑这些线程在运行环境下的调度和交替执行,无需关心多线程并发可能带来的对数据的结果的影响,也无需自己采取任何措施来保证线程的正确调用(额外的同步,或在调用方进行协调操作),调用这个对象的行为都可以获得正确的结果。
3.2.3 相对线程安全:
通常意义上的线程安全,对对象的单独操作是线程安全的,在调用时不需要额外的保障措施,但对于特定顺序的连续调用,可能需要在调用方使用额外的同步手段,协调操作来保证调用的正确性。
3.2.4 线程兼容:
对象本身不是线程安全的,但可以通过在调用端使用同步手段来保证对象在并发环境中可以安全使用。
3.2.5 线程对立:
无论调用端是否采取了同步措施,都无法在多线程环境中并发使用时保证安全的类。
4. 如何实现线程安全
从JMM中我们知道,线程安全问题是多个线程同时操作共享数据而产生的。如果只对这些共享数据进行读操作,是不是就是线程安全的呢。
实现线程安全包括:代码编写如何实现线程安全和虚拟机如何实现同步与锁两方面。
4.1 代码编写如何实现线程安全
个人理解代码编写实现线程安全就是给代码添加额外的保障措施来保证在JMM内存模型中线程执行时的原子性,可见性和有序性。Java提供了一些关键字来达到目的。
4.1 volatile关键字:
被volatile修饰的变量能保证可见性和禁止指令重排,但不能保证原子性;
4.1.1 可见性例子:
public class VolatileTest2 {
volatile int num = 0;
public void addNum(){
this.num = 1;
}
public class Test1 extends Thread{
@Override
public void run(){
System.out.println("working");
while (num == 0){
}
System.out.println("end");
}
}
public class Test2 extends Thread{
@Override
public void run(){
addNum();
}
}
public static void main(String[] args){
VolatileTest2 volatileTest2 = new VolatileTest2();
try {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("working");
while (volatileTest2.num == 0){
}
System.out.println("end");
}