慕课网高并发实战(四)- 线程安全性

定义

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确行为,那么就称这个类是线程安全的

线程安全性的三个方面

原子性-Atomic包

image.png

1.AtomicXXX:CAS 、Unsafe.compareAndSwapInt

看一下AtomicInteger.getAndIncrement的源码

/**    * Atomically increments by one the current value.    *    *@returnthe previous value    */

public

final intgetAndIncrement(){// 主要是调用了unsafe的方法 //  

  private static final Unsafe unsafe = Unsafe.getUnsafe();

return

unsafe.getAndAddInt(this, valueOffset,1);    }

/***  获取底层当前的值并且+1*@paramvar1 需要操作的AtomicInteger 对象*@paramvar2 当前的值 *@paramvar4 要增加的值*/publicfinalintgetAndAddInt(Object var1,longvar2,intvar4){intvar5;do{// 获取底层的该对象当前的值var5 =this.getIntVolatile(var1, var2);// 获取完底层的值和自增操作之间,可能系统的值已经又被其他线程改变了//如果又被改变了,则重新计算系统底层的值,并重新执行本地方法}while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));returnvar5;    }

/*** 本地的CAS方法核心*@paramvar1 需要操作的AtomicInteger 对象*@paramvar2 当前本地变量中的的值 *@paramvar4 当前系统从底层传来的值*@paramvar5 要更新后的值*@Return如果当前本地变量的值(var2)与底层的值(var4)不等,则返回false,否则更新为var5的值并返回True*/publicfinalnativebooleancompareAndSwapInt(Object var1,longvar2,intvar4,intvar5);

2.AtomicLong、LongAdder

我们看到AtomicInteger在执行CAS操作的时候,是用死循环的方式,如果竞争非常激烈,那么失败量就会很高,性能会受到影响

再看一下1.8以后的LongAdder

publicvoidadd(longx){        Cell[] as;longb, v;intm; Cell a;if((as = cells) !=null|| !casBase(b = base, b + x)) {booleanuncontended =true;if(as ==null|| (m = as.length -1) <0||                (a = as[getProbe() & m]) ==null||                !(uncontended = a.cas(v = a.value, v + x)))                longAccumulate(x,null, uncontended);        }    }

补充知识点,jvm对long,double这些64位的变量拆成两个32位的操作

LongAdder的设计思想:核心是将热点数据分离,将内部数据value分成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行技术,而最终计数结果为这个数组的求和累加,

其中热点数据value会被分离成多个热点单元的数据cell,每个cell独自维护内部的值,当前value的实际值由所有的cell累积合成,从而使热点进行了有效的分离,提高了并行度

LongAdder 在低并发的时候通过直接操作base,可以很好的保证和Atomic的性能基本一致,在高并发的场景,通过热点分区来提高并行度

缺点:在统计的时候如果有并发更新,可能会导致结果有些误差


实际运用中:优先使用LongAdder ,在线程竞争很低的情况下使用AtomicLong效率更高

全局序列号使用AtomicLong


3.AtomicReference、AtomicReferenceFieldUpdater

AtomicReference: 用法同AtomicInteger一样,但是可以放各种对象

@Slf4j@ThreadSafepublicclassAtomicExample4{publicstaticAtomicReference count =newAtomicReference<>(0);publicstaticvoidmain(String[] args)throwsInterruptedException{// 2count.compareAndSet(0,2);// nocount.compareAndSet(0,1);// nocount.compareAndSet(1,3);// 4count.compareAndSet(2,4);// nocount.compareAndSet(3,5);        log.info("count:{}",count.get());    }}

AtomicReferenceFieldUpdater

@Slf4j@ThreadSafepublicclassAtomicExample5{@Getterprivatevolatileintcount =100;/**

    * AtomicIntegerFieldUpdater 核心是原子性的去更新某一个类的实例的指定的某一个字段

    * 构造函数第一个参数为类定义,第二个参数为指定字段的属性名,必须是volatile修饰并且非static的字段

    */privatestaticAtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");publicstaticvoidmain(String[] args)throws InterruptedException{        AtomicExample5 example5 =newAtomicExample5();// 第一次 count=100 -> count->120 返回Trueif(updater.compareAndSet(example5,100,120)){log.info("update success 1:{}",example5.getCount());        }// count=120 -> 返回Falseif(updater.compareAndSet(example5,100,120)){log.info("update success 2:{}",example5.getCount());        }else{log.info("update field:{}",example5.getCount());        }    }}

5.AtomicStampReference:CAS的ABA问题

ABA问题:在CAS操作的时候,其他线程将变量的值A改成了B由改成了A,本线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作,这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的

解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化

/**    * Atomically sets the value of both the reference and stamp    * to the given update values if the    * current reference is {@code==} to the expected reference    * and the current stamp is equal to the expected stamp.    *    *@paramexpectedReference the expected value of the reference    *@paramnewReference the new value for the reference    *@paramexpectedStamp the expected value of the stamp(上面提到的版本号)    *@paramnewStamp the new value for the stamp    *@return{@codetrue} if successful    */publicbooleancompareAndSet(V  expectedReference,                                V  newReference,intexpectedStamp,intnewStamp){        Pair current = pair;returnexpectedReference == current.reference &&            expectedStamp == current.stamp &&            ((newReference == current.reference &&              newStamp == current.stamp) ||            casPair(current, Pair.of(newReference, newStamp)));    }

6.AtomicLongArray

可以指定更新一个数组指定索引位置的值

/**    * Atomically sets the element at position {@codei} to the given value    * and returns the old value.    *    *@parami the index    *@paramnewValue the new value    *@returnthe previous value    */publicfinallonggetAndSet(inti,longnewValue){returnunsafe.getAndSetLong(array, checkedByteOffset(i), newValue);    }....../**    * Atomically sets the element at position {@codei} to the given    * updated value if the current value {@code==} the expected value.    *    *@parami the index    *@paramexpect the expected value    *@paramupdate the new value    *@return{@codetrue} if successful. False return indicates that    * the actual value was not equal to the expected value.    */publicfinalbooleancompareAndSet(inti,longexpect,longupdate){returncompareAndSetRaw(checkedByteOffset(i), expect, update);    }

7.AtomicBoolean(平时用的比较多)

compareAndSet方法也值得注意,可以达到同一时间只有一个线程执行这段代码

/**    * Atomically sets the value to the given updated value    * if the current value {@code==} the expected value.    *    *@paramexpect the expected value    *@paramupdate the new value    *@return{@codetrue} if successful. False return indicates that    * the actual value was not equal to the expected value.    */publicfinalbooleancompareAndSet(booleanexpect,booleanupdate){inte = expect ?1:0;intu = update ?1:0;returnunsafe.compareAndSwapInt(this, valueOffset, e, u);    }

原子性-锁

synchronized:依赖JVM (主要依赖JVM实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程进行操作的)

Lock:依赖特殊的CPU指令,代码实现,ReentrantLock

修饰的内容分类

修饰内容

/**

* @author gaowenfeng

* @date

*/@Slf4jpublicclassSyncronizedExample1{/**

    * 修饰一个代码块,作用范围为大括号括起来的

    */publicvoidtest1(){        synchronized (this){for(inti =0; i <10; i++) {log.info("test1-{}",i);            }        }    }/**

    * 修改方法,作用范围是整个方法,作用对象为调用这个方法的对象

    * 若子类继承父类调用父类的synchronized方法,是带不上synchronized关键字的

    * 原因:synchronized 不属于方法声明的一部分

    * 如果子类也想使用同步需要在方法上声明

    */publicsynchronizedvoidtest2(){for(inti =0; i <10; i++) {log.info("test2-{}",i);        }    }publicstaticvoidmain(String[] args){        SyncronizedExample1 example1 =newSyncronizedExample1();        SyncronizedExample1 example2 =newSyncronizedExample1();// 使用线程池模拟一个对象的两个进程同时调用一段sync代码的执行过程ExecutorService executorService = Executors.newCachedThreadPool();// 线程pool-1-thread-1,pool-1-thread-2 交叉输出executorService.execute(()-> example1.test1());        executorService.execute(()-> example2.test1());// 线程pool-1-thread-1 先从0-9输出,然后pool-1-thread-2 从0到9顺序输出// executorService.execute(()-> example1.test1());// executorService.execute(()-> example1.test1());}}

@Slf4jpublicclassSyncronizedExample2{/**

    * 修饰类,括号包起来的代码

    * 作用对象为这个类的所有对象

    */publicstaticvoidtest1(){        synchronized (SyncronizedExample2.class){for(inti =0; i <10; i++) {log.info("test1-{}",i);            }        }    }/**

    * 修饰一个静态方法,作用对象为这个类的所有对象

    */publicstaticsynchronizedvoidtest2(){for(inti =0; i <10; i++) {log.info("test2-{}",i);        }    }publicstaticvoidmain(String[] args){        SyncronizedExample2 example1 =newSyncronizedExample2();        SyncronizedExample2 example2 =newSyncronizedExample2();// 使用线程池模拟一个对象的两个进程同时调用一段sync代码的执行过程ExecutorService executorService = Executors.newCachedThreadPool();// 线程pool-1-thread-1 先从0-9输出,然后pool-1-thread-2 从0到9顺序输出executorService.execute(()-> example1.test1());        executorService.execute(()-> example1.test1());// 线程pool-1-thread-1 先从0-9输出,然后pool-1-thread-2 从0到9顺序输出//        executorService.execute(()-> example1.test2());//        executorService.execute(()-> example2.test2());}}

原子性对比

可见性

导致共享变量在线程中不可见的原因

线程交叉执行

重排序结合线程交叉执行

共享变量更新后的值没有在工作内存与主内存间及时更新

java提供了synchronized和volatile 两种方法来确保可见性

JMM(java内存模型)关于synchronized的两条规定

线程解锁前,必须把共享变量的最新值刷新到主内存

线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁和解锁是同一把锁)

可见性-volatile:通过加入内存屏障和禁止重排序优化来实现

对volatile 变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存

对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

volatile写示意图

volatile读示意图

/** * 并发测试 *@authorgaowenfeng */@Slf4j@NotThreadSafepublicclassCountExample4extendsAbstractExample{/** 请求总数 */publicstaticintclientTotal =5000;/** 同时并发执行的线程数 */publicstaticintthreadTotal =50;publicvolatilestaticintcount =0;publicstaticvoidmain(String[] args)throwsInterruptedException{newCountExample4().test();    }/**

    * 本质上应该是这个方法线程不安全

    *

    * volatile只能保证 1,2,3的顺序不会被重排序

    * 但是不保证1,2,3的原子执行,也就是说还是有可能有两个线程交叉执行1,导致结果不一致

    */@Overrideprotectedvoidadd(){// 1.取内存中的count值// 2.count值加1// 3.重新写会主存count++;    }@OverrideprotectedvoidcountLog(){        log.info("count:{}",count);    }}

volatile使用条件

1.对变量写操作不依赖于当前值

2.该变量没有包含在具有其他变量的不必要的式子中

综上,volatile特别适合用来做线程标记量,如下图

volatile使用场景

有序性

有序性

Happens-before原则,先天有序性,即不需要任何额外的代码控制即可保证有序性,java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性

1-2

第一条规则要注意理解,这里只是程序的运行结果看起来像是顺序执行,虽然结果是一样的,jvm会对没有变量值依赖的操作进行重排序,这个规则只能保证单线程下执行的有序性,不能保证多线程下的有序性

3-4

5-6

7-8

总结

need-to-insert-img

作者:Meet相识_bfa5

链接:https://www.jianshu.com/p/23a8244cd24e

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容