Java中线程安全的实现方法

JVM

互斥同步

互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。

java中最基本的互斥同步手段是 synchronized 关键字,也是最简单的一种方式。
此关键字在经过编译之后,会在同步块前后形成monitorenter和monitorexit这两个字节码的指令,这两个字节码都需要一个reference来指定对象参数,来指明要锁定和解锁的对象。在java程序中如果明确指定了这个对象参数,那就是这个对象的reference,否则会根据当前的方法时实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

同步代码块在已经进入线程执行完之前,会阻塞后面的线程进入。Java的线程实现,是映射到操作系统的原生线程上的,线程的阻塞和唤醒需要操作系统的帮助,需要从用户态切换到内核态,而内核状态的切换需要耗费CPU很多时间。对于代码简单的同步块,状态切换的时间可能比代码执行的事件还长。所以synchronized是一个重量级的同步操作,使用的时候需要注意。而且虚拟机也对synchronized进行了一些必要的优化。

除了synchronized之外,还有J.U.C中实现了Lock的ReentrantLock来实现同步。区别是一个表现为API层面的互斥锁(Lock()和unlock()方法配合try/finally语句块来完成),另一个变现为语法层面的互斥锁。此外,重入锁还增加一些高级功能:等待可中断,可实现公平锁,以及锁可以绑定多个条件。

等待可中断: 当持有锁的线程,长期不释放锁时,正在等待的线程,可以选择放弃等待,对处理执行时间非常长的同步块很有用。

公平锁: 多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized中的锁是非公平的。ReentrantLock默认非公平锁。

绑定多个条件: 对于synchronized,锁对象的wait和notify或notifyAll可以实现一个隐含的条件,如果要和多个条件关联就不得不额外的添加一个锁,但是重入锁无需这样做,只需要多次调用newCondition()方法即可创建并绑定多个Condition对象。

在性能方面,在JDK 1.5和之前的版本,synchronized的性能要比重入锁的性能要差,但是在之后的版本中对synchronized采取了很多的优化措施,所以在之后的版本中两者的性能基本上是持平的。所以如果在JDK 1.6及其以后的版本上,性能因素不是我们选择使用synchronized和重入锁的关键。如果需求允许的话,更提倡使用原生的synchronized,因为虚拟机在未来的性能改进中肯定会更偏向原声的synchronized。

主要的互斥实现方式:临界区,信号量,互斥量。

非阻塞同步

除了上面说的互斥同步,还有一种方式就是非阻塞同步。互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(如加锁),那就肯定会出现问题,无论共享数据是否会发生竞争,都要进行加锁等操作。

随着硬件指令集的发展有了另外的选择:基于冲突检测的乐观并发策略。通俗的说,就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,那就再采取补偿措施(最常见的补偿措施就是不断的重试,直到成功为止),这种乐观的并发策略不需要把线程挂起,因此这种同步操作称为非阻同步。

乐观并发策略的具体实现依赖于某些特定的“硬件指令”(从语义上看起来需要多次操作的行为但实际只通过一条处理器指令只能就能完成),比如最典型的;比较并交换(Compare-and-Swap,简称CAS)和获取并设置。

CAS 操作包含三个操作数内存位置(V)、预期原值(A)和新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。

举个CAS操作的应用场景的一个例子,当一个线程需要修改共享变量的值。完成这个操作,先取出共享变量的值赋给A,然后基于A的基础进行计算,得到新值B,完了需要更新共享变量的值了,这个时候就可以调用CAS方法更新变量值了。

在JDK1.5之后,Java程序中才可以使用CAS操作:sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等方法包装提供。但该类不能由用户程序的类调用(除非使用反射)。J.U.C包里面的整数原子类的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。

使用CAS避免阻塞同步的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AtomicTest {

public static AtomicInteger race = new AtomicInteger(0);


public static void increase() {
race.incrementAndGet();
}

private static final int THREAD_COUNT = 20;

public static void main(String[] args) {


Thread[] threads = new Thread[THREAD_COUNT];

for(int i=0;i<THREAD_COUNT;i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++) {
increase();
}
}
});
threads[i].start();
}

while (Thread.activeCount() > 1) {
Thread.yield();
}

System.out.println(race);

}

}

J.U.C原子类,comapreAndSet(),getAndIncrement()调用了Unsafe类的CAS操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public final int incrementAndGet() {
/* for(;;) {
int current = get();
int next = current + 1;
//这里调用了CAS
if (compareAndSet(current, next)) {
return next;
}
}
*/
//Java 8
return unsafe.getAndAddInt(this, valueOffset, 1);
}

CAS的逻辑漏洞——ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,此时并不能说它的值没有被其他线程修改过,有可能在这期间它的值先被改成了B,后又被改为了A,而CAS操作就会认为它从来没有改变过。大部分情况下ABA情况不会影响程序并发的正确性,如果需要解决ABA问题(JDK通过引入AtomicStampedReference来保证CAS的正确性),改用传统的互斥手段可能会比原子类更高效。

无同步方案

同步措施只是保证共享数据争用时的正确性的手段,至于第三种「无同步方案」的意思就是不涉及共享数据,自然而然不需要任何同步的措施去保证正确性,因为有些代码天生就是线程安全的,比如可重入代码和线程本地存储。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器