线程的安全性 - 并发基础篇

个人博客:javalover.cc

前言

官人们好啊,我是汤圆,今天给大家带来的是《线程的安全性 - 并发基础篇》,希望有所帮助,谢谢

文章纯属原创,个人总结难免有差错,如果有,麻烦在评论区回复或后台私信,谢啦

简介

当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就说这个类是线程安全的

目录

这次分三步走:关于相关知识点,放在文末的脑图里了,大家想看结论的,可直接下拉观看哦

创建一个线程安全的类

创建一个线程不安全的类:有一个状态变量

创建一个线程不安全的类:有多个状态变量

正文

线程的安全性主要是针对对象的状态(实例属性或静态属性)而言的,如果在多线程中,访问到的对象状态不一致(比如常见的自增属性),那么就是线程不安全的

下面我们一步步来

先来个无状态类

第一步:无状态类

这里我们写一个简单的线程安全类,简单到什么地步呢?如下所示

public class SafeDemo { public int sum(int n, int m){ return n + m; } }

就是这么简单,我们说这个类是线程安全的

为啥安全呢?

因为这个类没有状态,即无状态类;

只有局部变量n,m,而这些局部变量是存在于栈中的,栈是每个线程独有的,不跟其他线程共享,堆才共享

所以每个线程操作sum时,对应的n,m只有自己可见,当然就安全了

好了,通过上面的例子,我们知道了什么是线程安全类,那本节的内容就到此结束了,再见

疑问

上面的例子,我们举了一个无状态类,接下来我们添加一个状态试试

第二步:加一个状态变量

加一个状态变量(静态属性),代码如下

public class UnSafeDemo { static int a = 0; public static void main(String[] args) throws InterruptedException { // 线程1 new Thread(()-> { for(int j=0;j<100000;j++){ a++; } }).start(); // 线程2 new Thread(()-> { for(int j=0;j<100000;j++){ a++; } }).start(); Thread.sleep(3000); // 这里不是每次运行都会输出200,000 System.out.println(a); } }

上面我们创建了两个线程,每个线程都执行10万次的自增操作

但是因为自增不是原子操作,实际分三步:读-改-写

此时如果两个线程同时读到相同的值,则累加次数就会少一次

这种在并发编程中,由于不恰当的执行时序而出现不正确的结果的情况,叫做竞态条件

如下图所示:

期望的是正常执行,每个线程交替执行

自增-正常

结果却有可能是不正常的,如下

自增-不正常

这时我们就可以说,上面加的这个状态是不安全的,结果就是整个类也是不安全的

不安全的状态有二:

可变状态(变量):非final修饰的变量

共享状态(变量):非局部变量

像上面这个例子,状态就同时属于可变状态和共享状态

那要怎么确保安全:

同步:synchronized、volatile、显式锁、原子变量(比如AtomicInteger)

不可变变量:final(都不能改了,当然安全了)

不共享变量:不在多线程中共享变量(即局部变量)

PS:代码的封装性越好,访问可变变量的代码块越少,越容易确保线程安全

这里的自增我们就可以用同步中的原子变量来解决

关于原子变量的细节,后面章节再介绍,这里只需要知道,原子变量内部的操作是原子操作就可以了

修改后的代码如下:

public class SafeDemo { static final AtomicInteger a = new AtomicInteger(0); // static int a = 0; public static void main(String[] args) throws InterruptedException { // 线程1 new Thread(()-> { for(int j=0;j<100000;j++){ // 这里的自增是原子操作 a.incrementAndGet(); } }).start(); // 线程2 new Thread(()-> { for(int j=0;j<100000;j++){ // 这里的自增是原子操作 a.incrementAndGet(); } }).start(); Thread.sleep(3000); System.out.println(a.get()); } }

可以看到,加了AtomicInteger.incrementAndGet()方法,这个方法是原子操作

这时,不管怎么运行,都是输出200,000

第三步:加多个状态变量

上面我们加了一个状态变量,可以用原子变量来保证线程安全

那如果是多个状态变量呢?此时就算用了原子变量也不行了

因为原子变量只是保证它内部是原子操作,但是当多个原子变量放到一起组合操作时,他们之间又存在竞态条件了,就又不是原子操作了

竞态条件:并发编程中,由于不恰当的执行时序而出现不正确的结果的情况,就是竞态条件(重复陈述ing,加深记忆)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsfszd.html