文章内容均来摘自:《深刻理解Java虚拟机:JVM高级特性与最佳实践》。内容缩减 |
《Java Concurrency In Practice》做者Brian Goetz对线程安全作了个定义:当多个线程访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,后者在调用方进行任何其它的协调操做,调用这个对象的行为均可以得到正确的结果,那么这个对象就是线程安全的。java
它要求线程安全的代码必须具有一个特性:代码自己封装了全部必要的正确性保障(如互斥同步等),令调用者无需关心多线程的问题,更无需本身采起任何措施来保证多线程的正确调用(好比J.U.C包下的一些并发集合框架)。数组
在Java语言中,如何体现线程安全?若是一段代码不会与其余线程共享数据,那么从线程安全的角度来看,程序是串行执行仍是并发执行对它来讲是没有区别的,也就是这段代码是线程安全的;即多线程安全问题,在于多线程之间存在共享数据访问操做。安全
那么有哪些操做时线程安全的?在Java中,按照线程安全的“程度”由强至弱排序,分为5种:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。多线程
(特指JDK1.5版本以后,即Java内存模型被修复以后的Java版本),不可变(Immutable)的对象必定是线程安全的,不管对象的方法实现仍是方法的调用者,都不须要采起任何的线程安全保障,若是一个对象是不可变的,那么它永远是一致的。并发
若是共享数据是一个基本数据类型,那么只要在定义的时候使用final修饰就能够保证它是不可变的(不可变即线程安全),若是共享数据是一个对象,咱们要保证操做该对象不会对它的状态产生变化,好比java.lang.String类,咱们调用它的substring、replace等操做都会返回一个新对象而不对自身状态进行变动(一个简单的作法就是将对象中带有状态的变量以final修饰,这样再初始化以后,便为不可变)。框架
在Java中,标注为线程安全的类,通常都不是绝对线程安全的,能够用java中的一个非绝对线程安全的线程安全类来看看这个”绝对”的意思。性能
好比java.util.Vector是一个线程安全的容器,全部方法都被synchronized修饰,尽管这样效率很低,但的确是安全的;可是,即便它全部的方法都被修饰成同步,也并不意味着使用它就再也不须要同步手段了。测试
若是A线程删除了一个元素,致使某个序号不在可用,B线程恰好用那个序号访问数组就会抛出越界异常,因此须要对A线程的remove操做和B线程的get操做进行同步操做。优化
对于b中的比方就是典型的相对线程安全示例。java中大多数线程安全类都是相对线程安全的,好比Vector、Hashtable、Collections下的synchronizedCollection方法包装的集合等。spa
对象自己并非线程安全的,可是能够经过同步手段来保证对象在并发环境中能够安全使用,日常说的一个类不是线程安全的,大部分指的就是这种状况。好比相对于c的Vector、Hashtabl的ArrayList、HashMap等。
不管调用端是否采起了同步手段,都没法在并发环境中使用的代码。
一个线程对立的例子,就是Thread类的suspend()和resume()方法,好比有两个线程同时调用一个对象,一个尝试去中断线程,另外一个尝试去恢复线程,若是并发执行的话,不管是否进行了同步,都会产生死锁的风险。
咱们能够经过三种方式来实现线程安全。
互斥同步是常见的一种并发正确性保障手段,同步指的是在多个线程并发访问共享数据的时候,保证共享数据在同一时刻只被一个线程使用(或者一些,使用信号量的时候)。
Java中基本的同步手段就是synchronized关键字,synchronized同步块对同一个线程是可重入的,不会把本身锁死的状况(即我得到了这把锁,在同步块中还能够继续使用而不会锁住不让执行);同步块在线程执行完以前,会阻塞其它线程的进入。
除了synchronized以外,还可使用J.U.C包下的重入锁ReentrantLock来实现同步,基本用法和synchronized相似,都具备同样的线程可重入性,只是代码上的区别,一种是API层面的互斥锁(利用lock和unlock方法配合try/finally块来完成),另外一种是原生语法层面的互斥锁。不过,相比synchronized,ReentrantLock增长了一些高级功能主要有:等待可中断、可实现公平锁、可绑定多个条件。
·等待可中断:指当前持锁线程长期不释放锁,正在等待的线程能够放弃等待,转而处理其它事情,可中断操做对处理执行时间很长的同步块颇有帮助。
·公平锁:多个线程在等待同一个锁的时候,必须按照申请的时间顺序依次得到锁(相似于生活中的排队,先来后到)。而非公平锁在锁被释放的时候,任一线程均可能得到锁;synchronized是一个非公平锁,ReentrantLock默认也是非公平锁,可是能够经过参数来设定。
·绑定多个条件:一个ReentrantLock能够同时绑定多个Conditional对象,而在synchronized中,所对象的wait()和notify()或notifyAll()方法可实现一个隐含的条件,若是要喝多于一个的条件关联的时候,就不得不额外的添加一个锁,而ReentrantLock则无需这么作,只须要屡次调用newConditional便可。
基于性能及虚拟机将来优化的考虑,在能知足同步需求的状况下,推荐使用synchronized。(在JDK1.6版本及以上synchronized性能基本与ReentrantLock持平)。
互斥同步最主要的问题是进行线程阻塞和唤醒所带来的性能问题,所以这种同步也叫阻塞同步;从处理问题的方式上来讲,互斥同步属于一种悲观的并发策略,认为不去作正确的同步操做(例如加锁),都会出现问题,不管共享数据是否真的会出现竞争。
而非阻塞同步(乐观的并发策略),通俗讲就是先进行操做,若是无其它线程对共享数据进行争用,那就操做成功了;若是有争用,产生了冲突,那就重来,直到成功为止。(参阅AtomicInteger的incrementAndGet()方法)
乐观并发策略须要“硬件指令集”,这类指令经常使用的有:
CAS操做有一个逻辑漏洞:若是有一个值是A,而后曾经被改为了B,后来又改回A,那么CAS会误认为它没有改变过,这个问题被称为CAS的"ABA"问题(J.U.C包下的一个带有标记的原子引用类AtomicStampedReference,它能够经过控制变量的版本号来保证CAS的正确性)。可是大部分状况下"ABA"问题不会影响程序并发的正确性,若是须要解决ABA问题,采用传统的互斥同步相对比原子类操做更高效。
要保证线程安全,并不必定要进行同步,同步只是保证共享数据的正确性,若是一个方法不涉及共享数据,那它天然就无需任何同步手段去保证正确性了;所以会有一些代码天生具备线程安全性。
·可重入代码:可重入代码具备一些共同的特性,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数传入、不调用非可重入的方法等。咱们能够经过一个简单的原则来判断一段代码是否具备可重入性:例若有一个方法,它的返回结果老是可预测的,只要输入了相同的数据,给你的都是同一个结果,那它就知足了可重入的要求,那就是线程安全的了。
·线程本地存储:ThreadLocal