Java 并发编程:核心理论

Java并发编程系列:html

  并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能。它要求编程者对计算机最底层的运做原理有深入的理解,同时要求编程者逻辑清晰、思惟缜密,这样才能写出高效、安全、可靠的多线程并发程序。本系列会从线程间协调的方式(wait、notify、notifyAll)、Synchronized及Volatile的本质入手,详细解释JDK为咱们提供的每种并发工具和底层实现机制。在此基础上,咱们会进一步分析java.util.concurrent包的工具类,包括其使用方式、实现源码及其背后的原理。本文是该系列的第一篇文章,是这系列中最核心的理论部分,以后的文章都会以此为基础来分析和解释。安全

1、共享性多线程

  数据共享性是线程安全的主要缘由之一。若是全部的数据只是在线程内有效,那就不存在线程安全性问题,这也是咱们在编程的时候常常不须要考虑线程安全的主要缘由之一。可是,在多线程编程中,数据共享是不可避免的。最典型的场景是数据库中的数据,为了保证数据的一致性,咱们一般须要共享同一个数据库中数据,即便是在主从的状况下,访问的也同一份数据,主从只是为了访问的效率和数据安全,而对同一份数据作的副本。咱们如今,经过一个简单的示例来演示多线程下共享数据致使的问题:并发

代码段一:ide

package com.paddx.test.concurrent;

public class ShareData {
    public static int count = 0;

    public static void main(String[] args) {
        final ShareData data = new ShareData();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //进入的时候暂停1毫秒,增长并发问题出现的概率
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int j = 0; j < 100; j++) {
                        data.addCount();
                    }
                    System.out.print(count + " ");
                }
            }).start();

        }
        try {
            //主程序暂停3秒,以保证上面的程序执行完成
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count=" + count);
    }

    public void addCount() {
        count++;
    }
}

  上述代码的目的是对count进行加一操做,执行1000次,不过这里是经过10个线程来实现的,每一个线程执行100次,正常状况下,应该输出1000。不过,若是你运行上面的程序,你会发现结果却不是这样。下面是某次的执行结果(每次运行的结果不必定相同,有时候也可能获取到正确的结果):

 

能够看出,对共享变量操做,在多线程环境下很容易出现各类意想不到的的结果。

2、互斥性

  资源互斥是指同时只容许一个访问者对其进行访问,具备惟一性和排它性。咱们一般容许多个线程同时对数据进行读操做,但同一时间内只容许一个线程对数据进行写操做。因此咱们一般将锁分为共享锁和排它锁,也叫作读锁和写锁。若是资源不具备互斥性,即便是共享资源,咱们也不须要担忧线程安全。例如,对于不可变的数据共享,全部线程都只能对其进行读操做,因此不用考虑线程安全问题。可是对共享数据的写操做,通常就须要保证互斥性,上述例子中就是由于没有保证互斥性才致使数据的修改产生问题。Java 中提供多种机制来保证互斥性,最简单的方式是使用Synchronized。如今咱们在上面程序中加上Synchronized再执行:

代码段二:

package com.paddx.test.concurrent;

public class ShareData {
    public static int count = 0;

    public static void main(String[] args) {
        final ShareData data = new ShareData();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //进入的时候暂停1毫秒,增长并发问题出现的概率
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int j = 0; j < 100; j++) {
                        data.addCount();
                    }
                    System.out.print(count + " ");
                }
            }).start();

        }
        try {
            //主程序暂停3秒,以保证上面的程序执行完成
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count=" + count);
    }

    /**
     * 增长 synchronized 关键字
     */
    public synchronized void addCount() {
        count++;
    }
}

  如今再执行上述代码,会发现不管执行多少次,返回的最终结果都是1000。

3、原子性

  原子性就是指对数据的操做是一个独立的、不可分割的总体。换句话说,就是一次操做,是一个连续不可中断的过程,数据不会执行的一半的时候被其余线程所修改。保证原子性的最简单方式是操做系统指令,就是说若是一次操做对应一条操做系统指令,这样确定能够能保证原子性。可是不少操做不能经过一条指令就完成。例如,对long类型的运算,不少系统就须要分红多条指令分别对高位和低位进行操做才能完成。还好比,咱们常用的整数 i++ 的操做,其实须要分红三个步骤:(1)读取整数 i 的值;(2)对 i 进行加一操做;(3)将结果写回内存。这个过程在多线程下就可能出现以下现象:

这也是代码段一执行的结果为何不正确的缘由。对于这种组合操做,要保证原子性,最多见的方式是加锁,如Java中的Synchronized或Lock均可以实现,代码段二就是经过Synchronized实现的。除了锁之外,还有一种方式就是CAS(Compare And Swap),即修改数据以前先比较与以前读取到的值是否一致,若是一致,则进行修改,若是不一致则从新执行,这也是乐观锁的实现原理。不过CAS在某些场景下不必定有效,好比另外一线程先修改了某个值,而后再改回原来值,这种状况下,CAS是没法判断的。

4、可见性

   要理解可见性,须要先对JVM的内存模型有必定的了解,JVM的内存模型与操做系统相似,如图所示:

  

从这个图中咱们能够看出,每一个线程都有一个本身的工做内存(至关于CPU高级缓冲区,这么作的目的仍是在于进一步缩小存储系统与CPU之间速度的差别,提升性能),对于共享变量,线程每次读取的是工做内存中共享变量的副本,写入的时候也直接修改工做内存中副本的值,而后在某个时间点上再将工做内存与主内存中的值进行同步。这样致使的问题是,若是线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所作的修改。经过下面这段程序咱们能够演示一下不可见的问题:

package com.paddx.test.concurrent;

public class VisibilityTest {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (!ready) {
                System.out.println(ready);
            }
            System.out.println(number);
        }
    }

    private static class WriterThread extends Thread {
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            number = 100;
            ready = true;
        }
    }

    public static void main(String[] args) {
        new WriterThread().start();
        new ReaderThread().start();
    }
}

从直观上理解,这段程序应该只会输出100,ready的值是不会打印出来的。实际上,若是屡次执行上面代码的话,可能会出现多种不一样的结果,下面是我运行出来的某两次的结果:

固然,这个结果也只能说是有多是可见性形成的,当写线程(WriterThread)设置ready=true后,读线程(ReaderThread)看不到修改后的结果,因此会打印false,对于第二个结果,也就是执行if (!ready)时尚未读取到写线程的结果,但执行System.out.println(ready)时读取到了写线程执行的结果。不过,这个结果也有多是线程的交替执行所形成的。Java 中可经过Synchronized或Volatile来保证可见性,具体细节会在后续的文章中分析。

5、有序性

  为了提升性能,编译器和处理器可能会对指令作重排序。重排序能够分为三种:

  (1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。

  (2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。
  (3)内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。

  咱们能够直接参考一下JSR 133 中对重排序问题的描述:

  

        (1)                    (2)

先看上图中的(1)源码部分,从源码来看,要么指令 1 先执行要么指令 3先执行。若是指令 1 先执行,r2不该该能看到指令 4 中写入的值。若是指令 3 先执行,r1不该该能看到指令 2 写的值。可是运行结果却可能出现r2==2,r1==1的状况,这就是“重排序”致使的结果。上图(2)便是一种可能出现的合法的编译结果,编译后,指令1和指令2的顺序可能就互换了。所以,才会出现r2==2,r1==1的结果。Java 中也可经过Synchronized或Volatile来保证顺序性。

六 总结

  本文对Java 并发编程中的理论基础进行了讲解,有些东西在后续的分析中还会作更详细的讨论,如可见性、顺序性等。后续的文章都会以本章内容做为理论基础来讨论。若是你们可以很好的理解上述内容,相信不管是去理解其余并发编程的文章仍是在平时的并发编程的工做中,都可以对你们有很好的帮助。

相关文章
相关标签/搜索