从多线程的三个特性理解多线程开发

       工做中许多地方须要涉及到多线程的设计与开发,java多线程开发当中咱们为了线程安全所作的任何操做其实都是围绕多线程的三个特性:原子性、可见性、有序性展开的。针对这三个特性的资料网上已经不少了,在这里我但愿在站在便于理解的角度,用相对直观的方式阐述这三大特性,以及为何要实现和知足三大特性。java

1、原子性数组

原子性是指一个操做或者一系列操做要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。其实这句话就是在告诉你,若是有多个线程执行相同一段代码时,而你又可以预见到这多个线程相互之间会影响对方的执行结果,那么这段代码是不知足原子性的。结合到实际开发当中,若是代码中出现这种状况,大几率是你操做了共享变量。安全

针对这个状况网上有个很经典的例子,银行转帐问题:多线程

好比A和B同时向C转帐10万元。若是转帐操做不具备原子性,A在向C转帐时,读取了C的余额为20万,而后加上转帐的10万,计算出此时应该有30万,但还将来及将30万写回C的帐户,此时B的转帐请求过来了,B发现C的余额为20万,而后将其加10万并写回。而后A的转帐操做继续——将30万写回C的余额。这种状况下C的最终余额为30万,而非预期的40万。 若是A和B两个转帐操做是在不一样的线程中执行,而C的帐户就是你要操做的共享变量,那么不保证执行操做原子性的后果是十分严重的。并发

OK,上面的情况咱们理清楚了,由此能够引伸出下列三个问题app

一、哪些是共享变量ide

从JVM内存模型的角度上讲,存储在堆内存上数据都是线程共享的,如实例化的对象、全局变量、数组等。存储在线程栈上的数据是线程独享的,如局部变量、操做栈、动态连接、方法出口等信息。工具

举个通俗的例子,若是你的执行方法至关于作菜,你能够认为每一个线程都是一名厨师,方法执行时会在虚拟机栈中建立栈帧,至关于给每一个厨师分配一个单独的厨房,作菜也就是执行方法的过程当中须要不少资源,里面的锅碗瓢盆各类工具,就诸如你在方法内的局部变量是每一个厨师独享的;但若是须要使用水电煤气等公共资源,就诸如全局变量通常是共享的,使用时须要保证线程安全。优化

二、哪些是原子操做spa

既然是要保证操做的原子性,如何判断个人操做是否符合原子性呢,一段代码确定是不符合原子性的,由于它包含不少步操做。但若是只是一行代码呢,好比上面的银行转帐的例子若是没有这么复杂,共享变量“C的帐户”只是一个简单的count++操做呢?针对这个问题,首先咱们要明确,看起来十分简单的一句代码,在JMM(java线程内存模型)中多是须要多步操做的。

先来看一个经典的例子:使用程序实现一个计数器,指望获得的结果是1000,代码以下:

public class threadCount {
     public volatile static int count = 0; 
     public static void main( String[] args ) throws InterruptedException {
          ExecutorService threadpool = Executors.newFixedThreadPool(1000);
            for (int i = 0; i < 1000; i++) {
                threadpool.execute(new Runnable() {
                    @Override
                    public void run() {
                        count++;
                    }
                });
            }
            threadpool.shutdown();
            //保证提交的任务所有执行完毕
            threadpool.awaitTermination(10000, TimeUnit.SECONDS);
            System.out.println(count);
     }
}

运行程序你能够看到,输出的结果并不每次都是指望的1000,这正是由于count++不是原子操做,线程不安全致使的错误结果。

实际上count++包含2个操做,首先它先要去读取count的值,再将count的值写入工做内存,虽然读取count的值以及将count的值写入工做内存 2个操做都是原子性操做,但合起来就不是原子性操做了。

在JMM中定义了8中原子操做,以下图所示,原子性变量操做包括read、load、assign、use、store、write,其实你能够理解为只有JMM定义的一些最基本的操做是符合原子性的,若是须要对代码块实行原子性操做,则须要JMM提供的lock、unlock、synchronized等来保证。

 

三、如何保证操做的原子性

使用较多的三种方式:

内置锁(同步关键字):synchronized;

显示锁:Lock;

自旋锁:CAS;

固然这三种实现方式和保证同步的机制上都有所不一样,在这里咱们不作深刻的说明。

2、可见性

可见性是一种复杂的属性,由于可见性的错误一般比较隐蔽而且违反咱们的直觉。

咱们看下面这段代码

public class VolatileApp {
    //volatile
    private static boolean isOver = false;

    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isOver) {
                    //Thread.yield();
                }
                System.out.println(number);
            }
        });
        thread.start();
        Thread.sleep(1000);
        number = 50;
        isOver = true;
    }

}

若是你直接运行上面的代码,那么你永远也看不到number的输出的,线程将会无限的循环下去。你可能会有疑问代码当中明明已经把isOver设置为了false,为何循环还不会中止呢?这正是由于多线程之间可见性的问题。在单线程环境中,若是向某个变量写入某个值,在没有其余写入操做的影响下,那么你总能取到你写入的那个值。然而在多线程环境中,当你的读操做和写操做在不一样的线程中执行时,状况就并不是你想象的理所固然,也就是说不知足多线程之间的可见性,因此为了确保多个线程之间对内存写入操做的可见性,必须使用同步机制。

咱们来看下JMM(java线程内存模型):

JMM规定多线程之间的共享变量存储在主存中,每一个线程单独拥有一个本地内存( 逻辑概念),本地内存存储线程操做的共享变量副本;
  • JMM中的变量指的是线程共享变量(实例变量,static字段和数组元素),不包括线程私有变量(局部变量和方法参数);
  • JMM规定线程对变量的写操做都在本身的本地内存对副本进行,不能直接写主存中的对应变量;
  • 多线程间变量传递经过主存完成(Java线程通讯经过共享内存),线程修改变量后经过本地内存写回主存,从主存读取变量,彼此不容许直接通讯(本地内存私有缘由);
综上,JMM经过控制主存和每一个线程的本地内存的数据交互,保证一致的内存可见性;也就是说线程之间“变量的共享”都须要经过刷新主内存,其余线程读取来完成,而一旦没法保证这个动做完成,多个线程之间是没法及时获取共享变量的变化的。那么咱们怎么知道何时工做内存的变量会刷写到主内存当中呢?这其实要基于java的happens-before原则(先行发生原则),这也也与多线程的有序性相关,咱们放到后面阐述。

volatile

保证线程之间可见性的手段有多种,在上面的代码中,咱们就能够经过volatile修饰静态变量来保证线程的可见性。

你能够把volatile变量看做一种削弱的同步机制,它能够确保将变量的更新操做通知到其余线程;使用volatile保证可见性相比通常的同步机制更加轻量级,开销也相对更低。

其实这里还有另一种状况,若是上面的代码中你撤销对Thread.yield()的注释,你会发现即使没有volatile的修饰两个静态变量 ,number也会正常打印输出了,乍一看你会觉得可见性是没有问题的,其实否则,这是由于Thread.yield()的加入,使JVM帮助你完成了线程的可见性。

下面这段段话阐述的比较明确:

程序运行中,JVM会尽力保证内存的可见性,即使这个变量没有加同步关键字。换句话说,只要CPU有时间,JVM会尽力去保证变量值的更新。这种与volatile关键字的不一样在于,volatile关键字会强制的保证线程的可见性。而不加这个关键字,JVM也会尽力去保证可见性,可是若是CPU一直有其余的事情在处理,它也没办法。也就是说Thread.yield()的加入,线程让出了一部分执行时间,使CPU从一直被while循环占用中占分配出了一些时间给JVM,这才可以保证线程的可见性。
因此说若是你不用volatile变量强制保证线程的可见性,虽然运行结果可能符合预期,也并不表明程序是线程安全的,你的程序会在有“隐患”的状态下运行,出现问题也很差排查与处理。

3、有序性

理解多线程的有序性实际上是比较困难的,由于你很难直观的去观察到它。

有序性的本义是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。可是在Java内存模型中,是容许编译器和处理器对指令进行重排序的,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。也就是说在多线程中代码的执行顺序,不必定会与你直观上看到的代码编写的逻辑顺序一致。

下面咱们举个简单的例子:

线程A:

context = loadContext(); //语句1
inited = true; //语句2

线程B:

while(!inited ){
 sleep
}
doSomethingwithconfig(context);

线程A中的代码中语句1与语句2之间没有必然的联系,因此线程A是会发生重排序问题的,也就是说语句2会在语句1以前执行,这必然会影响到线程B的执行(context没有实例化)。

其实指令的重排序之因此抽象难懂,由于它是一种较为底层的行为,是基于编译器对你代码进行深层优化的一种结果,结合上面的例子若是loadContext()中存在阻塞的话,优先执行语句2能够说是一种合理的行为。

4、happen-before规则

上面咱们也提到了,多线程的可见性与有序性之间实际上是有联系的,若是程序没有按你但愿的顺序执行,那么可见性也就无从谈起。JMM(Java 线程内存模型) 中的 happen-before规则,该规则定义了 Java 多线程操做的有序性和可见性,防止了编译器重排序对程序结果的影响。

按照官方的说法:

当一个变量被多个线程读取而且至少被一个线程写入时,若是读操做和写操做没有happen-before关系,则会产生数据竞争问题。 要想保证操做 B 的线程看到操做 A 的结果(不管 A 和 B 是否在一个线程),那么在 A 和 B 之间必须知足 HB 原则,若是没有,将有可能致使重排序。 当缺乏 happen-before关系时,就可能出现重排序问题。

简单来讲能够理解为在JMM中,若是一个的线程执行的结果须要对另外一个对另外一个线程B可见,那么这两个线程A操做与线程B操做之间必须存在happens-before关系。happens-before规则以下:
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做;
2.锁定规则:一个unLock操做先行发生于后面对同一个锁额lock操做;
3.volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做;
4.传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C;
5.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

从上面的规则中咱们能够看到,使用synchronized、volatile,加锁lock等方式通常及能够保证线程的可见性与有序性。

经过以上对多线程三大特性的总结,能够看出多线程开发中线程安全问题主要是基于原子性、可见性、有序性实现的,在这里我根据本身的理解进行了一下简单整理和阐述,自我感受仍是比较浅显的,若有不足之处还望指出与海涵。

相关文章
相关标签/搜索