Java 多线程基础(四)线程安全

Java 多线程基础(四)线程安全

在多线程环境下,如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 在了解线程安全之前,先来说一下Java的内存模型 JMM ,先了解多线程是如何工作的。

一、JMM(Java Memory Model)

JMM是一种基于计算机内存模型(定义了共享内存系统中多线程程序读写操作行为的规范),屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性。

(一)、多线程的执行环境

Java 多线程基础(四)线程安全

上图描述了一个多线程执行场景。线程 A 和线程 B 分别对主内存的变量进行读写操作。其中主内存中的变量为共享变量,也就是说此变量只此一份,多个线程间共享。但是线程不能直接读写主内存的共享变量,每个线程都有自己的工作内存,线程需要读写主内存的共享变量时需要先将该变量拷贝一份副本到自己的工作内存,然后在自己的工作内存中对该变量进行所有操作,线程工作内存对变量副本完成操作之后需要将结果同步至主内存。

线程的工作内存是线程私有内存,线程间无法互相访问对方的工作内存。

(二)、多线程执行的流程

Java 多线程基础(四)线程安全

从上图可以看出,线程A执行完x++后,将数据同步到主内存中,线程B再访问时,是线程A同步后的数据,所以保证了线程安全。那么,线程工作内存怎么知道什么时候又是怎样将数据同步到主内存呢?这是因为有了 JMM。JMM 规定了何时以及如何做线程工作内存与主内存之间的数据同步。

二、多线程中的核心概念

(一)、原子性:对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。

(二)、可见性:多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。

(三)、有序性:程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。

三、线程安全

从上面可以知道,主内存中的变量是共享的,所有线程都可以访问读写,而线程工作内存又是线程私有的,线程间不可互相访问。那在多线程场景下,图上的线程 A 和线程 B 同时来操做共享内存里的同一个变量,那么主内存内的此变量数据就会被破坏。也就是说主内存内的此变量不是线程安全的。我们来看个代码帮助理解。

public class ThreadDemo {
private int x = 0;
public void count() {
x++;
}
public void test() {
// 线程A
new Thread(() ->{
for (int i = 0; i < 1000000; i++) {
count();
}
System.out.println(Thread.currentThread().getName() + " : " + x);
}).start();
// 线程B
new Thread(() ->{
for (int i = 0; i < 1000000; i++) {
count();
}
System.out.println(Thread.currentThread().getName() + " : " + x);
}).start(); }
public static void main(String[] args) {
new ThreadDemo().test();
}
}
// 输出结果
Thread-0 : 1033950
Thread-1 : 1944281

上面代码开启两个线程,分别调用 count() 执行 x++ 的操作,理论上,两个线程执行完成后,应该有一个线程会输出 x = 2000000,但是从结果上看,和预期的结果不同,出现这样的结果的原因也就是我们上面所说的,在多线程环境下,我们主内存的 x 变量的数据被破坏了。执行流程如下图:

Java 多线程基础(四)线程安全

从上图可以看到,线程A和线程B共同访问同一资源 x , 其中线程A在对资源进行写操作中途,线程B对这个线程A写了一半的资源进行读写操作,导致资源数据 x 出现了错误。为了避免出现这种情况,Java 提供了线程同步的方法来解决这个问题。线程同步内容可以参考:Java 多线程基础(五)线程同步

四、总结

(一)、出现线程安全问题的原因

在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写入已经开始,但还没 结束),其他线程对这个写了一半的资源进行了读操作,或者对这个写了一半的资源进行了写操作,导致此资源出现数据错误。

(二)、如何避免线程安全问题?

1、保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性)。

2、将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。

上一篇:Linux下的C Socket编程 -- 获取对方IP地址


下一篇:MRC的下setter访问器的两种形式