线程安全究竟是什么意思?

本文转发自Jason’s Blog原文连接 http://www.jasongj.com/java/thread_safe/html

多线程编程中的三个核心概念

原子性

这一点,跟数据库事务的原子性概念差很少,即一个操做(有可能包含有多个子操做)要么所有执行(生效),要么所有都不执行(都不生效)。java

关于原子性,一个很是经典的例子就是银行转帐问题:好比A和B同时向C转帐10万元。若是转帐操做不具备原子性,A在向C转帐时,读取了C的余额为20万,而后加上转帐的10万,计算出此时应该有30万,但还将来及将30万写回C的帐户,此时B的转帐请求过来了,B发现C的余额为20万,而后将其加10万并写回。而后A的转帐操做继续——将30万写回C的余额。这种状况下C的最终余额为30万,而非预期的40万。数据库

可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程可以当即看到。可见性问题是好多人忽略或者理解错误的一点。编程

CPU从主内存中读数据的效率相对来讲不高,如今主流的计算机中,都有几级缓存。每一个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会当即更新该缓存,但并不必定会当即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤为是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。api

这一点是操做系统或者说是硬件层面的机制,因此不少应用开发人员常常会忽略。缓存

顺序性

顺序性指的是,程序执行的顺序按照代码的前后顺序执行。安全

如下面这段代码为例bash

boolean started = false; // 语句1
long counter = 0L; // 语句2
counter = 1; // 语句3
started = true; // 语句4

从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们必定彻底按照此顺序执行。多线程

处理器为了提升程序总体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。并发

讲到这里,有人要着急了——什么,CPU不按照个人代码顺序执行代码,那怎么保证获得咱们想要的效果呢?实际上,你们大可放心,CPU虽然并不保证彻底按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。

Java如何解决多线程并发问题

Java如何保证原子性

锁和同步

经常使用的保证Java操做原子性的工具是锁和同步方法(或者同步代码块)。使用锁,能够保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。

public void testLock () {
  lock.lock();
  try{
    int j = i;
    i = j + 1;
  } finally {
    lock.unlock();
  }
}

与锁相似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例

public void testLock () {
  synchronized (anyObject){
    int j = i;
    i = j + 1;
  }
}

不管使用锁仍是synchronized,本质都是同样,经过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。

CAS(compare and swap)

基础类型变量自增(i++)是一种常被新手误觉得是原子操做而实际不是的操做。Java中提供了对应的原子操做类来实现该操做,并保证原子性,其本质是利用了CPU级别的CAS指令。因为是CPU级别的指令,其开销比须要操做系统参与的锁的开销小。AtomicInteger使用方法以下。

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}

Java如何保证可见性

Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会当即被更新到内存中,而且将其它缓存中对该变量的缓存设置成无效,所以其它线程须要读取该值时必须从主内存中读取,从而获得最新的值。

Java如何保证顺序性

上文讲过编译器和处理器对指令进行从新排序时,会保证从新排序后的执行结果和代码顺序执行的结果一致,因此从新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。

Java中可经过volatile在必定程序上保证顺序性,另外还能够经过synchronized和锁来保证顺序性。

synchronized和锁保证顺序性的原理和保证原子性同样,都是经过保证同一时间只会有一个线程执行目标代码段来实现的。

除了从应用层面保证目标代码段执行的顺序性外,JVM还经过被称为happens-before原则隐式的保证顺序性。两个操做的执行顺序只要能够经过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不做任何保证,可对其进行任意必要的从新排序以获取高效率。

happens-before原则(先行发生原则)

  • 传递规则:若是操做1在操做2前面,而操做2在操做3前面,则操做1确定会在操做3前发生。该规则说明了happens-before原则具备传递性
  • 锁定规则:一个unlock操做确定会在后面对同一个锁的lock操做前发生。这个很好理解,锁只有被释放了才会被再次获取
  • volatile变量规则:对一个被volatile修饰的写操做先发生于后面对该变量的读操做
  • 程序次序规则:一个线程内,按照代码顺序执行
  • 线程启动规则:Thread对象的start()方法先发生于此线程的其它动做
  • 线程终结原则:线程的终止检测后发生于线程中其它的全部操做
  • 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
  • 对象终结规则:一个对象构造先于它的finalize发生

volatile适用场景

volatile适用于不须要保证原子性,但却须要保证可见性的场景。一种典型的使用场景是用它修饰用于中止线程的状态标记。以下所示

boolean isRunning = false;

public void start () {
  new Thread( () -> {
    while(isRunning) {
      someOperation();
    }
  }).start();
}

public void stop () {
  isRunning = false;
}

在这种实现方式下,即便其它线程经过调用stop()方法将isRunning设置为false,循环也不必定会当即结束。能够经过volatile关键字,保证while循环及时获得isRunning最新的状态从而及时中止循环,结束线程。

线程安全十万个为何

问:平时项目中使用锁和synchronized比较多,而不多使用volatile,难道就没有保证可见性?
答:锁和synchronized便可以保证原子性,也能够保证可见性。都是经过保证同一时间只有一个线程执行目标代码段来实现的。

 


问:锁和synchronized为什么能保证可见性?
答:根据JDK 7的Java doc中对concurrent包的说明,一个线程的写结果保证对另外线程的读操做可见,只要该写操做能够由happen-before原则推断出在读操做以前发生。

 

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

问:既然锁和synchronized便可保证原子性也可保证可见性,为什么还须要volatile?
答:synchronized和锁须要经过操做系统来仲裁谁得到锁,开销比较高,而volatile开销小不少。所以在只须要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。

问:既然锁和synchronized能够保证原子性,为何还须要AtomicInteger这种的类来保证原子操做?
答:锁和synchronized须要经过操做系统来仲裁谁得到锁,开销比较高,而AtomicInteger是经过CPU级的CAS操做来保证原子性,开销比较小。因此使用AtomicInteger的目的仍是为了提升性能。

问:还有没有别的办法保证线程安全
答:有。尽量避免引发非线程安全的条件——共享变量。若是能从设计上避免共享变量的使用,便可避免非线程安全的发生,也就无须经过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。

问:synchronized便可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。

相关文章
相关标签/搜索