李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
11.并发编程之变量的线程安全分析
Leefs
2022-10-11 PM
1361℃
0条
[TOC] ### 一、**成员变量和静态变量是否线程安全?** - 如果它们没有共享,则线程安全 - 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 - 如果只有读操作,则线程安全 - 如果有读写操作,则这段代码是临界区,需要考虑线程安全 ### 二、局部变量是否线程安全? - 局部变量是线程安全的 - 但局部变量引用的对象则未必 - 如果该对象没有逃离方法的作用范围,它是线程安全的 - 如果该对象逃离方法的作用范围,需要考虑线程安全 ### 三、局部变量线程安全分析 **示例代码** ```java public static void test1() { int i = 10; i++; } ``` 每个线程调用 `test1()` 方法时局部变量 i,**会在每个线程的栈帧内存中被创建多份**,因此不存在共享 ```java public static void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 0, 1 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 3 4 0 i I ``` **如下图所示** ![11.并发编程之变量的线程安全分析01.png](https://lilinchao.com/usr/uploads/2022/10/2571899213.png) 当`test1()`被多个线程同时调用时,每个线程在调用时都会独立开辟一个栈出来,局部变量i将会在对应的栈帧中进行运算,栈与栈之间相互独立,所以不会产生线程安全问题。 ### 四、成员变量线程安全分析 看一个成员变量的例子 ```java import java.util.ArrayList; public class Test04 { static final int THREAD_NUMBER = 2; static final int LOOP_NUMBER = 200; public static void main(String[] args) { ThreadUnsafe test = new ThreadUnsafe(); //循环两次,开启两个线程 for (int i = 0; i < THREAD_NUMBER; i++) { //开启新线程调用ThreadUnsafe类的method1方法 new Thread(() -> { test.method1(LOOP_NUMBER); }, "Thread" + i).start(); } } } class ThreadUnsafe{ //成员变量list集合 ArrayList
list = new ArrayList<>(); //method1方法会循环调用method2和method3方法,进行向成员变量list中先增加、再删除元素 public void method1(int loopNumber){ for (int i = 0; i < loopNumber; i++){ //临界区,会产生竞态条件 //{ method2(); method3(); // } } } //调用list的add方法添加元素 private void method2(){ list.add("1"); } //调用list的remove方法从集合中移除一个元素 private void method3(){ list.remove(0); } } ``` **运行结果** ``` Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:653) at java.util.ArrayList.remove(ArrayList.java:492) at com.lilinchao.thread.demo02.ThreadUnsafe.method3(Test04.java:42) at com.lilinchao.thread.demo02.ThreadUnsafe.method1(Test04.java:30) at com.lilinchao.thread.demo02.Test04.lambda$main$0(Test04.java:16) at java.lang.Thread.run(Thread.java:748) ``` **说明** ![11.并发编程之变量的线程安全分析02.png](https://lilinchao.com/usr/uploads/2022/10/3347224894.png) + 创建的list对象是存储在堆中,而方法的调用是在每个线程对应的栈中进行; + 每个栈帧中通过记录堆中对象的引用地址来和对象进行绑定; + 因为List集合是成员变量,在栈中同时被两个线程的method2和method3栈帧所共享; **报错原因分析** ArrayList集合并非是线程安全的,多线程情况下,在调用add()方法或remove()方法时,很可能发生指令错乱,下面我们来分析其中一种情况。 ![11.并发编程之变量的线程安全分析03.jpg](https://lilinchao.com/usr/uploads/2022/10/949256699.jpg) + 线程一执行add方法时,先从`ArrayList`中读取集合数据长度0,以确定将数据添加到集合中0元素的位置; + 线程一在位置0添加一个元素,但是并未将元素写入`ArrayList`中去,此时CPU发生了上下文切换; + 测试,线程二获取到了CPU的执行权,也开始读取`ArrayList`集合长度,此时size仍然是0; + 线程二向集合0元素位置添加一个元素完成,CPU执行权被线程一获取到; + 线程一仍然完成之前未完成的操作,将元素添加在`ArrayList`的0元素位置,这时,线程二之前在0元素位置添加的数据将会被覆盖掉; + 当线程一和二同时执行remove方法,同时删除0位置的元素时,就会发生数组角标越界异常。 *解决方法:可以将list修改成局部变量,那么就不会有上述问题了* #### List 修改为局部变量 ```java class ThreadSafe { public final void method1(int loopNumber){ ArrayList
list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++){ method2(list); method3(list); } } private void method2(ArrayList
list) { list.add("1"); } private void method3(ArrayList
list) { list.remove(0); } } ``` **分析** - list 是局部变量,**每个线程调用时会创建其不同实例**,没有共享 - 而 `method2` 的参数是从 `method1` 中传递过来的,与 `method1` 中引用同一个对象 - `method3` 的参数分析与 `method2` 相同 ![11.并发编程之变量的线程安全分析03.png](https://lilinchao.com/usr/uploads/2022/10/2547096229.png) #### 子类覆写父类方法 *方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代来线程安全问题?* - 情况1:有其它线程调用 method2 和 method3 - **其他线程直接调用method2 和 method3传过来的 list 与method1传进去的不是同一个,因此不会有问题** - 情况2:在 情况1 的基础上,为 `ThreadSafe` 类添加子类,子类覆盖 `method2` 或 `method3` 方法 ```java class ThreadSafe { public final void method1(int loopNumber){ ArrayList
list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++){ method2(list); method3(list); } } public void method2(ArrayList
list) { list.add("1"); } public void method3(ArrayList
list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe{ @Override public void method3(ArrayList
list) { new Thread(() -> { list.remove(0); }).start(); } } ``` **运行结果** ``` Exception in thread "Thread-391" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:653) at java.util.ArrayList.remove(ArrayList.java:492) at com.lilinchao.thread.demo02.ThreadSafeSubClass.lambda$method3$0(Test05.java:39) at java.lang.Thread.run(Thread.java:748) ``` **分析** `ThreadSafeSubClass extends ThreadSafe`,重写了父类`method3`,开辟了新线程,共享list,即出现了子类与父类共享资源,因此出现问题。 **不能控制子类的行为,造成了线程安全的问题** > 从这个例子可以看出 private(限制子类不能重写父类) 或 final (不可继承)提供【安全】的意义所在,请体会开闭原则中的【闭】 访问修饰符在一定程度上,保护了线程安全 ### 五、常见线程安全类 - String - Integer - StringBuffer - Random - Vector - Hashtable - java.util.concurrent 包下的类 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。 **示例** ```java Hashtable table = new Hashtable(); new Thread(()->{ table.put("key", "value1"); }).start(); new Thread(()->{ table.put("key", "value2"); }).start(); ``` 也可以理解为它们的**每个方法是原子的**,但**注意**它们**多个方法的组合不是原子的(不是线程安全的)**。 #### 线程安全类方法的组合 分析下面代码是否线程安全? ```java Hashtable table = new Hashtable(); // 线程1,线程2 if(table.get("key") == null) { table.put("key", value); } ``` ![11.并发编程之变量的线程安全分析04.png](https://lilinchao.com/usr/uploads/2022/10/1615575447.png) #### 不可变类线程安全性 `String、Integer` 等都是不可变类(final类),因为其内部的状态不可以改变,因此它们的方法都是线程安全的 同学或许有疑问,`String` 有 `replace`,`substring` 等方法可以改变值啊,那么这些方法又是如何保证线程安全的呢? 答案:**创建新的字符串对象** + **`subString()`源码** ```java public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex;//截取长度 = 总长度 - 索引下标 if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } //若索引为0?返回本身:创建新的字符串对象 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); } ``` + **`String`构造器源码** ```java //value为char数组 public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } //创建value字符串时,在原有字符串的基础上进行复制,赋值给新字符串(没有改动原有对象属性,直接创建新的) this.value = Arrays.copyOfRange(value, offset, offset+count); } public class Immutable{ private int value = 0; public Immutable(int value){ this.value = value; } public int getValue(){ return this.value; } } ``` 如果想增加一个增加的方法呢? ```java public class Immutable{ private int value = 0; public Immutable(int value){ this.value = value; } public int getValue(){ return this.value; } public Immutable add(int v){ return new Immutable(this.value + v); } } ``` *附参考原文:* *《黑马程序员之并发编程》*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2480.html
上一篇
10.并发编程之线程八锁
下一篇
12.并发编程之线程安全实例分析
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Nacos
Beego
查找
Stream流
Http
JavaWEB项目搭建
Elasticsearch
NIO
JavaScript
Spark Streaming
Typora
Docker
Linux
MyBatis-Plus
HDFS
Golang
数学
稀疏数组
随笔
Eclipse
微服务
BurpSuite
序列化和反序列化
RSA加解密
Spark Core
JVM
Livy
机器学习
VUE
Flink
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭