线程安全定义
《Java Concurrency In Practice》一书中定义“线程安全”:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。
Java语言中线程安全中,各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
不可变
不可变的对象一定是线程安全的,不论对象的方法实现还是方法的调用者,都不需要采取任何线程安全保障措施。“不可变”带来的安全性是最简单和最纯粹的。
在Java语言中,如果共享数据是一个基本数据类型,只要final关键字修饰就可以保证它是不可变的。如果共享对象是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。
绝对线程安全
完全满足上诉线程安全的定义,这个定义其实是很严格的,通常需要付出很大的代价才能达到,甚至有时候是不切实际的代价。
相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的。我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。在Java语言中,大部分的线程安全类都属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这种情况。Java API中大部分的类都是属于线程兼容的,如ArrayList和HashMap等。
线程对立
线程对立指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现,而且通常都是有害的,应当避免。常见的例子有Thread类的suspend()和resume()方法,System.setIn()、System.setOut()和System.runFinalizersOnExit()等。
线程安全的实现
互斥同步
互斥同步(Mutual Exclusion & Synchronion)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此这里面,互斥是因,同步是果,互斥是方法,同步是目的。
在Java中,最基本的互斥同步方法就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来明确要锁定和解锁的对象。synchronized同步块对同一线程来说是可重入的 ,不会出现自己把自己锁死的情况,其次,同步块在已进入线程执行完之前,会阻塞后面其他线程的进入。
除了使用synchronized之外,我们还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。相比于synchronized,ReentrantLock增加了一些高级功能:
- 等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行非常长的同步块很有帮助
- 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized是非公平锁,ReentrantLock默认情况下是非公平的,但是可以设置成公平锁
- 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而synchronized中,锁对象的wait()和notify()或者notifyAll()方法可以实现一个隐含的条件,如果要和多于一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。从出了问题的方式来说,互斥同步是一种悲观的并发策略,总是认为要是不做正确的同步措施,就肯定出现问题。随着硬件指令集的发展,可以采用基于冲突检测的乐观并发策略,通俗的将就是先进行操作,产生了冲突,那就再采取其他补偿措施,这种乐观的并发策略许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blockinig Synchronization)。
无同步方案
要保证线程安全,并不是一定要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
可重入代码:这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入代码都是线程安全的,但是并不是所有的线程安全代码都是可重入的。可重入代码有一些共同特征,例如不依赖存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入、不调用非可重入的方法等。
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据是否能保证在同一个线程中执行,如果能保证,就可以把共享数据的可见范围限制在同一线程之内,这样无须同步也能保证下城之间不出现数据争用的问题。
符合这种特点的应用并不少,典型的就是消费队列架构模式,都会将产品的消费过程尽量在一个线程中消费完。Java语言中,如果一个变量要被多线程访问,可以使用volatile关键字声明为“易变的”:如果一个变量要被某个线程独享,可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。每个线程的Thread对象都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每个ThreadLocal对象都包含了独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。
参考资料
- 深入理解Java虚拟机 JVM高级特性与最佳实践 第2版
网友评论