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

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线程状态的转换图如下:

其中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、参考


------------------------------本文同步发布于 http://zhangsr.com/i/1114-------------------

猜你喜欢

转载自blog.csdn.net/yangzl2008/article/details/39988841