深入理解Java虚拟机-13.2 线程安全- 高飞网

13.2 线程安全

2017-02-22 12:16:48.0

    当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

13.2.1 Java语言中的线程安全

    按照线程安全的“安全程度”由强及弱排序,可以将Java语言中各种操作共享的数据分为以下五类:

1. 不可变

    在Java语言里面,不可变的对象一定是线程完全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何的线程安全保障措施。如果共享数据是一个基本类型的变量,只要在其前面加上final关键字即可。如果共享数据是一个对象,就要保证对象里的状态信息也是final的,例如java.lang.String,或下面的java.lang.Integer中的代码

/**
 * The value of the {@code Integer}.
 *
 * @serial
 */
private final int value;

/**
 * Constructs a newly allocated {@code Integer} object that
 * represents the specified {@code int} value.
 *
 * @param   value   the value to be represented by the
 *                  {@code Integer} object.
 */
public Integer(int value) {
    this.value = value;
}


2. 绝对线程安全

    以java.util.Vector为例,该容器类的所有方法都是用synchronized修饰的,也就是这些方法都是线程安全的,尽管这样效率很低。但是,即使所有的方法都被修饰成同步,也不意味着调用它的时候永远不再需要同步了。

import java.util.*;

public class VectorTest{
    private static Vector<Integer> vs = new Vector<Integer>();

    public static void main(String[] args){
        while(true){
            for(int i=0;i<10;i++){
                vs.add(i);
            }
            Thread removeThread = new Thread(new Runnable(){
                public void run(){
                    for(int i=0;i<vs.size();i++){
                        try{Thread.sleep(1);}catch(Exception e){}
                        vs.remove(i);
                    }
                }
            });
            Thread printThread = new Thread(new Runnable(){
                public void run(){
                    for(int i=0;i<vs.size();i++){
                        System.out.println(vs.get(i));
                    }
                }
            });
            removeThread.start();
            printThread.start();

            while(Thread.activeCount()>10);

        }
    }
}

结果:

Exception in thread "Thread-702" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 7
	at java.util.Vector.remove(Vector.java:827)
	at VectorTest$1.run(VectorTest.java:15)
	at java.lang.Thread.run(Thread.java:745)

    很显然,即使用到的方法都是同步的,但在多线程环境中,如果不在方法调用端做额外的同步措施,这段代码仍然是不安全的。要保证安全,就要在操作对象的地方加上同步操作。


           Thread removeThread = new Thread(new Runnable(){
                public void run(){
                   

 synchronized(vs){


                        for(int i=0;i<vs.size();i++){
                            try{Thread.sleep(1);}catch(Exception e){}
                            vs.remove(i);
                        }
                    }
                }
            });
            Thread printThread = new Thread(new Runnable(){
                public void run(){
                    

synchronized(vs){


                        for(int i=0;i<vs.size();i++){
                            System.out.println(vs.get(i));
                        }
                    }
                }
            });


3. 相对线程安全

   相对线程安全就是通常意义上的线程安全,它需要保证对这个对象单独的操作是线程安全的,在调用时不需要做额外的保障措施,但对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手机来保证调用的正确性。


4. 线程兼容

    线程兼容是指对象本身并不是线程安全的,但可以通过调用端正确地使用同步手机来保证对象在并发环境中安全地使用。即通常说一个类不是线程安全的情况。


5. 线程对立

    线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。


13.2.2 线程安全的实现方法

1. 互斥同步

    互斥同步是最常见的一种并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。而互斥是实现同步的一种手段。

    在Java里面,最基本的互斥同步手段是synchronzied关键字,synchronized关键字经过编译后,会在同步块的前后分别加上monitorenter和monitorexit这两个字节码指令,这两个指令都需要一个reference类型的参数指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronzied修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

public class SynTest{
    private Object o = new Object();
    //对当前对象的锁定
    public synchronized void method1(){
    }   

    public void method2(){
        synchronized(o){//对指定对象的锁定
        }   
    }   
    //对类对象的锁定
    public synchronized static void method3(){
    }   

}

通过命令:javap -c SynTest.class 可以看到synchronized块的指令:


    根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,执行monitorexit指令时,会将锁计数器减1,当计数器为0时,锁就被释放了。如果获取对象的锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

    在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点需要特别注意:

  1. synchronized同步块对同一条线程来说是可重入的,即不会出现自己把自己锁死的问题
  2. 同步块在已经进入的线程执行完之前,会阻塞后面其他线程的进入。

    除了synchronized外,还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。它们的异同为:

    相同点:都是可重入的
    不同点:synchronized是语法层面的互斥锁,ReentrantLock是API层面的互斥锁。

    另外ReentrantLock增加了一些高级功能:

  1. 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间长的同步块很有帮助
  2. 公平锁:多个线程在等待同一锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点。synchronized是非公平的,ReentrantLock默认也是非公平的,但可以通过构造方法要求使用公平锁。
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  3. 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次newCondtion()方法即可。

    性能方面,在JDK1.5及以前,在多线程环境下,并发越多,synchronized相对于ReentrantLock性能下降的越厉害。


    但在JDK1.6之后,再者的性能因素基本上完全持平了。

    综上所述,还是提倡在synchronized能实现需求的情况下优先考虑使用synchronized来进行同步。


2. 非阻塞同步

    互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也被称为阻塞同步(Blocking Synchronization)。另外,它属于一种悲观的并发策略。此外,还有一种基于冲突检测的乐观锁并发策略,通俗地说就是先进行操作,如果没有其他线程急用共享数据,那就操作成功了:如果共享数据有急用,产生了冲突,那就进行其他的补偿措施(如不断地重试),这种乐观锁策略的许多实现不需要把线程扶起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization)

    非阻塞同步一般基于硬件的CAS(比较并交换,Compare-and-Swap)指令,CAS是在JDK1.5之后才引用的,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供,虚拟机在内部 做了这些方法特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用过程,或者可以认为是无条件内联进去了。

    但Unsafe类不是提供给用户程序调用的类(只有启动类加载器加载的Class才能访问它),如果不通过反射手段,就只能通过其他的Java API来间接使用它,如并发包里的整数原子类

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * @param expect the expected value
 * @param update the new value
 * @return true if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}


3. 无同步方案

    要保证线程安全,并不一定要进行同步。同步只是保障共享数据争用时的正确性手机,如果一个方法本来就不涉及共享数据,那它自然就无须同步,因此会有一些代码天生就是线程安全的。如;

    可重入代码(Reentrant Code):也可以理解为无状态代码,即那些对相同的输入,总有相同输出的函数,它们一般有这样的特征:不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

    线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那看看是能在同一个线程中进行?如果可以,就可以把共享数据的可见范围限制在同一线程中,这样就无须同步也能保证线程之间不出现数据争用的问题。如经典的JavaEE中的request请求数据。

上一篇:12.4 Java与线程
下一篇:13.3 锁优化