深刻理解JVM(③)再谈线程安全

前言

咱们在编写程序的时候,通常是有个顺序的,就是先实现再优化,并非全部的牛P程序都是一次就写出来的,确定都是不断的优化完善来持续实现的。所以咱们在考虑实现高并发程序的时候,要先保证并发的正确性,而后在此基础上来实现高效。因此线程安全是高并发程序首先须要保证的。java

线程安全定义

对于线程安全的定义能够理解为:当多个线程同时访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以得到正确的结果,那就称这个对象是线程安全的
这个定义是很严谨且有可操做性,它要求线程安全的代码都必须具有一个共同特征:代码自己封装了全部必要的正确性保障手段(互斥、同步等),令调用者无须关心多线程下的调用问题,更无须本身实现任何措施来保证多线程环境下的正确调用。程序员

Java中的线程安全

要讨论Java中的线程安全,咱们要以多个线程之间存在共享数据访问为前提。咱们能够不把线程安全看成一个非真即假的二元排他选项来看待,而是按照线程安全的“安全程度”由强至弱来排序,将Java中各操做共享的数据分为如下五类:不可变、绝对线程安全、相对相对安全、线程兼容和线程对立数据库

不可变

Java内存模型中,不可变的对象必定是线程安全的,不管对象的方法实现仍是方法的调用者,都不须要再进行任何线程安全保障措施。在学习Java内存模型这一篇文章中咱们在介绍Java内存模型的三个特性的可见性的时候说到,被final修饰的字段在构造器中一旦被初始化完成,而且构造器没有吧“this”的引用传递出去,那么在其余线程中就能看见final字段的值。而且外部可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最直接、最纯粹的。数组

在Java中若是共享数据是一个基本类型,那么在定义时使用final修饰它就能够保证它是不可变的。若是共享数据是一个对象,那就须要对象自行保证其行为不会对其状态产生任何影响才行。例如java.lang.String类的对象实例,它的substring()、replace()、concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响本身状态的途径有不少种,最简单的一种就是把对象里面带有状态的变量都声明为final,这样在构造函数结束后,他就是不可变的。
例如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;
}

除了String以外,还有枚举类型以及java.lang.Number的部分子类,如LongDouble等数值包装类型、BigIntegerBigDecimal等大数据类型。服务器

绝对线程安全

绝对线程安全是可以彻底知足上面的线程安全的定义,这个绝对线程安全的定义是很严格的:“无论运行时环境如何,调用者都不须要任何额外的同步措施”。Java的API中标注本身是线程安全的类,大多数都不是绝对的线程安全。
例如java.util.Vector是一个线程安全的容器,相信全部的Java程序员对此都不会有异议,由于它的add()、get()、和size()等方法都被synhronized修饰。可是这样并不意味着调用它的时候,就永远再也不须要同步手段了。多线程

public class VectorTest {

    private static Vector<Integer> vector = new Vector<Integer>();

    public static void main(String[] args){
        while (true){
            for (int i=0;i<10;i++){
                vector.add(i);
            }

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=0;i<vector.size();i++){
                        vector.remove(i);
                    }
                }
            });

            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<vector.size();i++){
                        System.out.println(vector.get(i));
                    }
                }
            });

            removeThread.start();
            printThread.start();

            while (Thread.activeCount() > 20);
        }
    }

}

运行结果:架构

Exception in thread "Thread-653" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 18
	at java.util.Vector.get(Vector.java:748)
	at com.eurekaclient2.test.jvm3.VectorTest$2.run(VectorTest.java:33)
	at java.lang.Thread.run(Thread.java:748)

经过上述代码的例子,就能够看出来,尽管Vector的get()、remove()和size()方法都是同步的,可是在多线程的环境中,若是调用端不作额外的同步措施,使用这段代码仍然是不安全的。由于在并发运行中,若是提早删除了一个元素,然后面还要去打印它,就会抛出数组越界的异常。
若是非要这段代码正确执行下去,就必须把removeThreadprintThread进行加锁操做。并发

Thread removeThread = new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (vector){
            for (int i=0;i<vector.size();i++){
                vector.remove(i);
            }
        }
    }
});

Thread printThread = new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (vector){
            for(int i=0;i<vector.size();i++){
                System.out.println(vector.get(i));
            }
        }
    }
});

相对线程安全

相对线程安全就是咱们一般意义上所讲的线程安全,它须要保证对这个对象单词的操做时线程安全的,咱们在调用的时候不须要进行额外的保证措施,可是对于一些特定顺序的连续调用,就可能须要在调用端使用额外的同步手段来保证调用的正确性。上面的代码例子就是相对线程安全的案例。jvm

线程兼容

线程兼容是指对象自己并不线程安全的,可是能够经过在调用端正确地使用同步手段来保证对象在并发环境中能够安全地使用。Java类库API中大部分的类都是线程兼容的,如ArrayListHashMap等。

线程对立

线程对立是指无论调用端是否采用了同步措施,都没法在多线程环境中并发是使用代码。因为Java语言天生就支持多线程的特性,此案从对立这种排斥多线程的代码时不多出现的,并且一般都是有害的,应当尽可能避免。

线程安全的实现方法

Java虚拟机为实现线程安全,提供了同步和锁机制,在了解了Java虚拟机线程安全措施的原理与运做过程,再去用代码实现线程安全就不是一件困难的事情了。

互斥同步

互斥同步(Mutual Exclusion & Synchronization)是一种常见也是最主要的并发正确性保障手段。
同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用
互斥是指实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式

在Java里,最基本的互斥同步手段就是synchronized关键字,这是一种块结构的同步语法。在Java代码里若是synchronized明确指定了对象参数,那就以这个对象的引用做为reference;若是没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例仍是取类型对应的Class对象来做为线程要持有的锁。

在使用sychronized时须要特别注意的两点:

  • synchronized修饰的同步块对同一条线程来讲是可重入的。这意味着同一线程反复进入同步块也不会出现本身把本身锁死的状况。
  • synchronized修饰的同步块在持有锁的线程执行完毕并释放锁以前,会无条件地阻塞后面其余线程的进入。这意味着没法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也没法强制正在等待锁的线程中断等待或超时退出。

除了synchronized关键字之外,自JDK5起,Java类库中新提供了java.util.concurrent包(J.U.C包),其中java.util.concurrent.locks.Lock接口便成了Java的另外一种全新的互斥同步手段。

重入锁(ReentrantLock)是Lock接口最多见的一种实现,它与synchronized同样是可重入的。在基本用法是,ReentrantLocksynchronized很类似,只是代码写法上稍有区别而已。
可是ReentrantLocksynchronized相比增长了一些高级特性,主要有如下三项:

  • 等待可中断:是指当持有锁的线程长期不释放的时候,正在等待的线程能够选择放弃等待,改成处理其余事情。可中断特性对处理执行时间很是长的同步颇有帮助。
  • 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次得到锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会得到锁。
    synchronized是非公平锁,ReentrantLock在默认状况系也是非公平锁,但能够经过构造函数的参数设置成公平锁,不过一旦设置了公平锁,ReentrantLock性能急剧降低,会明显影响性能。
  • 锁绑定多个条件:是指一个ReentrantLock对象能够同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合能够实现一个隐含条件,若是要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样作,屡次调用newCondition()方法便可。

虽说ReentrantLock比synchronized增长了一些高级特性,可是从JDK6对synchronized作了不少的优化后,他俩的性能其实几乎相差无几了。而且在如下的几种状况下虽然synchronized和ReentrantLock均可以知足需求时,建议优先使用synchronized

  • synchronized是在Java语法层面的同步,清晰简单。而且被普遍熟知,但J.U.C中的Lock接口并不是如此。所以在只须要基础的同步功能时,更推荐synchronized
  • Lock应该确保在finally块中释放锁,不然一旦受同步保护的代码块中抛出异常,则有可能永远不释放持有的锁。
  • 尽管在JDK5时代ReentrantLock曾经在性能上领先过synchronized,但这已是十多年以前的胜利。从长远看,Java虚拟机更容易针对synchronized来进行优化,由于Java虚拟机能够在线程和对象的元数据中记录synchronized中锁的相关信息。

非同步阻塞

互斥同步面临的主要问题时进行线程阻塞和唤醒所带来的性能开销,所以这种同步也被称为阻塞同步(Blocking Synchronized)。从解决问题的角度来看,互斥同步是一种悲观的并发策略,不管共享的数据是否真的会出现竞争,都会进行加锁。
随着硬件指令集的发展,出现了另外一种选择,基于冲突检测的乐观并发策略,通俗地说就是无论风险,先进行操做,发生了冲突,在进行补偿,最经常使用的补偿就是不断重试,直到出现没有竞争的数据为止。使用这种乐观并发策略再也不须要线程阻塞挂起,所以这种同步操做被称为非阻塞同步(Non-Blocking Synchronized)

在进行操做和冲突检测时这个步骤要保证原子性,硬件能够只经过一条处理器指令就能完成,这类指令经常使用的有:

  • 测试并设置(Test and Set);
  • 获取并增长(Fetch and Increment);
  • 交换(Swap);
  • 比较并交换(Compare adn Swap,简称CAS)
  • 加载连接/条件存储(Load-Linked/Store-Conditional,简称LL/SC)

Java类库从JDK5以后才开始使用CAS操做,而且该操做有sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。可是Unsafe的限制了不提供给用户调用,所以在JDK9以前只有Java类库可使用CAS,譬如J.U.C包里面的整数原子类,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操做来实现。直到JDK9,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操做

下面来看一个例子
在这里插入图片描述
这是以前的一个例子在验证volatile变量不必定彻底具有原子性的时候的代码。20个线程自增10000次的操做最终的结果一直不会获得200000。若是按以前的理解就会把race++操做或increase()方法用同步块包起来。

可是若是改为下面的代码,效率将会提升许多。

public class AtomicTest {

    public static AtomicInteger race = new AtomicInteger(0);

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

    private static final int THREADS_COUNT = 20;
    
    public static void main(String[] args) throws Exception{
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0;i<THREADS_COUNT;i++){
            threads[i] = new Thread(() -> {
                for(int i1 = 0; i1 <10000; i1++){
                    increase();
                }
            });

            threads[i].start();
        }

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

        System.out.println(race);
    }

}

运行效果:

200000

使用哦AtomicInteger代替int后,获得了正确结果,主要归功于incrementAndGet()方法的原子性,incrementAndGet()使用的就是CAS,在此方法内部有一个无限循环中,不断尝试讲一个比当前值大一的新值赋值给本身。若是失败了,那说明在执行CAS操做的时候,旧值已经发生改变,因而再次循环进行下一次操做,直到设置成功为止。

无同步方案

要保证线程安全,也不必定非要用同步,线程安全与同步没有必然关系,若是能让一个方法原本就不涉及共享数据,那它天然就不须要任何同步措施去保证正确性,所以有一些代码天生就是线程安全的,主要有这两类:
可重入代码:是指能够在代码执行的任什么时候刻中断它,而后去执行另一段代码,而控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。
可重入代码有一些共同特征:
不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数传入,不调用非可重入的方法等
简单来讲就是一个原则:若是一个方法的返回结果是能够预测的,只要输入了相同的数据,就能返回相同的结果,那它就知足可重入性的要求,固然也就是线程安全的
线程本地存储(Thread Local Storage)若是一段代码中所需的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。若是能,就能够把共享数据的可见范围限制在同一个线程内,这样无须同步也能保证线程之间不出现数据争用的问题
如大部分使用消费队列的架构模式,都会将产品的消费过程限制在一个线程中消费完,最经典一个实例就是Web交互模式中的“一个请求对应一个服务器线程”的处理方式,这种处理方式的普遍应用使得不少Web服务端应用均可以使用线程本地存储来解决线程安全问题。

.

相关文章
相关标签/搜索