011 线程安全性问题

一 . 概述

在之前,我们说到线程安全性问题是我们在并发设计首要考虑的问题.

  那么到底什么是并发问题呢?

  看下面的例子(经典的例子):      

public class Problem {
    
    private int count = 0;
    
    public static void main(String[] args) throws Exception {
        Problem demo = new Problem();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int x =0;x<10000;x++) {
                    demo.add();
                }
            }
            
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int x =0;x<10000;x++) {
                    demo.add();
                }
            }
            
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("获取最终的结果count : " + demo.count);
        
    }
    private void add() {
        count++;
    }
}

上面的代码描述了两个线程共同将一个count自增,每人1000次,但是最后运行的结果让我们大失所望,几乎每次都不是2000次.


 二 .问题的分析

  其实最根本的问题就是两个线程获取的count的状态不一致导致的,也就是说count++操作根本就不是原子性的操作,

    这就造成了线程可以获取到一个另外一个线程的中间状态.

  这可能很抽象,简单说,对count的操作应该是顺序的.


 三 .安全性问题发生的条件

  在上面我们展示了安全性问题,但是安全性问题何时会发生呢?毕竟,如何我们的程序中如果没有安全性问题,我们就减少了并发时所考虑的问题,这可以帮助我们简化问题.

  线程安全性发生的条件:

  [1]多线程并发情况

  [2]竞争相同资源

  [3]对竞争资源进行非原子操作[常见的就是读写不一致]

解释一个上面的条件:

  前面两个不去解释了.就对第三个条件进行解释.

  原子性:

    这个原子性的概念和我们在事务中的概念是一致的,那就是我们的操作应该是一个完整的个体,要么不发生,要么完全发生.在其中,不会被打断.

  拿上面的例子来说,count++这个操作就不是原子性的,因为这个语句在底层会分成多个语句执行,每个语句都是可以被打断的.


四 .问题的解决

  线程安全性问题的解决最常见的方法就是同步,当然同步的方式有很多种,

    比如加锁,synchronized,CAS等方式,但是他们的核心都是一点,保证原子性.


五 . JVM的内存结构

  java为了更快的提供运行速度,在每一个线程栈之中设置了缓存,由于缓存之中的不一致就会造成并发的安全性问题.

常常出现的就是这样的一个情景,一个线程将共享变量进行了修改,但是另外一个线程却还是使用的之前缓存之中的内容,也就是说,一个线程对共享变量的修改无法及时的反映到另外一个线程之中,这就是可见性问题.

  我们一般可以使用volatile关键词来完成线程的可见性问题.


六 .指令重排序

  jvm为了加速程序的执行,会对我们的代码进行指令重排序.这样造成的后果就是后面的代码未必比之前的代码后执行,也就是说代码的执行顺序是不确定的.

好像这种情况对我们造成了灾难性的后果,但是jvm为了解决这个问题,提供了happens-before的规范,jvm定义了一些列的执行顺序的规范,也就是说,.一些代码必须在另外一些代码之前运行.

我们可以通过happens-before来完成指令重排序的推导.

但是,我们更加常见的问题就是发生在共享变量访问的时候,此时不确定的指令重排序就可能造成安全性问题.

我们常用的解决方法就是happens-before中加锁的规范,加锁的代码一定保证指令的顺序性,这样我们就在一定的程度上抵消了指令重排序带来的问题.  

原文地址:https://www.cnblogs.com/trekxu/p/8995895.html