JAVA多线程线程安全性基础
线程安全性
一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能
什么是线程安全的代码
核心:对 共享的 和 可变的 状态的访问进行管理。防止对数据发生不受控的并发访问。
何为对象的状态?
状态是指存储在对象的状态变量(例如实例或静态域)中的数据。还可能包括 其他依赖对象 的域。
eg:某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中。
总而言之,在对象的状态中包含了任何可能影响其外部可见行为的数据。
何为共享的?
共享的 是指变量可同时被多个线程访问
何为可变的?
可变的 是指变量的值在其生命周期内可以发生变化。试想,如果一个共享变量的值在其生命周期内不会发生变化,那么在多个
线程访问它的时候,就不会出现数据不一致的现象,自然就不存在线程安全性问题了。
什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,达到预期的效果,那么就称这个类是线程安全的。
如下启动10个线程,每个线程对inc执行1000次递增,并添加一个计时线程,预期效果应为10000,而实际输出值为6880,是一个小于10000的值,并未达到预期效果,因此INS类不是线程安全的,整个程序也不是线程安全的。原因是递增操作不是原子操作,并且没有适当的同步机制
package hgh0808; public class Test { public static void main(String[] args){ for(int i = 0;i < 10;i++){ Thread th = new Thread(new CThread()); th.start(); } TimeThread tt = new TimeThread(); tt.start(); try{ Thread.sleep(21000); }catch(Exception e){ e.printStackTrace(); } System.out.println(INS.inc); } } --------------------------------------------------------------------- package hgh0808; import java.util.concurrent.atomic.*; public class TimeThread extends Thread{ @Override public void run(){ int count = 1; for(int i = 0;i < 20;i++){ try{ Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } System.out.println(count++); } } } --------------------------------------------------------------------- package hgh0808; public class CThread implements Runnable{ @Override public void run(){ for(int j = 0;j < 1000;j++){ INS.increase(); } } } --------------------------------------------------------------------- package hgh0808; public class INS{ public static volatile int inc = 0; public static void increase(){ inc++; } } =====================================================================
执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6880
通过synchronized加锁机制,对INS类实现同步,如下得到了正确的运行结果,很容易可以看出,主调代码中并没有任何额外的同步或协同,此时的INS类是线程安全的,整个程序也是线程安全的
package hgh0808; public class INS{ public static volatile int inc = 0; public static void increase(){ synchronized (INS.class){ inc++; } } }
执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000
如何编写线程安全的代码
------------------------------------------------------------------------------------------------
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,像上文中进行同步之前的代码
有三种方式可以修复这个问题:
*不在线程之间共享该状态变量
*将状态变量修改为不可变的变量
*在访问状态变量时使用同步
前两种方法是针对 共享 和 不变 这两个属性(见上文)解决问题,在有些情境下会违背程序设计的初衷(比如上文中INS类中的inc变量不可能不变,且在多核处理器的环境下为了提高程序性能,就需要多个线程同时处理,这样变量就必然要被多个线程共享)。
基于此,我们针对第三种方式------ 在访问状态变量时使用同步 展开讨论
在讨论第三种方式之前,我们先介绍几个简单的概念
原子性 :一个操作序列的所有操作要么不间断地全部被执行,要么一个也没有执行
竞态条件 :当某个计算的正确性取决于多个线程的的交替执行时序时,就会发生竞态条件。通俗的说,就是某个程序结果的正确性取决于运气时,就会发生竞态条件。(竞态条件并不总是会产生错误,还需要某种不恰当的执行时序)
常见的竞态条件类型:
*检查–执行(例如延迟初始化)
*读取–修改–写入(例如自增++操作)
针对以上两种常见的竞态条件类型,我们分别给出例子
延迟初始化(检查--执行) -------------------------------------------------------------------- package hgh0808; import java.util.ArrayList; public class Test1 { public ArrayList<Ball> list; public ArrayList<Ball> getInstance(){ if(list == null){ list = new ArrayList<Ball>(); } return list; } } class Ball{ }
大概逻辑是先判断list是否为空,若为空,创建一个新的ArrayList对象,若不为空,则直接使用已存在的ArrayList对象,这样可以保证在整个项目中list始终指向同一个对象。这在单线程环境中是完全没有问题的,但是如果在多线程环境中,list还未实例化时,A线程和B线程同时执行if语句,A和B线程都会认为list为null,A和B线程都会执行实例化语句,造成混乱。
自增++操作(读取--修改--写入) ------------------------------------------------------------------------ 参考上文中为改进之前的代码(对INS类中inc的自增)
以上两个例子告诉我们,必须添加适当的同步策略,保证复合操作的原子性,防止竞态条件的出现
策略一:使用原子变量类,在java.util.concurrent.atomic包中包含了一些原子变量类
package hgh0808; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; public class INS{ public static AtomicInteger inc = new AtomicInteger(0); public static void increase(){ inc.incrementAndGet(); } }
执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000
值得注意的是,只有一个状态变量时,可以通过原子变量类实现线程安全。但是如果有多个状态变量呢?
设想一个情景
多个线程不断产生1到10000的随机数并且发送到一个计算线程,计算线程每获取一个数字n,就计算sinx在[0,n]上的积分并打印到控制台上,为了提高程序性能,设计一个缓存机制,保存上次的数字n和积分结果(两个状态变量)。如果本次的数字和上次的数字相等,直接打印积分结果,避免重复计算。
看代码:
package hgh0808; import java.util.concurrent.atomic.AtomicReference; public class Calculate extends Thread{ private final AtomicReference<Double> lastNumber = new AtomicReference<Double>(); //缓存机制,原子变量类 private final AtomicReference<Double> lastRes = new AtomicReference<Double>(); //缓存机制,原子变量类 private static final double N = 100000; //将区间[0,e]分成100000份,方便定积分运算 public void service() throws Exception{ getData(); Thread.sleep(10000); //等待MyQueue队列中有一定数量的元素后,再开始从其中取元素 while(true){ Double e; if(!MyQueue.myIsEmpty()){ e = MyQueue.myRemove(); }else{ return; } if(e.equals(lastNumber.get())){ System.out.println(lastNumber.get()+" "+lastRes.get()); }else{ Double temp = integral(e); lastNumber.set(e); lastRes.set(temp); System.out.println(e+" "+temp); } Thread.sleep(2000); } } public void getData(){ //创建并启动四个获取随机数的线程,这四个线程交替向MyQueue队列中添加元素 Thread1 th1 = new Thread1(); Thread2 th2 = new Thread2(); Thread3 th3 = new Thread3(); Thread4 th4 = new Thread4(); th1.start(); th2.start(); th3.start(); th4.start(); } public Double integral(double e){ //计算定积分 double step = (e-0)/N; double left = 0,right = step; double sum = 0; while(right <= e){ double mid = left+(right-left)/2; sum+=Math.sin(mid); left+=step; right+=step; } sum*=step; return sum; } } --------------------------------------------------------------------- package hgh0808; import java.util.LinkedList; public class MyQueue { //由于LinkedList是线程不安全的,因此需要将其改写为线程安全类 private static LinkedList<Double> queue = new LinkedList<>(); synchronized public static void myAdd(Double e){ queue.addLast(e); } synchronized public static void myClear(){ queue.clear(); } synchronized public static int mySize(){ return queue.size(); } synchronized public static boolean myIsEmpty(){ return queue.isEmpty(); } synchronized public static double myRemove(){ return queue.removeFirst(); } } ----------------------------------------------------------------------- package hgh0808; import java.util.Random; public class Thread1 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ //由于从队列中取元素的速度低于四个线程向队列中加元素的速度,因此队列的长度是趋于扩张的,当达到一定程度时,清空队列 MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } } ------------------------------------------------------------------------ package hgh0808; import java.util.Random; public class Thread2 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } } ----------------------------------------------------------------------- package hgh0808; import java.util.Random; public class Thread3 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } } ------------------------------------------------------------------------ package hgh0808; import java.util.Random; public class Thread4 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } }
只看Calculate线程,不看其他线程和MyQueue中的锁机制,本问题的焦点在于Calculate线程中对多个状态变量的同步策略
存在问题:
尽管对lastNumber和lastRes的set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastRes;如果只修改了其中一个变量,那么在这两次修改操作之间,其它线程将发现不变性条件被破坏了。换句话说,就是没有足够的原子性
**当在不变性条件中涉及多个变量时,各个变量间并不是彼此独立的,而是某个变量的值会对其它变量的值产生约束。因此当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
改进 ================>加锁机制 内置锁 synchronized
之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象
synchronized修饰方法就是横跨整个方法体的同步代码块
非静态方法的锁-----方法调用所在的对象
静态方法的锁-----方法所在类的class对象
public class Calculate extends Thread{ private final AtomicReference<Double> lastNumber = new AtomicReference<Double>(); //缓存机制,原子变量类 private final AtomicReference<Double> lastRes = new AtomicReference<Double>(); //缓存机制,原子变量类 private static final double N = 100000; //将区间[0,e]分成100000份,方便定积分运算 public void service() throws Exception{ getData(); Thread.sleep(10000); //等待MyQueue队列中有一定数量的元素后,再开始从其中取元素 while(true){ Double e; synchronized (this){ //检查--执行 使用synchronized同步,防止出现竞态条件 if(!MyQueue.myIsEmpty()){ e = MyQueue.myRemove(); }else{ return; } } if(e.equals(lastNumber.get())){ System.out.println(lastNumber.get()+" "+lastRes.get()); }else{ Double temp = integral(e); synchronized (this) { //两个状态变量在同一个原子操作中更新 lastNumber.set(e); lastRes.set(temp); } System.out.println(e+" "+temp); } Thread.sleep(2000); } } public void getData(){ //创建并启动四个获取随机数的线程,这四个线程交替向MyQueue队列中添加元素 Thread1 th1 = new Thread1(); Thread2 th2 = new Thread2(); Thread3 th3 = new Thread3(); Thread4 th4 = new Thread4(); th1.start(); th2.start(); th3.start(); th4.start(); } public Double integral(double e){ //计算定积分 double step = (e-0)/N; double left = 0,right = step; double sum = 0; while(right <= e){ double mid = left+(right-left)/2; sum+=Math.sin(mid); left+=step; right+=step; } sum*=step; return sum; } }
对于包含多个变量的不变性条件中,其中涉及的所有变量都需要由同一个锁来保护
synchronized (this) { //两个状态变量在同一个原子操作中更新 lastNumber.set(e); lastRes.set(temp); }
锁的重入
如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功,“重入”意味着获取锁的操作的粒度是‘线程',而不是‘调用'。
重入的一种实现方式 :
为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
如果内置锁不可重入,那么以下这段代码将发生死锁(每个doSomething方法在执行前都会获取Father上的内置锁) ---------------------------------------------------------------------- public class Father{ public synchronized void doSomething(){ } } public class Son extends Father{ @Override public synchronized void doSomething(){ System.out.println("重写"); super.doSomething(); } }
线程安全性与性能和活跃性之间的平衡
活跃性:是否会发生死锁饥饿等现象
性能:线程的并发度
不良并发的应用程序:可同时调用的线程数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,可以平衡这个问题。
缩小作用范围的原则====>当执行时间较长的计算或者可能无法快速完成的操作时,一定不能持有锁!!!
总结
本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注猪先飞的更多内容!
相关文章
- 这篇文章主要介绍了如何利用java语言实现经典《复杂迷宫》游戏,文中采用了swing技术进行了界面化处理,感兴趣的小伙伴可以动手试一试...2022-02-01
C# WinForm多线程解决界面卡死问题的完美解决方案,使用BeginInvoke
问题描述:当我们的界面需要在程序运行中不断更新数据时,当一个textbox的数据需要变化时,为了让程序执行中不出现界面卡死的现像,最好的方法就是多线程来解决一个主线程来创建界...2020-06-24java 运行报错has been compiled by a more recent version of the Java Runtime
java 运行报错has been compiled by a more recent version of the Java Runtime (class file version 54.0)...2021-04-01- 这篇文章主要介绍了在java中获取List集合中最大的日期时间操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了教你怎么用Java获取国家法定节假日,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下...2021-04-23
- 这篇文章主要介绍了Java如何发起http请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-31
- 说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别...2020-06-25
- 这篇文章主要介绍了解决Java处理HTTP请求超时的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了c# 多线程处理多个数据的方法,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下...2021-03-31
- 这篇文章主要介绍了java 判断两个时间段是否重叠的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
java 画pdf用itext调整表格宽度、自定义各个列宽的方法
这篇文章主要介绍了java 画pdf用itext调整表格宽度、自定义各个列宽的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-31- 这篇文章主要介绍了超简洁java实现双色球若干注随机号码生成(实例代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-02
- 这篇文章主要介绍了Java生成随机姓名、性别和年龄的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-01
- 这篇文章主要介绍了C#基于委托实现多线程之间操作的方法,实例分析了C#的委托机制与多线程交互操作的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
- 这篇文章主要介绍了java正则表达式判断前端参数修改表中另一个字段的值,需要的朋友可以参考下...2021-05-07
Java使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
这篇文章主要介绍了Java使用ScriptEngine动态执行代码,并且分享Java几种动态执行代码比较,需要的朋友可以参考下...2021-04-15- 这篇文章主要介绍了Java开发实现人机猜拳游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-03
- 这篇文章主要介绍了C#多线程中的异常处理操作,涉及C#多线程及异常的捕获、处理等相关操作技巧,需要的朋友可以参考下...2020-06-25
- 这篇文章主要介绍了Java List集合返回值去掉中括号('[ ]')的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-29
- 这篇文章主要介绍了C#中异步和多线程的相关资料,帮助大家更好的理解和学习c#,感兴趣的朋友可以了解下...2021-01-16