线程安全性

什么是线程安全性

《Java Concurrency In Practice》一书的作者 Brian Goetz 是这样描述“线程安全”的:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。

在这定义中,最核心的概念是“正确性”。

在计算机世界中,在一段程序工作进行期间,会被不停的中断和切换,对象的属性(数据)可能会在中断期间被修改和变脏。

在 Java 语言中,线程安全性的问题限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说都是完全没有区别的。

如果每个线程中对共享数据(如全局变量、静态变量)只有读操作,而无写操作,一般来说这种共享数据是线程安全的,而如果存在多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

Java 中的线程安全

Brian Goetz 曾发表过一篇论文,他并没有将线程安全当做一个非真即假的概念,而是按照线程安全的“安全程度”由强至弱来排序,来将 java 语言中的各种操作共享的数据分为以下 5 类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

1.不可变

在 Java 语言中(特指 JDK 1.5 以后,即 Java 内存模型被修正之后的 Java 语言),不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要采取任何的线程安全保障措施。

在 Java 中,如果共享数据的数据类型不同,保证其不可变的方式也有所不同。

  • 共享数据是基本数据类型:这种情况只需要在定义时使用 final 关键字修饰它就可以保证它是不可变的。

  • 共享数据是一个对象:这种情况需要保证对象的行为不会对其状态产生影响。保证对象行为不会影响自己状态的途径有很多种:

    比如 String 对象,当我们调用 String 对象的 subString()、replace()等方法时都不会影响它原来的值,只会返回一个新构造的字符串对象。又或者我们可以直接将对象中所有的变量都声明为 final。

2.绝对线程安全

绝对线程安全即是完全满足 Brian Goetz 对线程安全的定义,这是个很严格的定义:一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”。

3.相对线程安全

相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,在调用时不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证正确性。Java 语言中的大部分线程安全类都属于这种类型,如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装集合等。

4. 线程兼容

线程兼容指的是对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。我们通常所说的一个类不是线程安全的,绝大多数时候指的是这种情况。Java API中的大部分类都是属于线程兼容的。比如集合类 ArrayList 和 HashMap 等。

5. 线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常是有害的,应当尽量避免。

非线程安全的影响

全局变量的非线程安全

在了解了什么是线程安全之后,我们来看一下在多线程环境下,对非线程安全的共享数据进行操作,会导致什么样的问题。

下面用经典的 Java 多线程模拟卖火车票的问题来进行说明:

public class TicketTest {

    public static void main(String[] args) {
TicketSaleRunnable runnable = new TicketSaleRunnable();
Thread t1 = new Thread(runnable, "1号窗口");
Thread t2 = new Thread(runnable, "2号窗口");
Thread t3 = new Thread(runnable, "3号窗口"); t1.start();
t2.start();
t3.start();
} } class TicketSaleRunnable implements Runnable { private int tickets = 10; //总票数10张 public void run() {
while(true) {
if(tickets > 0) {
tickets--;
Thread.yield(); //让出线程,增加出错几率
System.out.println(
Thread.currentThread().getName() + ",剩余票数:" + tickets);
}else {
break;
}
}
} }

输出结果:

1号窗口,剩余票数:9
3号窗口,剩余票数:7
2号窗口,剩余票数:7
1号窗口,剩余票数:6
2号窗口,剩余票数:4
3号窗口,剩余票数:4
1号窗口,剩余票数:3
3号窗口,剩余票数:1
2号窗口,剩余票数:1
1号窗口,剩余票数:0

可以看到当多个线程同时访问余票(全局变量)时,出现了线程不安全的问题,在不同的线程中输出了重复的结果。

下面我们再通过 ArrayList 和 Vector 来进一步分析一下非线程安全所带来的问题,以及产生的原因。

ArrayList 和 Vector 的线程安全性

不安全的 ArrayList

我们经常见到这样的面试题“ArrayList 和 Vector 的区别,HashMap 和 HashTable 的区别,StringBuilder 和 StringBuffer 的区别?”

答案在上文也有所提及:

ArrayList 是非线程安全的,Vector 是线程安全的;

HashMap 是非线程安全的,HashTable 是线程安全的;

StringBuilder 是非线程安全的,StringBuffer 是线程安全的。

下面通过一个示例来展示一下 ArrayList 非线程安全问题:

public class UnsafeTest {

    public static void main(String[] args) throws InterruptedException {
final List<Integer> list = new ArrayList<Integer>(); new Thread(new Runnable(){ public void run() {
for(int i = 0; i < 100; i++) {
list.add(i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} }).start(); new Thread(new Runnable() { public void run() {
for(int i = 100; i < 200; i++){
list.add(i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start(); Thread.sleep(10000); // 打印所有结果
for (int i = 0; i < list.size(); i++) {
System.out.println("第" + (i) + "号元素为:" + list.get(i));
}
}
}

运行结果:

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 109
at java.util.ArrayList.add(Unknown Source)
at selfprivate.UnsafeTest$2.run(UnsafeTest.java:32)
at java.lang.Thread.run(Unknown Source)

即便是我们多尝试几次,使得程序运行成功结束不抛出异常:

第0号元素为:0
第1号元素为:100
第2号元素为:1
···
第8号元素为:106
第9号元素为:6
第10号元素为:null
第11号元素为:107
第12号元素为:108
···
第185号元素为:197
第186号元素为:97
第187号元素为:98
第188号元素为:198
第189号元素为:199
第190号元素为:99

也经常会发现某些位置出现了 null 值的情况,并且 ArrayList 最终的 size 是小于 200 的。

从运行的结果来看,ArrayList 的确是非线程安全的,我们结合 ArrayList 的源码一起分析一下它的问题主要出在哪里:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//默认集合容量大小
private static final int DEFAULT_CAPACITY = 10;
//ArrayList内部维护的是一个数组来保存元素
transient Object[] elementData;
//elementData所存储的元素个数
private int size;
... public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断内部数组的容量是否足够,是否需要扩容
elementData[size++] = e; //将元素保存到数组中,并将 size 自增1
return true;
} }

1. 多个线程同时进行 add 操作可能会导致抛出数组越界 ArrayIndexOutOfBoundsException 的异常

当数组总容量为 10,且当前已保存了 9 个元素(即size=9)时,线程A 进入 add 方法,并调用ensureCapacityInternal方法判断了容量够用,不需要扩容。随后立即执行线程B 的add 方法开,也调用了ensureCapacityInternal判断了此时容量够用,不需要扩容,接着执行线程 A 的elementData[size++] = e 操作,size 变为 10,线程 B 也开始执行这个赋值操作,而 elementData[]数组的最大下标为9,则调用 elemenmt[10] = e,则就抛出了数组越界异常了。

2. ArrayList 集合中某些位置上的值出现了 null 的情况

3. 一个线程的值会覆盖掉另一个线程添加的值

这是因为赋值操作 element[size++] = e 并不是一个原子操作,它可以看成这样两步:

elementData[size] = e;
size = size + 1; //注意这一步也不是原子操作

当线程 A 执行了 elementData[size] = e 之后,即开始执行线程 B 的 elementData[size] = e 操作,此时这两个线程的 size 值都还没有增加,所以 线程 B 的值覆盖掉了 线程 A 的赋值。接着线程 A 执行 size 增加 1 的操作,线程 B 的 size 也加 1,这就导致了 size 一共增加了两次,这样就空出了一个位置,就导致某一位置的值为 null 的情况。

4. ArrayList 集合实际的 size 比期望的 size 值要小

这是因为源码中的递增操作 size++ 并非是原子操作,实际上它包含了三个独立的操作:读取 size 的值,将值加1,然后将计算结果写入 size。这在多线程环境就很容易导致 size 的计算出错。线程 A 读取了 size,在执行加1之前,线程 B 也读取了 size 的值,这两个线程获取的是同样的 size 值,然后这两个线程各自为 size 增加 1,将值写入 size 中,最终得到的 size 也只增加了一次,而不是两次。

安全的 Vector

现在我们把上面的例子中的 ArrayList

final List<Integer> list = new ArrayList<Integer>();

替换为 Vector

final List<Integer> list = new Vector<Integer>();

再次运行程序,输出结果:

第0号元素为:0
第1号元素为:100
第2号元素为:1
第3号元素为:101
第4号元素为:2
第5号元素为:102
···
第195号元素为:197
第196号元素为:198
第197号元素为:98
第198号元素为:199
第199号元素为:99

没有出现 null 值的情况,size 的值也与期望的一样是 200。

从结果来看 Vector 确实是线程安全的。那么Vector是如何保证线程安全的呢?

通过查看 Vector 的源码,可以看到它的 add 方法多了一个 synchronized 修饰符。

public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}

在下一篇文章我们将学习一下 synchronized 操作符的作用。

Java 多线程:什么是线程安全性的更多相关文章

  1. Java多线程编程(3)--线程安全性

    一.线程安全性   一般而言,如果一个类在单线程环境下能够运作正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运作正常,那么我们就称其是线程安全的.反之,如果一个类在单线程环境下运作 ...

  2. Java多线程系列--“JUC线程池”06之 Callable和Future

    概要 本章介绍线程池中的Callable和Future.Callable 和 Future 简介示例和源码分析(基于JDK1.7.0_40) 转载请注明出处:http://www.cnblogs.co ...

  3. Java多线程系列--“JUC线程池”02之 线程池原理(一)

    概要 在上一章"Java多线程系列--“JUC线程池”01之 线程池架构"中,我们了解了线程池的架构.线程池的实现类是ThreadPoolExecutor类.本章,我们通过分析Th ...

  4. Java多线程系列--“JUC线程池”03之 线程池原理(二)

    概要 在前面一章"Java多线程系列--“JUC线程池”02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包括:线程池示例参考代 ...

  5. Java多线程系列--“JUC线程池”04之 线程池原理(三)

    转载请注明出处:http://www.cnblogs.com/skywang12345/p/3509960.html 本章介绍线程池的生命周期.在"Java多线程系列--“基础篇”01之 基 ...

  6. Java多线程系列--“JUC线程池”05之 线程池原理(四)

    概要 本章介绍线程池的拒绝策略.内容包括:拒绝策略介绍拒绝策略对比和示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3512947.html 拒绝策略 ...

  7. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  8. 转:java多线程CountDownLatch及线程池ThreadPoolExecutor/ExecutorService使用示例

    java多线程CountDownLatch及线程池ThreadPoolExecutor/ExecutorService使用示例 1.CountDownLatch:一个同步工具类,它允许一个或多个线程一 ...

  9. Java多线程——进程和线程

    Java多线程——进程和线程 摘要:本文主要解释在Java这门编程语言中,什么是进程,什么是线程,以及二者之间的关系. 部分内容来自以下博客: https://www.cnblogs.com/dolp ...

  10. Java多线程之守护线程

    Java多线程之守护线程 一.前言 Java线程有两类: 用户线程:运行在前台,执行具体的任务,程序的主线程,连接网络的子线程等都是用户线程 守护线程:运行在后台,为其他前台线程服务 特点:一旦所有用 ...

随机推荐

  1. 【原创】大叔经验分享(78)hive查询报错NoViableAltException

    Hive或spark中执行sql字符常量包含;时会报错,比如 select instr('abc;abc', ';'); 报错 NoViableAltException(-1@[147:1: sele ...

  2. C++通用框架和库

    C++通用框架和库 来源 https://www.cnblogs.com/skyus/articles/8524408.html 关于 C++ 框架.库和资源的一些汇总列表,内容包括:标准库.Web应 ...

  3. Qt使用自带的windeployqt 查找生成exe 必需的库文件

    集成开发环境 QtCreator 目前生成图形界面程序 exe 大致可以分为两类:Qt Widgets Application  和 Qt Quick Application.下面分别介绍这两类exe ...

  4. com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction 问题解决

    有两种设置方法 第一种在mysql的配置文件中加入,然后重启mysql innodb_lock_wait_timeout = 500 第二种直接执行如下命令 set global innodb_loc ...

  5. element-ui表格带复选框使用方法及默认选中方法

    一.实现多选:步骤1:在表格中添加一列 步骤2:在data中定义以及数组用来存储选中的元素.例如:multipleSelection:[] selection-change方法用户实时监听选中元素 实 ...

  6. 三台服务器的时间同步-Linux

    192.168.1.30    做服务器时间 192.168.1.40   同步30 192.168.1.50  同步30 step1.在30机器上修改ntp.conf 添加: restrict  1 ...

  7. 《数字图像处理(MATLAB)》冈萨雷斯

    <数字图像处理(MATLAB)>冈萨雷斯 未完结! 参考:数字图像处理——https://blog.csdn.net/dujing2019/article/category/8820151 ...

  8. 404boom 博客闪现【不断的优化更新中。。。】

    404boom 博客闪现[不断的优化更新中...] 停止本篇博文EQ继续优化,所有博文将会在标签[cnblogs_v2 ]中重新整理,待完成统一放上链接 一:Java SE相关1.Java的概述2.J ...

  9. java——java跨平台原理

    不同操作系统不同的虚拟机,屏蔽不同系统指令集的差异. 开发程序只需要遵循java规范:

  10. mybatis的配置文件详解(二)

    一.properties 这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递.例如 1) <?xml versio ...