java并发编程笔记(三)——线程安全性

线程安全性:

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

线程安全体现在三个方面:

  • 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
  • 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

原子性:Atomic包

使用AtomicInteger保证该变量操作的原子性

public class CountExample2 {

    // 请求总数
public static int clientTotal = 5000; // 同时并发执行的线程数
public static int threadTotal = 200; public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
} private static void add() {
count.incrementAndGet(); //相当于++x;
// count.getAndIncrement(); //相当于x++
}
}

原理:AtomicInteger的incrementAndGet()方法里边用到了一个unsafe的类

public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

继续深入点进去看getAndAddInt的实现:

//
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;
}

这里最重要的一个方法是:compareAndSwapInt(),这是java底层的一个方法,它不是通过java实现的:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

参数解释:

Object var1:所操作的对象,比如本次案例中,这个Obect是AtomicInteger count;

long var2:这个对象当前的值;

int var4:当前对象要增加的值,比如本次案例中做+1操作,那么var4就是1;

int var5:调用底层得到的一个值,如果没有其他线程过来操作,这个值应该是等于var2

getAndAddInt()方法中compareAndSwapInt()方法执行解释:如果对于var1这个对象,如果var2与从底层获取的值var5是相同的,那么就执行var5 + var4;

进一步解释:count的当前值,是当前线程中的值,属于线程中的工作内存中的值,而底层获取的值是主存中值,只有当工作内存中的值和主存中的值是一致的时候,才可以修改。

AtomicLong、LongAdder

在上边的例子中,把AtomicInteger 替换成AtomicLong,整个方法依然是线程安全的。

第二种方式是使用LongAdder:

public class AtomicExample3 {

    // 请求总数
public static int clientTotal = 5000; // 同时并发执行的线程数
public static int threadTotal = 200; public static LongAdder count = new LongAdder(); public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
} private static void add() {
count.increment();
}
}

AtomicLong和LongAdder的对比:

AtomicLong:该类底层实现是在一个死循环内,不断的尝试修改目标值,直到修改成功,在竞争不激烈的情况下,修改成功概率很大,在竞争激烈情况下修改失败的概率较大,这种情况下会有损性能。

LongAdder:由于Long、Double类型的值JVM允许将他们64位的读写操作分拆成32位的读写操作,根据此原理 LongAdder将操作的数值分拆成数组,然后最终得到的是数组的加和,通过分拆均衡操作压力,因此其性能相对较好

使用场景的选择:在高并发计数的情景下优先使用LongAdder,其他情况使用AtomicLong

**AtomicBoolean **

底层实现的方法是:

public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

这个方法是指某个代码块逻辑值执行一次。

使用案例(该案例演示了某一段代码在多线程情况下,只执行了一次):

public class AtomicExample6 {

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

    // 请求总数
public static int clientTotal = 5000; // 同时并发执行的线程数
public static int threadTotal = 200; public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("isHappened:{}", isHappened.get());
} private static void test() {
if (isHappened.compareAndSet(false, true)) {
log.info("execute");
}
}
}

AtomicReference、AtomicReferenceFieldUpdater

AtomicReference使用示例:

public class AtomicExample4 {

    private static AtomicReference<Integer> count = new AtomicReference<>(0);

    public static void main(String[] args) {
count.compareAndSet(0, 2); // 2
count.compareAndSet(0, 1); // no
count.compareAndSet(1, 3); // no
count.compareAndSet(2, 4); // 4
count.compareAndSet(3, 5); // no
log.info("count:{}", count.get()); //4
}
}

AtomicReferenceFieldUpdater使用示例:

public class AtomicExample5 {

    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count"); @Getter
public volatile int count = 100; public static void main(String[] args) { AtomicExample5 example5 = new AtomicExample5(); if (updater.compareAndSet(example5, 100, 120)) {
log.info("update success 1, {}", example5.getCount());
} if (updater.compareAndSet(example5, 100, 120)) {
log.info("update success 2, {}", example5.getCount());
} else {
log.info("update failed, {}", example5.getCount());
}
}
}

AtomicStampedReference:解决CAS的ABA问题

ABA问题:在CAS操作的时候,其他线程将变量的值A改成了B,但是又改回了A,本线程使用期望值A与当前变量进行比较的时候,发现变量A没有变,于是CAS将A值进行了交换操作。

解决思路:每次变量更新的时候,把版本号+1

核心类:

AtomicStampedReference

其中的核心方法:compareAndSet()

public boolean compareAndSet(V   expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}

AtomicLongArray

这个类维护的是一个数组

这个类与AtomicLong比较,方法 里多了一个索引值让我们指定。

原子性——锁

  • synchronized:依赖JVM去实现锁
  • Lock:依赖特殊的cpu指令,代码实现,ReenteantLock

synchronized:

  • 修饰代码块:大括号括起来的代码,作用于调用的对象

  • 修饰方法:整个方法,作用于调用的对象

  • 修饰静态方法:整个静态方法,作用于所有对象

  • 修饰类,括号括起来的部分,作用于所有对象

原子性——对比

synchronized:不可中断锁,适合竞争不激烈,可读性好

Lock:可中断锁,多样化同步,竞争激烈时能维持常态

Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值

可见性

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

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主内存间及时更新

可见性——synchronized

JMM关于synchronized的两条规定:

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

可见性——volatile

通过假如内存屏障和禁止重排序优化来实现

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

volatile关键字不具有原子性

适合的场景:

  • 对变量的写操作不依赖与当前值;
  • 该变量没有包含在具有其他变量的不变式中。

因此volatile特别适合状态标记量

有序性

java内存模型中,允许编辑器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

通常情况下可以通过以下三个关键字来保证有序性:

  • volatile
  • synchronized
  • Lock

happens-before原则

如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证他们的有序性,虚拟机就可以对他们随意的进行重排序。

也就是除了下面这些规则规定的场景,其他场景,虚拟机可以对其进行重排序。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作

  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

  • 线程中断原则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始

java并发编程笔记(三)——线程安全性的更多相关文章

  1. java并发编程实战之线程安全性(一)

    1.1什么是线程安全性 要对线程安全性给出一个确切的定义是非常复杂的.最核心的概念就是正确性.正确性:某个类的行为与其规范完全一致.在良好的规范中通常会定义各种不变性条件来约束对象的状态,以及定义各种 ...

  2. Java并发编程实战 之 线程安全性

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

  3. java并发编程笔记(七)——线程池

    java并发编程笔记(七)--线程池 new Thread弊端 每次new Thread新建对象,性能差 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM 缺 ...

  4. java并发编程笔记(五)——线程安全策略

    java并发编程笔记(五)--线程安全策略 不可变得对象 不可变对象需要满足的条件 对象创建以后其状态就不能修改 对象所有的域都是final类型 对象是正确创建的(在对象创建期间,this引用没有逸出 ...

  5. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  6. Java并发编程系列-(2) 线程的并发工具类

    2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...

  7. java并发编程笔记(十一)——高并发处理思路和手段

    java并发编程笔记(十一)--高并发处理思路和手段 扩容 垂直扩容(纵向扩展):提高系统部件能力 水平扩容(横向扩容):增加更多系统成员来实现 缓存 缓存特征 命中率:命中数/(命中数+没有命中数) ...

  8. java并发编程笔记(十)——HashMap与ConcurrentHashMap

    java并发编程笔记(十)--HashMap与ConcurrentHashMap HashMap参数 有两个参数影响他的性能 初始容量(默认为16) 加载因子(默认是0.75) HashMap寻址方式 ...

  9. java并发编程笔记(九)——多线程并发最佳实践

    java并发编程笔记(九)--多线程并发最佳实践 使用本地变量 使用不可变类 最小化锁的作用域范围 使用线程池Executor,而不是直接new Thread执行 宁可使用同步也不要使用线程的wait ...

随机推荐

  1. 【前端技术】一篇文章搞掂:微信小程序

    实战: 1.[openId]获取openId 有如下几种方法: 通过wx.login()获取临时登录凭证 code,然后通过code2session获取openId wx.login():https: ...

  2. python类与对象练习题扑克牌

    #定义一个扑克类,属性是颜色,数字.#定义一个手类,属性是扑克牌得颜色数字#定义一个人类,属性是左手,右手.类里定义一些方法,比如交换,展示 class Poker : def __init__(se ...

  3. nRF51822 之 Interrupt

    nRF51822的中断使用在官方的例程中好像没有!

  4. Nuget-Doc:Nuget 简介

    ylbtech-Nuget-Doc:Nuget 简介 1.返回顶部 1. NuGet 简介 2019/05/24 适用于任何现代开发平台的基本工具可充当一种机制,通过这种机制,开发人员可以创建.共享和 ...

  5. 116、TensorFlow变量的版本

    import tensorflow as tf v = tf.get_variable("v", shape=(), initializer=tf.zeros_initialize ...

  6. 同时连接gitlab和github

    ---恢复内容开始--- 原文地址:https://juejin.im/post/5ac0cf356fb9a028df22c246 1. 分别生成gitlab和github的ssh key 生成第一个 ...

  7. vue搭建项目之设置axios

    首先要下载axios: npm install axios -S 要注意的是,axios不支持Vue.use();这种方式,可以改写原型链. 第二步就是新建axios存放位置: 在项目中src中单独建 ...

  8. Pikachu漏洞练习平台实验——CSRF(三)

    概述 CSRF 是 Cross Site Request Forgery 的 简称,中文名为跨域请求伪造 在CSRF的攻击场景中,攻击者会伪造一个请求(一般是一个链接) 然后欺骗目标用户进行点击,用户 ...

  9. Can't connect to local MySQL server through socket '/opt/lampp/var/mysql/mysql.sock' (2)

    ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/opt/lampp/var/mysql/mysql.s ...

  10. spring注解开发:容器中注册组件方式

    1.包扫描+组件标注注解 使用到的注解如下,主要针对自己写的类 @Controller @Service @Repository @Component @ComponentScan 参考 spring ...