文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步
- 或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的
按照线程安全的安全程度由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类:
- 不可变
- 绝对线程安全
- 相对线程安全
- 线程兼容
- 线程对立
不可变:
- 不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施
【注】:
final
关键字带来的可见性时曾经提到过这一点:
- 只要一个不可变的对象被正确地构建出来(即没有发生this引用逃逸的情况)
- 那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态
Java语言中,如果多线程共享的数据是一个 基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的
- 对于对象,还没有相应的支持方式,API中符合不可变要求的类型有String之外,常用的还有枚举类型及
java.lang.Number
的部分子类如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型
- 但同为Number子类型的原子类AtomicInteger和AtomicLong则是可变的
不可变带来的安全性是最直接、最纯粹的
绝对线程安全:
绝对的线程安全能够完全满足我们文章开头定义的线程安全概念(也就是说更加严格 )
- (调用者都不需要任何额外的同步措施”可能需要付出非常高昂的,甚至不切实际的代价)
【注】:在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全
比如
java.util.Vector
是一个线程安全的容器(因为它的add()、get()和size()等方法都是被synchronized修饰的,尽管这样效率不高
- 但保证了具备原子性、可见性和有序性,但是在多线程环境中
- 如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全(不是绝对线程安全)的
- 假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照
- 这样要付出的时间和空间成本都是非常大的
相对线程安全 :
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的
我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性
【注】:在Java语言中,大部分声称线程安全的类都属于这种类型
- 例如:Vector、HashTable、Collections的
synchronizedCollection()
方法包装的集合等
线程兼容:
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用
【注】:我们平常说一个类不是线程安全的,通常就是指这种情况
- Java类库API中大部分的类都是线程兼容的
线程对立:
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码
【注】:Thread类的suspend()和resume()方法
- 调用这两个方法的线程存在很大的死锁风险,如今已经废弃
互斥同步:
互斥同步是一种最常见也是最主要的并发正确性保障手段
互斥是实现同步的一种手段,临界区、互斥量和信号量 都是常见的互斥实实现方式
- 互斥是因,同步是果,互斥是方法,同步是目的
【注】:最基本的互斥同步手段就是synchronized关键字、J.U.C包、重入锁ReentrantLock
等待可中断:
- 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情
公平锁:
- 指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- 而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁
指一个ReentrantLock对象可以同时绑定多个Condition对象
在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件
如果要和多于一个的条件关联的时候,就不得不额外添加一个锁
- 而ReentrantLock则无须这样做,多次调用newCondition()方法即可
非阻塞同步:
互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步
互斥同步属于一种悲观的并发策略,而随着硬件指令集的发展,我们已经有了另外一个选择:基于冲突检测的乐观并发策略
通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了
- 如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止
- 这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步,使用这种措施的代码也常被称为无锁编程
无同步方案:
要保证线程安全,也并非一定要进行阻塞或非阻塞同步,同步与线程安全两者没有必然的联系
同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据
- 那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的
两类:
可重入代码:
又称纯代码,指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身)
而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响