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

标签: | 发表时间:2014-10-26 03:29 | 作者:yangzl2008
出处:http://blog.csdn.net/yangzl2008

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()是互斥的。
类锁,如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-------------------
作者:yangzl2008 发表于2014/10/25 19:29:59 原文链接
阅读:572 评论:0 查看评论

相关 [java 多线程 synchronized] 推荐:

Java多线程之synchronized

- - CSDN博客推荐文章
这里通过三个测试类阐述了synchronized应用的不同场景. 首先是最基本的synchronized Method的使用.  * @see 概述:Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor) .  * @see 说明:当synchronized关键字修饰一个方法时,则该方法为同步方法 .

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

- - Snowball
新建状态(New):新创建了一个线程对象. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法. 该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行.

[转]java synchronized详解

- - 小彰
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.      一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行. 另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块.

文章: Java SE1.6中的Synchronized

- - InfoQ cn
在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗,而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程.

Java同步块(Synchronized Blocks)

- - 并发编程网 - ifeve.com
原文链接  作者:Jakob Jenkov  译者:李同杰. Java 同步块(synchronized block)用来标记方法或者代码块是同步的. Java同步关键字(synchronzied). Java 同步关键字( synchronized ). Java中的同步块用synchronized标记.

java 并发编程 synchronized

- - Java - 编程语言 - ITeye博客
同步原语--synchronized. synchronized(class)很特别,它会让另一个线程在任何需要获取class做为monitor的地方等待.class与this做为不同的监视器可以同时使用,不存在一个线程获取了class,另一个线程就不能获取该class的一切实例.. ->线程各自获取monitor,不会有等待..

Java中Synchronized的用法

- - CSDN博客推荐文章
synchronized是Java中的关键字,是一种同步锁. 它修饰的对象有以下几种:. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;.

java-synchronized修饰方法释疑

- - ITeye博客
java里面用synchronized修饰方法时:. 对于 同一个对象的来说. 所有synchronized修饰的方法会 相互阻塞(即调用了某一个synchronized修饰的方法,则其余所有synchronized修饰的方法的调用都会阻塞,需要等待获取内置锁). 没有synchronized修饰的方法的则不会阻塞.

Java synchronized同步方法和同步块总结

- - 编程语言 - ITeye博客
今天做了一些实验,把Java synchronized同步方法和同步块总结一下,欢迎拍砖. Java synchronized同步方法和同步块总结:. synchronized method() {...} 锁对象的所有同步方法. 一个进程进入某对象同步方法后,其它线程不能同时访问这个对象中任何一个同步方法.

Java线程同步中关键字synchronized详述

- - 编程语言 - ITeye博客
synchronized关键可以修饰函数、函数内语句. 无论它加上方法还是对象上,它取得的锁都是对象,而不是把一段代码或是函数当作锁. 1,当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一段时间只能有一个线程得到执行,而另一个线程只有等当前线程执行完以后才能执行这块代码.