首先线程安全的实现方法多是理论,比较枯燥!
首先知道什么是线程安全?
“线程安全”是一个老生常谈的话题,但是对线程安全比较规范的定义却不是一件容易的事。
Brian Goetz是这样描述线程安全的:
当多个线程同时访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方法进行其它的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称这个对象是安全的。
简单说:但多个线程同时访问同一个类(对象、方法)的时候,可以得到预期的结果,那么这就是线程安全的。
线程安全的实现方法
1.互斥同步
同步是指当多个线程并发访问共享数据时,保证共享数据在同一时刻只能由一条线程使用。
互斥是实现同步的一种手段。互斥是因、同步是果。互斥是一种方法,同步是目的
Java中实现互斥同步-synchronized关键字
sunchronized关键字实现互斥同步。synchronized关键字在javac编译之后会在代码块前后添加monitorenter和monitorexit这两个字节码指令。这两个字节码指令需要一个引用类型的参数来明确锁定和解锁的对象。如果Java源码中指定了该对象的参数,那么这个就以这个对象作为reference,如果没有指定,那么会根据synchronized关键字修饰的是实例方法还是类方法来决定是该reference是具体的实例对象还是对应的Class对象。
根据《Java虚拟机规范》在执行monitorenter指令前,首先尝试获取对象的锁。
如果这个对象没被锁定,或者当前线程已经拥有该对象的锁,那么就将锁的计数器的
值+1。当执行monitorexit指定时就将锁计数器的值减1。一旦计数器的值为0
那么锁随机就被释放了。如果当前线程不能获取锁对象,那么当前线程会阻塞等待
直到请求的对象被所持有的线程释放。
Java中实现互斥同步-Lock类
java.util.concurrent.locks.Lock
该接口是一种全新的互斥手段
ReentrantLock锁(重入锁)
重入锁:指一个线程获取对象的锁后,依旧可以获取该对象的锁。即锁关联的计数器
如果持有锁的线程再次获取它,那么计数器加1,每次释放锁的时候就将计数器的值
减1.直到计数器的值为0,才算真正的释放锁!
ReentrantLock与synchronized区别:
1.等待可中断:当持有锁的线程长期不释放锁的时候,此时等待线程可以选择放弃等待,选择处理别的事情。
2.公平锁:多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来依次获取锁对象(非公平锁在释放锁时,任何一个线程都有机会获取锁)ReentrantLock默认是非公平锁,但是可以使用构造函数来指定使用公平锁。但是会导致ReentrantLock的性能极具下降。
3.锁绑定多个条件:一个ReentrantLock可以同时绑定多个锁对象
2.非阻塞同步
上述介绍的互斥同步也称之为阻塞同步,该同步最大的问题就是,线程阻塞和唤醒的开销比较大。从解决问题的方式上说,互斥同步属于悲观的并发策略(无论共享资源是否存在竞争,它认为只要不同步就一定会发生问题)。
非阻塞同步是一种乐观的并发策略,该策略不需要将线程进行阻塞挂起。简单说,非阻塞同步不管是否有安全问题,先进行操作,如果没有其它线程争用共享数据,那么就操作成功。如果共享数据被争用存在冲突,就采用补救策略。常见的是不断的进行重试,直到没有共享数据的竞争。
3.无同步方案
要保证线程安全,也并非一定要进行阻塞或非阻塞同步,同步与线程安全两者没有必然的联系。 同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据,那它自 然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的,可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何 时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不 会出现任何错误,也不会对结果有所影响。在特指多线程的上下文语境里(不涉及信号量等因 素[6]),我们可以认为可重入代码是线程安全代码的一个真子集,这意味着相对线程安全来说,可重 入性是更为基础的特性,它可以保证代码线程安全,即所有可重入的代码都是线程安全的,但并非所 有的线程安全的代码都是可重入的。 可重入代码有一些共同的特征,例如,不依赖全局变量、存储在堆上的数据和公用的系统资源, 用到的状态量都由参数中传入,不调用非可重入的方法等。我们可以通过一个比较简单的原则来判断 代码是否具备可重入性:如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返 回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就 看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可 见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。