线程安全

 

1、线程安全

java中线程安全可以分成5类:不可变绝对线程安全相对线程安全线程兼容线程独立

不可变。对于基本的数据类型,只要加上final修饰,就可以实现不可变。对于对象,其实就将其构造函数相关的属性编程final即可。

绝对线程安全。绝对安全的很难达到,而且java中线程安全的类并不是绝对安全的。就算是vector也需要利用Synchronized去保证同时操作读写。

相对安全。平常使用的时候不用刻意去做额外的保护措施,但是在一些特殊的调用的时候才需要进行调用。如Vector、volatile等。

线程兼容。线程兼容表示对象本身并不是线程安全的,只能通过同步手法去保证线程的安全。

线程独立。 表示采用任何措施都无法发保证线程的安全。这样的类很少见。
 

2、线程安全的实现方法

互斥同步、非阻塞同步、无同步。

互斥同步是最常用的方案,互斥同步指的是在同一个时间内多个线程去访问同一个数据, 保证共享数据只会被一个线程使用。

实现的手段是Synchronized。Synchronized经过编译之后会变成monitor entry和monitor exit。这两个字节码指令都需要一个reference类型的额参数来明确加锁或者解锁的对象。根据虚拟机规范中的明确,monitorentry在执行的时候会首先尝试去获取对象的锁,如果可以获得,那么就会把锁的计数器增加一。执行monitorexit的时候会把锁的计数器减一。只有当锁的计数为0的时候才会被释放。如果获取对象的锁失败,那么线程就会进入阻塞等待,等待锁被释放为止。

Synchronized同步块对同一个线程来说是可重入的,所以不会出现死锁的情况。

除了Synchronized以外,还可以利用concurrent包里边的重入锁来实现同步,concurrent和Synchronized的效果是一样的,但是增加了一些功能,主要是:等待可中断、可实现公平锁、锁可以绑定多个条件。

等待可中断,顾名思义,就是在线程等待的时候可以放弃等待,转去做其他的事。

公平锁是指根据申请锁的时间顺序来获取锁,非公平锁不能保证这一点。Synchronized中的锁默认是非公平的,ReentrantLock默认情况下是非公平的,但是可以设置成公平锁。

同时绑定多个Condition对象。在Synchronized中,wait()、notify()等方法可以实现一个隐含条件,而ReentrantLock可以通过多次调用newCondition()方法去实现多个条件。

 

非阻塞同步

互斥同步的主要问题是进行线程阻塞和唤醒的时候带来的线程问题,这样的同步也被称为阻塞同步。这样的方式是一种悲观的方式,不论这个数据是否存在竞争,都需要进行加锁。在加锁的时候又需要对内核的状态进行切换,并且维护锁计数器,这些都是对资源的浪费。

随着硬件指令集的发展,就出现了可以基于冲突检查的乐观并发策略。简单的说,就是先对数据进行操作, 然后在插入的是去检查数据是否存在竞争。如果没有就直接插入,如果有就才去补偿措施。这样的措施一般是去进行重试。这样的乐观的策略被称为非阻塞同步。

非阻塞同步需要有硬件指令集的支持,直到最近才出现了CAS指令。这个指令的意思是比较并交换。CAS需要3个操作数:内存位置、旧的预期值和新值。

先比较当前的内存位置的值符合就的预期值,如果符合旧的预期值,就用新值去更新,如果不符合,就不更新。不论更不更新,都会返回内存位置的值。

但是CAS并不能解决ABA这样的变化。

 

无同步方案

要保证线程安全,并不是一定要进行同步的。如果一些数据本来就是线程独占的,就不会出现同步的问题。实现这样的方法的有两个典型的例子:可重入代码线程本地存储

可重入代码表示一一段代码在执行的时候中断了,一段时间后回来再执行,结果不变的代码。

线程本地存储。如果一段代码中的所需数据需要和其他代码共享,同时保证这些代码在同一个线程里边执行就不会有线程安全的问题了。

还有另外一种方案就是把线程的数据复制一份出来。这样就无需竞争。