Java中如何保证线程安全性
一、线程安全在三个方面体现
1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
接下来,依次分析。
二、原子性---atomic
JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。
它们是通过CAS完成原子性。
我们一次来看AtomicInteger,AtomicStampedReference,AtomicLongArray,AtomicBoolean。
(1)AtomicInteger
先来看一个AtomicInteger例子:
public class AtomicIntegerExample1 {// 请求总数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();}
}
我们可以执行看到最后结果是5000是线程安全的。
那么看AtomicInteger的incrementAndGet()方法:
再看getAndAddInt()方法:
这里面调用了compareAndSwapInt()方法:
它是native修饰的,代表是java底层的方法,不是通过java实现的 。
再重新看getAndAddInt(),传来第一个值是当前的一个对象 ,比如是count.incrementAndGet(),那么在getAndAddInt()中,var1就是count,而var2第二个值是当前的值,比如想执行的是2+1=3操作,那么第二个参数是2,第三个参数是1 。
变量5(var5)是我们调用底层的方法而得到的底层当前的值,如果没有别的线程过来处理我们count变量的时候,那么它正常返回值是2。
因此传到compareAndSwapInt方法里的参数是(count对象,当前值2,当前从底层传过来的2,从底层取出来的值加上改变量var4)。
compareAndSwapInt()希望达到的目标是对于var1对象,如果当前的值var2和底层的值var5相等,那么把它更新成后面的值(var5+var4).
compareAndSwapInt核心就是CAS核心。
关于count值为什么和底层值不一样:count里面的值相当于存在于工作内存的值,底层就是主内存。
(2)AtomicStampedReference
接下来我们看一下AtomicStampedReference。
关于CAS有一个ABA问题:开始是A,后来改为B,现在又改为A。解决办法就是:每次变量改变的时候,把变量的版本号加1。
这就用到了AtomicStampedReference。
我们来看AtomicStampedReference里的compareAndSet()实现:
而在AtomicInteger里compareAndSet()实现:
可以看到AtomicStampedReference里的compareAndSet()中多了 一个stamp比较(也就是版本),这个值是由每次更新时来维护的。
(3)AtomicLongArray
这种维护数组的atomic类,我们可以选择性地更新其中某一个索引对应的值,也是进行原子性操作。这种对数组的操作的各种方法,会多处一个索引。
比如,我们看一下compareAndSet():
(4)AtomicBoolean
看一段代码:
public class AtomicBooleanExample {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");}}
}
执行之后发现,log.info("execute");只执行了一次,且isHappend值为true。
原因就是当它第一次compareAndSet()之后,isHappend变为true,没有别的线程干扰。
通过使用AtomicBoolean,我们可以使某段代码只执行一次。
三、原子性---synchronized
synchronized是一种同步锁,通过锁实现原子操作。
JDK提供锁分两种:一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。
synchronized修饰的对象有四种:
(1)修饰代码块,作用于调用的对象;
(2)修饰方法,作用于调用的对象;
(3)修饰静态方法,作用于所有对象;
(4)修饰类,作用于所有对象。
修饰代码块和方法:
@Slf4j
public class SynchronizedExample1 {// 修饰一个代码块public void test1(int j) {synchronized (this) {for (int i = 0; i < 10; i++) {log.info("test1 {} - {}", j, i);}}}// 修饰一个方法public synchronized void test2(int j) {for (int i = 0; i < 10; i++) {log.info("test2 {} - {}", j, i);}}public static void main(String[] args) {SynchronizedExample1 example1 = new SynchronizedExample1();SynchronizedExample1 example2 = new SynchronizedExample1();ExecutorService executorService = Executors.newCachedThreadPool();//一executorService.execute(() -> {example1.test1(1);});executorService.execute(() -> {example1.test1(2);});//二executorService.execute(() -> {example2.test2(1);});executorService.execute(() -> {example2.test2(2);});//三executorService.execute(() -> {example1.test1(1);});executorService.execute(() -> {example2.test1(2);});}
}
执行后可以看到对于情况一,test1内部方法块作用于example1,先执行完一次0-9输出,再执行下一次0-9输出;情况二,同情况一类似,作用于example2;情况三,可以看到交叉执行,test1分别独立作用于example1和example2,互不影响。
修饰静态方法和类:
@Slf4j
public class SynchronizedExample2 {// 修饰一个类public static void test1(int j) {synchronized (SynchronizedExample2.class) {for (int i = 0; i < 10; i++) {log.info("test1 {} - {}", j, i);}}}// 修饰一个静态方法public static synchronized void test2(int j) {for (int i = 0; i < 10; i++) {log.info("test2 {} - {}", j, i);}}public static void main(String[] args) {SynchronizedExample2 example1 = new SynchronizedExample2();SynchronizedExample2 example2 = new SynchronizedExample2();ExecutorService executorService = Executors.newCachedThreadPool();executorService.execute(() -> {example1.test1(1);});executorService.execute(() -> {example2.test1(2);});}
}
test1和test2会锁定调用它们的对象所属的类,同一个时间只有一个对象在执行。
四、可见性---volatile
对于可见性,JVM提供了synchronized和volatile。这里我们看volatile。
(1)volatile的可见性是通过内存屏障和禁止重排序实现的
volatile会在写操作时,会在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存:
volatile在进行读操作时,会在读操作前加一条load指令,从内存中读取共享变量:
(2)但是volatile不是原子性的,进行++操作不是安全的
@Slf4j
public class VolatileExample {// 请求总数public static int clientTotal = 5000;// 同时并发执行的线程数public static int threadTotal = 200;public static volatile int count = 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);}private static void add() {count++;}
}
执行后发现线程不安全,原因是 执行conut++ 时分成了三步,第一步是取出当前内存 count 值,这时 count 值时最新的,接下来执行了两步操作,分别是 +1 和重新写回主存。假设有两个线程同时在执行 count++ ,两个内存都执行了第一步,比如当前 count 值为 5 ,它们都读到了,然后两个线程分别执行了 +1 ,并写回主存,这样就丢掉了一次加一的操作。
(3)volatile适用的场景
既然volatile不适用于计数,那么volatile适用于哪些场景呢:
1. 对变量的写操作不依赖于当前值
2. 该变量没有包含在具有其他变量不变的式子中
因此,volatile适用于状态标记量:
线程1负责初始化,线程2不断查询inited值,当线程1初始化完成后,线程2就可以检测到inited为true了。
五、有序性
有序性是指,在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
可以通过volatile、synchronized、lock保证有序性。
另外,JMM具有先天的有序性,即不需要通过任何手段就可以得到保证的有序性。这称为happens-before原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性。虚拟机可以随意地对它们进行重排序。
happens-before原则:
1.程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行。
2.锁定规则:一个unlock操作happen—before后面对同一个锁的lock操作。
3.volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4.线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5.线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6.线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7.对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8.传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
Java中如何保证线程安全性相关推荐
- Java中枚举的线程安全性及序列化问题
转载自 Java中枚举的线程安全性及序列化问题 Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序 ...
- java中什么是线程安全_Java 多线程:什么是线程安全性
线程安全性 什么是线程安全性 <Java Concurrency In Practice>一书的作者 Brian Goetz 是这样描述"线程安全"的:"当多 ...
- java中的后台线程、前台线程、守护线程区别
java中的后台线程.前台线程.守护线程区别 区别和联系 区别 联系 区别和联系 区别 后台线程和守护线程是一样的. 后台线程不会阻止进程的终止,而前台线程会, 可以在任何时候将前台线程修改为后台线程 ...
- Java中如何实现线程的超时中断
转载自 Java中如何实现线程的超时中断 背景 之前在实现熔断降级组件的时候,需要实现接口请求的超时中断.意思是,业务在使用熔断降级功能时,在平台上设置了一个超时时间,如果请求进入熔断器开始计时,接 ...
- 什么是Java中的守护程序线程?
谁能告诉我Java中有哪些守护程序线程? #1楼 守护程序线程就像其他与守护程序线程在同一进程中运行的线程或对象的服务提供者一样. 守护程序线程用于后台支持任务,仅在执行正常线程时才需要. 如果正常线 ...
- Java中的守护程序线程
Daemon thread in java can be useful to run some tasks in background. When we create a thread in java ...
- 多线程中的互斥控制程序代码_Java中的并发——线程安全性
一.什么是线程安全性? 当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,则这个类是线程安全的 ...
- Java多线程编程(3)--线程安全性
一.线程安全性 一般而言,如果一个类在单线程环境下能够运作正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运作正常,那么我们就称其是线程安全的.反之,如果一个类在单线程环境下运作 ...
- java中我爱你_Java线程学习(转)
编写具有多线程能力的程序经常会用到的方法有: run(),start(),wait(),notify(),notifyAll(),sleep(),yield(),join() 还有一个重要的关键字:s ...
最新文章
- ThinkPHP 3.1.2 视图 1
- R语言置换检验(permutation tests、响应变量是否独立于组、两个数值变量是独立的吗、两个分类变量是独立的吗)、置换检验的基本步骤、R语言自助法Bootstrapping计算置信区间
- Kaggle八项大奖斩获其6:用于筛选和分析文献的paperai
- 深度学习项目中在yaml文件中定义配置,以及使用的python的PyYAML库包读取解析yaml配置文件
- 关于iptables
- ABAP-SQL基础知识
- linux网络编程-----TCP连接及相关问题
- OPA 6 - module(Create Button Test);
- 父盒子高度为子盒子总高度自动撑满 height: fit-content; //设置内容高度
- 前端学习(2860):简单秒杀系统学习之前端优化
- 智能机器人建房子后房价走势_人工智能未来10年将颠覆房地产行业,你还敢买房吗?...
- wsdl接口_DEBUG系列四:第三方接口debug
- Linux系统管理(7)——Linux单用户模式详解 及应用场景
- 双刃剃须刀行业调研报告 - 市场现状分析与发展前景预测
- 六款值得推荐的数据挖掘得力助手
- php格式文件用什么看,.zbf是什么格式文件,用什么看的
- 免费的安卓录屏、录音软件(无需root)
- SpringBoot项目配置明文密码泄露问题处理
- 灵信LED屏 二次开发C#
- 微信小程序-“授权失败”场景的优雅处理