Java多线程中的synchronized、volatile和无锁编程

时间:2021-07-25 13:05:57

1、Java线程的状态

1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁(synchronized)被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

锁池:Java多线程中有两种同步锁synchronized和Lock,其中Lock关键字是JDK1.5之后新加入的锁,锁具有排他性,当一个线程获得锁之后,其他线程只能等待其他线程释放该锁,等待的线程也就进入了锁池。
等待池:当线程调用Object.wait()或者Condition.await()时,程序所在的线程会释放其所占有的资源(相应的会释放synchronized和Lock锁),而进入等待池,等待池当中的线程会等待其他线程调用Object.notifyAll(),Object.notify()或者 Condition.signalAll(),Condition.signal()唤醒,这样进入等待的线程就进入等待池,从等待池出来之后进入锁池,获得锁之后便可进行工作了。
需要说明的是,synchronized锁和调用wait()的对象应为同一对象!否则会报java.lang.IllegalMonitorStateException错误。正确方式如下:
 public synchronized static void function04() {//类锁
try {
Test05.class.wait();//本类的wait池
} catch (InterruptedException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}

}

public void function02() {
synchronized (lock) {//lock锁
try {
lock.wait();//同样为lock锁的wait池
} catch (InterruptedException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}

}
}

Java线程状态的转换图如下: Java多线程中的synchronized、volatile和无锁编程
其中Thread.join()调用的是Object.wait()方法实现的,意思是让当前线程等待。是当前调用thread1.join()的线程等待,而不是让thread1等待

2、并发编程的思考

并发安全性的几个相关因素:可见性、顺序性、原子性。关于这三者的详细描述,见原子性与可见性。其中原子性可以引申为互斥性,而顺序性的产生是原子性的结果即有了原子性才有了顺序性,因此以上三个因素可以推导为可见性和互斥性。根据并发安全的特性,对synchronized关键字、volatile关键字和无锁编程(Unsafe)三种并发处理的效果如下:

 

可见性

互斥性

synchronized

块可见

块互斥

volatile

变量可见

变量互斥(无意义)

无锁编程(Unsafe)

变量可见

不保证


3、synchronized关键字

synchronized关键字一般情况下有以下几种用法:
/**
* Created with IntelliJ IDEA.
* User: yangzl2008
* Date: 14-10-25
* Time: 下午8:31
* To change this template use File | Settings | File Templates.
*/
public class TeshSynchronized {
Object lock = new Object();
public synchronized void function01() {
}
public void function02() {
synchronized (lock) {
}
}
public void function03() {
synchronized (this) {
}
}
public synchronized static void function04() {
}
public void function05() {
synchronized (TeshSynchronized.class) {
}
}
}

以上synchronized关键字的用法可以根据锁的不同分为两类,对象锁类锁
对象锁,其中function01()、function02()、function03()用的是对象锁的形式。在多线程环境当中,调用同一对象的function01()、function02()、function03()是不互斥的,因为三个方法的锁是不一样的,分别是this,lock,this;这样function01和function03是互斥的,而与function02是不互斥的。 类锁,如function04()、function05(),这种锁对于同一类的不同线程都具有互斥作用。在多线程环境当中,调用不同对象的function04()、function05()是互斥的。

 

同一对象

不同对象但同一类

对象锁

多线程互斥

多线程不互斥

类锁

多线程互斥

多线程互斥


synchronized保证的是synchronized块级别的互斥性和可见性。
块级别的互斥性:当有一个线程获得synchronized的锁之后,其他线程不能进入这个块,而只能等获得锁的线程执行完毕之后,在进入这个块。 块级别的可见性:在多线程环境下,当一个线程进入synchronized块后,其修改的变量值在其他线程当中能够看到这个值。 基于以上以上两个特性,synchronized关键字能够保证多线程安全,这是真正意义上线程安全。

4、volatile关键字

volatile关键字,根据清英文章聊聊并发(一)深入分析Volatile的实现原理,可知道当我们在一个变量之上volatile之后在多核处理器下会引发了两件事情。
  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
volatile在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
/**
* Created with IntelliJ IDEA.
* User: yangzl2008
* Date: 14-10-26
* Time: 下午10:09
* To change this template use File | Settings | File Templates.
*/
public class TestVolatile {
private volatile int a1; //多线程可见
private int a2; //多线程有问题
private int a3;

public int getA1() {
return a1;
}

public void setA1(int a1) {
this.a1 = a1;
}

public int getA2() {
return a2;
}

public void setA2(int a2) {
this.a2 = a2;
}

public int getA3() {
return a3;
}

public synchronized void setA3(int a3) {
this.a3 = a3;
}

}

以上代码,我们来看看volatile的变量可见性。 对于a2,当线程调用setA2()方法对a2设值时,因为每个线程都有缓存,因此此时有可能会造成其他线程看不到新的值,而需要等到a2的同步到内存当中后,其他线程读内存时才能看到,存在多线程问题。 对于a1,因为volatile保证了a1只有一份数据在内存当中,因此,其他线程是可见的。 对于a3,因为其set方法使用synchronized 关键字,synchronized 关键字能够保证块可见性,因此其他线程是可见的。
由以上分析可知,volatile实现了synchronized 一样的多线程安全的效果。但是其实现的仅仅是可见性,对于块互斥性,并没有实现。看一下例子:
/**
* Created with IntelliJ IDEA.
* User: yangzl2008
* Date: 14-10-26
* Time: 下午10:21
* To change this template use File | Settings | File Templates.
*/
public class TestVolatile2 {

volatile int count;
Map<String, String> map = new ConcurrentHashMap<String, String>();

public void addContent(String key, String value) {
if (count < 100) {
map.put(key, value);
count++;
}
}

@Test
public void testAddContent() throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(10);

for (int i = 0; i < 10; i++) {
executorService.execute(new AddContentTask());
}
// 关闭启动线程
executorService.shutdown();
// 等待子线程结束,再继续执行下面的代码
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);

System.out.println(map.size());

}

private final class AddContentTask implements Runnable {

@Override
public void run() {
//每个线程放11次
for (int i = 0; i <= 10; i++) {
addContent(Thread.currentThread().getName() + " " + System.currentTimeMillis() + " " + i, "value");
}
}
}


}
以上判断count判断到达100后,就无法再向map当中放东西,但实际上,map当中的数量绝大多数情况下是大于100的。因此,volatile只能保证变量的可见性,而并不能保证块的互斥性,在某些情况下,其是无法代替synchronized的。

5、无锁编程

Java当中的无锁编程通过sun.misc.Unsafe实现的。我们以AtomicInteger源码来分析一下,其在多线程下的运作方式。首先,Unsafe通过内存偏移量得到要变量的内存位置,代码如下:
 static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
在我们调用getAndIncrement时,其代码如下:
 public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
compareAndSet的代码如下:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);是一个本地方法。

CAS (compare and swap)操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
在认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。他是非阻塞的。

从某种意义上来看,简单的复合操作,不管是getAndInc和getAndDec还有IncAndGet、DecAndGet等等,其实都可以归结为一个CAS操作,比如getAndIncrement,在for循环内取原值,并且+1,并且和原值比较设置结果,如果成功的话返回,否则继续。

而以上之所以会产生不成功的情况,是因为在多线程情况下,有可能有别的线程已经修改value的值,在比较的时候,value的值跟原先的值不同,因此其继续进行比较,只有在没有线程改变之后,才能修改value的值。比如,线程A打算修改value的值,但是B线程在这个时候修改了value的值,A看到value的值变量,继续下一个循环,这时,C线程又来修改了value的值,A看到后只能又进行下一个循环。因此无锁编程,无法保证顺序性,即无法保证互斥性,因为每个线程都有可能修改value的值,但是value值得修改对每个线程的修改都是可见的。

6、总结

Java多线程中的synchronized、volatile和无锁编程在不同的应用场景下,都能保证线程安全,我们在选择不同的工具时,需要根据不同的场景选择不同的工具,当然synchronized是肯定能够实现多线程安全的,但是在某些情况下,后两者的效率可能更高,这就需要我们对不同的业务场景进行仔细的分析,找到最合适的工具!

7、参考

1、深入理解Java内存模型 2、并发总结2--volatile、CAS、HB 3、原子性与可见性
4、线程安全性:原子性,可见性,加锁机制 5、聊聊并发(一)深入分析Volatile的实现原理
------------------------------本文同步发布于http://zhangsr.com/i/1114-------------------