李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
11.并发编程之变量的线程安全分析
Leefs
2022-10-11 PM
675℃
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
43
标签云
Filter
DataWarehouse
Map
容器深入研究
算法
Livy
Nacos
JavaWEB项目搭建
Git
Typora
Spark Streaming
工具
ClickHouse
Yarn
RSA加解密
Tomcat
SQL练习题
Hadoop
Spark SQL
Thymeleaf
随笔
Spark
持有对象
队列
BurpSuite
Eclipse
Jenkins
Spark Core
二叉树
NIO
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞