异步编程学习之路(二)-通过Synchronize实现线程安全的多线程

本文是异步编程学习之路(二)-通过Synchronize实现线程安全的多线程,若要关注前文,请点击传送门:

异步编程学习之路(一)-通过Thread实现简单多线程(线程不安全)

上篇我们通过Thread实现了几种线程不安全的多线程写法,导致最后的结果与预期的值不一样。

一、通过Synchronize实现安全的多线程

/**
 * @Description:简单多线程的实现(线程安全)
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class TestThread {
 
    private int count = 0;

    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.calculation();
    }

    public void calculation() {
        List<Thread> threads = Lists.newArrayList();
        Runnable runnable = () -> {
            synchronized (this) {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            threads.add(thread);
            thread.start();
        }
        while (true) {
            if(this.allThreadFinish(threads)) {
                System.out.println(count);
                break;
            }
        }
    }

    public Boolean allThreadFinish(List<Thread> threads) {
        for (Thread thread : threads) {
            if (thread.isAlive()) {
                return false;
            }
        }
        return true;
    }
}

运行结果:

    100000

结果完全正确。

有人说通过volatile也能够实现安全的多线程,得到正确的结果,代码如下:

/**
 * @Description:简单多线程的实现(线程安全)
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class TestThread {
 
    private volatile int count = 0;

    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.calculation();
    }

    public void calculation() {
        List<Thread> threads = Lists.newArrayList();
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        };
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            threads.add(thread);
            thread.start();
        }
        while (true) {
            if(this.allThreadFinish(threads)) {
                System.out.println(count);
                break;
            }
        }
    }

    public Boolean allThreadFinish(List<Thread> threads) {
        for (Thread thread : threads) {
            if (thread.isAlive()) {
                return false;
            }
        }
        return true;
    }
}

运行结果:

    70267

正确结果应该是10万。

其实volatile关键字只能够保证多线程之间的内存可见性,而不能保证多个线程之间的有序性。 

为什么synchronize能够保证线程安全而volatile不行呢?

二、JMM内存模型

Java 内存模型来屏蔽掉各种硬件和操作系统的内存差异,达到跨平台的内存访问效果。JLS(Java语言规范)定义了一个统一的内存管理模型JMM(Java Memory Model)

Java内存模型规定了所有的变量都存储在主内存中,此处的主内存仅仅是虚拟机内存的一部分,而虚拟机内存也仅仅是计算机物理内存的一部分(为虚拟机进程分配的那一部分)。

Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。

每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下图:

Java内存间交互操作

JLS定义了线程对主存的操作指令:lock,unlock,read,load,use,assign,store,write。这些行为是不可分解的原子操作,在使用上相互依赖,read-load从主内存复制变量到当前工作内存,use-assign执行代码改变共享变量值,store-write用工作内存数据刷新主存相关内容。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

为什么需要要多线程 (充分利用CPU)

举一个栗子,假设现在要10000条数据,总共需要100分钟。如果是单线程的串行操作,需要100分钟。那么如果同时开10个线程,每一个线程运行100条数据,那么只需要10分钟就可以完成所有的操作。(总之是充分利用物理资源CPU)

三、多线程特性

1、原子性(Atomicity)
原子性是指一个原子操作在cpu中不可以暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子操作保证了原子性问题。

x++(包含三个原子操作)a.将变量x 值取出放在寄存器中 b.将将寄存器中的值+1 c.将寄存器中的值赋值给x

由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块---synchronized关键字,因此在synchronized块之间的操作也具备原子性。

2、可见性(Visibility)

java 内存模型的主内存和工作内存,解决了可见性问题。

volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。

3、有序性(Ordering)

Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

注意:

1. Java语言作为高级语言支持多线程的操作,主要是为了解决单线程因阻塞而带来的效率问题,同时也充分利用多核CPU的优势。使用多线程也带了问题,线程之间如何通信?线程之间如何和同步?

2. 线程之间的通信是依靠共享内存和线程方法的调用来实现。在多线程的体系下,Java的内存模型分为主内存和共享内存,通过内存之间的数据交换,依赖多线程的可见性,实现线程之间的通信;线程具有基本状态,主动调用线程的wait、notify方法也可以实现线程之间的通信。

3.线程的同步也是并发线程操作共享变量所带来的问题。多线程允许使用synchronize、volatile、ThreadLocal来保证多线程的安全。synchronize是一个重量级的锁,直接使得线程阻塞,单线程顺利执行,对于更新变量不会有并发操作,很大程度的降低的系统的性能。volatile是一个轻量级的原子锁,对于volatile修饰的变量,每一次的读和写,都必须和主内存交互,他禁止了编译器和处理器的一些重排序优化。

四、Volatile非线程安全的原因

示例中count变量在内存中如下图:

由上,我们可以得出以下结论。

  1. read和load阶段:从主存复制变量到当前线程工作内存;
  2. use和assign阶段:执行代码,改变共享变量值;
  3. store和write阶段:用工作内存数据刷新主存对应变量的值。

在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读时的可见性问题 ,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

四、Synchronize原理分析

synchronize是平时用的比较多的多线程问题的解决方案,但各位知道吗,synchronize也有失效的情况存在,下面我就逐个来分析synchronize失效的种种情况以及它的底层原理。

情况1:同一个对象在两个线程中分别访问该对象的两个同步方法。

结果:会产生互斥。

分析:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。

情况2:不同对象在两个线程中调用同一个同步方法。

结果:不会产生互斥。

分析:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙。

情况3:.Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”,用类直接在两个线程中调用两个不同的同步方法。

结果:会产生互斥。

分析:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。

情况4:用一个类的静态对象在两个线程中调用静态方法或非静态方法。

结果:会产生互斥。

分析:因为是一个对象调用,同上。

情况5:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法。

结果:不会产生互斥。

分析:因为虽然是一个对象调用,但是两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行。

情况6:一个对象,两个线程,对象方法是Runnable返回值,并且该方法上有synchronize关键字,两个线程都调用这个对象的这个方法返回的Runnable来做实际执行逻辑。

结果:synchronize同步锁失效。

分析:具体原因不明。

1、实践一,synchronize使用在一般方法内。

/**
 * @Description:Synchronized原理分析
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class User {
    public synchronized void test1(){
        try {
            System.out.println("test1 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test1 end");
    }
    public synchronized void test2(){
        try {
            System.out.println("test2 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test2 end");
    }
    public static void main(String[] args) {
        final User user = new User();
        new Thread(new Runnable() {
            @Override
            public void run() {
                user.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                user.test2();
            }
        }).start();
    }
}

运行结果:

test2 start
test2 end
test1 start
test1 end

分析:对于同一个对象user,可以看见对于普通方法的时,每一次只能执行一个方法,类似于串行执行的效果。 

2、实践二,synchronize使用在类方法内。

/**
 * @Description:Synchronized原理分析
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class Test {
    public synchronized static void test1(){
        System.out.println("test1 in");
        try{
            System.out.println("test1 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test1 end");
    }
    public synchronized static void test2(){
        System.out.println("test2 in");
        try{
            System.out.println("test2 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test2 end");
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test.test2();
            }
        }).start();
    }
}

运行结果:

test1 in
test1 start
test1 end
test2 in
test2 start
test2 end

分析:可以看见对于类方法,依然是起到了锁的作用,但是要注意,这边使用的仍然是Test这个Class,可以知道此时的锁加在了Class上了。 

3、实践三,synchronize混合使用与类方法和普通方法。

/**
 * @Description:Synchronized原理分析
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class Test {
    public synchronized static void test1(){
        System.out.println("test1 in");
        try{
            System.out.println("test1 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test1 end");
    }
    public synchronized void test2(){
        System.out.println("test2 in");
        try{
            System.out.println("test2 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test2 end");
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().test2();
            }
        }).start();
    }
}

运行结果:

test1 in
test1 start
test2 in
test2 start
test1 end
test2 end

分析:这边可以看出运行结果的不同了,发现两个方法并没有被锁住,而是同时执行了。这是为什么呢???这边先简单解释下吧。这是因为static test1方法是类方法,也就是它属于的是Class,而不是属于这个Class产生的具体的对象。而test2方法是由Class产生出的具体对象才能调用的,因此它属于的是Class产生的对象,而不是Class本身。这是两个概念,所以简单来说就是这两种情况锁的条件不一样,因此产生了锁不住的效果。。(具体原因需要分析源码或者底层原理才能给出更规范的解释)。

 4、实践四,synchronize修饰普通方法时重入的效果。

/**
 * @Description:Synchronized原理分析
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class Test {
 
    public synchronized void test1(){
        System.out.println("test1 in");
        try{
            System.out.println("test1 start");
            test2();
        }catch (Exception e){
        }
        System.out.println("test1 end");
    }
    public synchronized void test2(){
        System.out.println("test2 in");
        try{
            System.out.println("test2 start");
        }catch (Exception e){
        }
        System.out.println("test2 end");
    }
    public static void main(String[] args) throws Exception{
        final Test t1 = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.test1();
            }
        }).start();
    }
}

运行结果:

test1 in
test1 start
test2 in
test2 start
test2 end
test1 end

分析:此处可以发现synchronize是一个可重入锁,相信对于可重入的概念,c.u.t包下的ReentrantLock不陌生的同学是明白的。此处,如果synchronize不是可重入的话,那么再调用test2()的方法时应该会出现死锁。但是没有,说明是支持的,同样的,详细的原因需要分析底层原理,此处只是再通过例子来分析它的特性。

5、实践五,synchronize修饰普通方法时不同对象的执行的效果。

/**
 * @Description:Synchronized原理分析
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class Test {
    public synchronized void test1(){
        System.out.println("test1 in");
        try{
            System.out.println("test1 start");
            Thread.sleep(3000);
            test2();
        }catch (Exception e){
        }
        System.out.println("test1 end");
    }
    public synchronized void test2(){
        System.out.println("test2 in");
        try{
            System.out.println("test2 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test2 end");
    }
    public static void main(String[] args) throws Exception{
        final Test t1 = new Test();
        final Test t2 = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程:"+Thread.currentThread().getName());
                t1.test1();
            }
        },"t1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程:"+Thread.currentThread().getName());
                t2.test1();
            }
        },"t2").start();
    }
}

运行结果:

线程:t1
test1 in
test1 start
线程:t2
test1 in
test1 start
test2 in
test2 start
test2 in
test2 start
test2 end
test1 end
test2 end
test1 end

 分析:对于不同的对象,没有锁住,对于拿到t1对象的线程,它能够执行t1对象的同步方法,但是对于t2来说,t1的锁对他毫无用处,它依然能执行自己的同步方法。测试1就是对于同一个对象user,它在不同的线程中需要顺序执行,但是测试5却证明,不同的对象之间互相不影响。

 5、实践六,Runnable使用synchronize锁失效情况

/**
 * @Description:Synchronized原理分析
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 20:30:54
 * @Version:1.0
 */
public class User {
    public synchronized Runnable runnable(String name){
        Runnable runnable = () -> {
            System.out.println(name + " start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.ou.println(name + " end");
        };
        return runnable;
    }
    public synchronized void test2(){
        try {
            System.out.println("test2 start");
            Thread.sleep(3000);
        }catch (Exception e){
        }
        System.out.println("test2 end");
    }
    public static void main(String[] args) {
        final User user = new User();
        new Thread(user.runnable("test01")).start();
        new Thread(user.runnable("test02")).start();
    }
}

运行结果:

test01 start
test02 start
test01 end
test02 end

分析:对于相同的对象并在返回Runnable的方法上加锁,test01 start和test02 start一起打印出来可以知道synchronize并没有生效,不过实现原因不明,特此记录,以便日后查阅。

本文讲解了多线程之间如何实现同步运行,但是仅仅做到同步运行是不能处理实际工作中的具体业务的,我们还需要多个线程之间能够协同作业,比如说A线程运行得出M,B线程需要通过M来计算N,此时A和B相互之间是存在一定关系的,这个时候就需要A线程和B线程之间协同作业,详细讲解请见下文:

异步编程学习之路(三)-多线程之间的协作与通信

猜你喜欢

转载自blog.csdn.net/qq_19734597/article/details/85174886