首页 > 编程语言 > 详细

线程安全

时间:2019-06-26 20:54:05      阅读:102      评论:0      收藏:0      [点我收藏+]

  线程安全有关的的几个概念,先引用大牛Jakob Jenkov文章的一个段落说明下竞态条件和临界区

   原文链接      作者:Jakob Jenkov

  在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

  多线程同时执行下面的代码可能会出错:

1 public class Counter {
2     protected long count = 0;
3     public void add(long value){
4         this.count = this.count + value;   
5     }
6 }

  竞态条件 & 临界区:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。


  共享资源:多个线程共享的变量,数组,或对象等等程序执行需要的资源。  

  不可变对象:  创建不可改变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全(因为没有写操作)。例如不提供类私有属性的set方法,则属性被创建后就不可修改。

  原子操作定义:原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可被打乱,也不可被切割只执行其中的一部分(不可中断性)。将整个操作视为一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

   由以上的概念可知,若要避免临界区的代码产生竞态条件,我们可以使临界区代码实现原子性,让临界区代码的多个步骤不可被切割执行,即多线程顺序的访问修改共享资源。

  这里要说明下如上例中的add()方法,虽然只有一行代码,但它并不是原子操作,JVM不是将这段代码视为单条指令来执行,而是按照下面的顺序执行:

  ①从内存获取 this.count 的值放到寄存器
  ②将寄存器中的值增加value   
  ③将寄存器中的值写回内存

  我们无法知道操作系统何时会在两个线程之间切换,那就会有这样的问题:

  1.线程A从内存获取到this.count的值(0)放入寄存器

  2.线程A将寄存器中的值加value,假设这里value为2

  3.此时操纵系统切换为线程B

  4.线程B从内存获取this.count的值放到寄存器,由于此时线程A并未完成将寄存器中的值写回内存的操作,故线程B取到的this.count的值仍然为0

  5.然后线程B将寄存器中的值加value,假设这里value为3

  6.最后无论是线程A先将寄存器的值回写内存,还是线程B先回写,先回写的那个值将会被覆盖掉,count的值只能是2或者3,而不会得到我们想要的两个线程结果相加的值5

  而我们要做的是使①②③这三个步骤实现原子性,当A线程确定全部操作完成时,B线程执行再读取内存的值就会是已经完成加2操作并回写到内存的值2,这样最终的结果就会是2+3=5

 

  实现原子性的方式:循环+CAS;锁

  CAS机制:

  Compare and Swap  比较和交换。属于硬件同步原语(硬件层次的支持)。处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

  以下代码示例描述的是多线程(两个线程)共同对变量i进行递增操作。

 1 public class UnsafeDemo {
 2     private static int i = 0;
 3     public static void main(String[] args) throws InterruptedException {
 4         for (int j = 0; j < 2; j++) {
 5             new Thread(() -> {
 6                 for (int k = 0; k < 10000; k++) {
 7                     int oldvalue = i;
 8                     int newvalue = oldvalue +1;
 9                     if(oldvalue == i){
10                         i = newvalue;
11                     }
12                 }
13             }).start();
14         }
15         TimeUnit.SECONDS.sleep(2);
16         System.out.println("value of i :"+i);
17     }
18 }

 

  该段代码的7~11行模拟了CAS的逻辑,即在计算前保存旧值,计算后比较旧值是否与变量 相同,如果不同则表示在此期间其他的线程已经对变量 进行了修改,i 的值发生了改变,则本次计算无效。

显然由于代码无法保证原子性,所以这段代码只能是模拟CAS的操作逻辑,真正的实现我们需要用到JAVA中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS。请看如下代码示例:

 1 public class UnsafeDemo {
 2     private volatile static int i = 0;
 3     private static Unsafe unsafe;
 4     private static long valueoffset; //属性在类的内存中相对于该类首地址的偏移量
 5 
 6     static {
 7         try {
 8             //利用反射获取Unsafe对象,无法通过Unsafe.getUnsafe()方法获取,因为该类加载时会判断加载器是否可信任,只有JDK源码中可以通过Unsafe.getUnsafe()方法获取。
 9             Field thUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
10             thUnsafe.setAccessible(true);
11             unsafe = (Unsafe) thUnsafe.get(null);
12             //Java语言不能直接获取内存相关信息,所以通过Unsafe的方法,获取类属性相对于类对象的偏移量
13             valueoffset = unsafe.staticFieldOffset(UnsafeDemo.class.getDeclaredField("i"));
14         } catch (Exception e) {
15             e.printStackTrace();
16         }
17     }
18 
19     public static void add() {
20         int oldvalue;
21         int newvalue;
22         do {
23             oldvalue = unsafe.getIntVolatile(UnsafeDemo.class, valueoffset); // 读取当前值,直接赋值oldvalue = i 也可以
24             newvalue = oldvalue + 1;
25         } while (!unsafe.compareAndSwapInt(UnsafeDemo.class, valueoffset, oldvalue, newvalue));
26     }
27 
28     public static void main(String[] args) throws InterruptedException {
29         for (int j = 0; j < 2; j++) {
30             new Thread(() -> {
31                 for (int k = 0; k < 10000; k++) {
32                     add();
33                 }
34             }).start();
35         }
36         TimeUnit.SECONDS.sleep(2);
37         System.out.println("value of i :" + i);
38     }
39 }

   CAS 的问题:

  1.循环+CAS,自旋的实现让所有的线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。

  2.仅针对单个变量的操作,不能用于多个变量来实现原子操作。

  3.ABA问题,无法体现出数据的变动。

 

  为了方便开发,JDK提供了封装了原子操作的java.util.concurrent.atomic包,我们可以利用该包下的AtomicInteger类实现增加的原子操作。但追踪其实现原理也是利用了Unsafe 类的CAS操作。

 1 public class AtomicDemo {
 2     private static AtomicInteger i = new AtomicInteger(0);
 3 
 4     public static void add() {
 5        i.getAndIncrement();
 6     }
 7 
 8     public static void main(String[] args) throws InterruptedException {
 9         for (int j = 0; j < 2; j++) {
10             new Thread(() -> {
11                 for (int k = 0; k < 10000; k++) {
12                     add();
13                 }
14             }).start();
15         }
16         TimeUnit.SECONDS.sleep(2);
17         System.out.println("value of i :" + i);
18     }
19 }

  JDK1.8之后,还提供了增强版

  更新器:DoubleAccumulator   LongAccumulator

  计数器:LongAdder    DoubleAdder

  高并发下性能更好,适用于更新频繁但是读取汇总信息不太频繁的场景。原理是将操作分为多个单元,不同线程更新不同的单元,只有需要汇总的时候才计算所有单元的操作总和。

 

  CAS的ABA问题描述如下图示例:栈中的每个元素Node都保有指向下一个元素的引用。

 技术分享图片

技术分享图片

 技术分享图片

 

线程安全

原文:https://www.cnblogs.com/peripateticism/p/11084240.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!