线程不安全类的产生的原因是什么?我们来分析一下!

1.前言
我们都知道,对于线程不安全的类,我们需要采用一些方法去保证线程安全;那么,我们首先要知道什么类是线程不安全的。

2.set相关
如果说:对于,一个资源来说:所有的线程都是去读的,那么,这个资源就是线程安全的。(不涉及资源的更改)但是,如果,有写操作时,就可能导致线程不安全了;

线程安全类定义:不存在竞态条件(类中不存在被修改的成员变量),或存在时进行了同步控制

举例说明:SimpleDateFormat

private StringBuffer format(Date date, StringBuffer toAppendTo,                             FieldDelegate delegate) {     // 这行语句会导致线程不安全;                             calendar.setTime(date);      boolean useDateFormatSymbols = useDateFormatSymbols();      for (int i = 0; i < compiledPattern.length; ) {         int tag = compiledPattern[i] >>> 8;         int count = compiledPattern[i++] & 0xff;         if (count == 255) {             count = compiledPattern[i++] << 16;             count |= compiledPattern[i++];         }          switch (tag) {         case TAG_QUOTE_ASCII_CHAR:             toAppendTo.append((char)count);             break;          case TAG_QUOTE_CHARS:             toAppendTo.append(compiledPattern, i, count);             i += count;             break;          default:             subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);             break;         }     }     return toAppendTo; } 欢迎大家加群:1142951706  一起交流吹水 

calendar.setTime(date);这里进行设置date;
正常情况下:
线程1,执行了calendar.setTime(date),将date1 设置成了后面格式化所需要的时间;
线程1,将data1把数据进行转换,转成标准格式
线程2,执行了calender.setTime(data),将data2 设置成了后面格式化所需要的时间;
线程2,将data2把数据进行转换,转成标准格式

非正常情况:
线程1,执行了calendar.setTime(date),将date1 设置成了后面格式化所需要的时间;
线程1,暂停执行,线程2得到CPU时间片开始执行
线程2,执行了calender.setTime(data),将data2 设置成了后面格式化所需要的时间;
线程2,暂停执行,线程1得到CPU时间片开始执行
线程1,由于,calender是实例变量,现在,线程1的calendr也是指向的是data2,所以,将data2的数据进行转换,转成标准格式
线程2,将data2的数据进行转换,转成标准格式

所以,由于,在set实例变量的过程中,没有使用锁来控制执行顺序,导致,第一个data1被第二个data2进行覆盖;所以,输出的都是data2格式化后的内容,造成线程不安全;

竞态条件&临界区

当多个线程访问同一个资源时,对先后顺序敏感,就存在竞态条件。导致竞态条件发生的代码区称为临界区。
以上面例子为例:SimpleDateFormat.format()这个方法,如果按照顺序执行的话,那么,是没有问题的;如果,不按照顺序执行的话,就会有问题;对先后敏感,所以,存在竞态条件。
总结:当多个线程访问共享资源变量时,并且进行了写操作,就会引发竞态条件;

3.类中能造成线程不安全的共享资源

3.1.方法中的局部基本变量

因为,在方法中会有栈封闭技术,每执行一个方法,压入一个栈帧;而在栈帧中,上面存放局部变量表,来存8大基本数据类型+对象的引用类型;在方法里新建的局部变量,实际上是存储在每个线程私有的栈帧,而每个栈的栈帧是不能被别的线程访问到的。所以,就不会有线程安全问题;

3.2 方法中的局部引用对象

对象引用存在每个线程的线程栈中,但是,new出来的对象是在堆中的,如果,在某个方法中,这个new出来的对象,不会被别的线程获取到,那么,当方法执行完毕后,该对象随着栈帧的出栈而回收;这个时候,也是线程安全的;当然,如果,这个对象被别的线程获取到,并且进行了修改。那么,就是线程不安全的;

3.3 类的成员变量

当两个线程对类的成员变量同时进行修改的时候,会产生竞态条件,就会有线程安全的问题;

4. 执行顺序

当不同线程执行时,结果和执行顺序有关,这个时候,就会导致线程安全问题;比如说:我想去读一个文件,自然时需要在这个文件写完之后,如果,线程配合不好,在没写完的时候,就进行读取,这就会导致顺序上的错误,解决这个问题,可以使用ReentrantReadWriteLock 、CountDownLatchSemaphore ;

5.发布逸出

发布逸出:就是说,对象不应该提供给外界的,却提供给了外界;
举例:下面是一个工具类,提供给各个线程进行使用的,让各个线程可以查询map
public class MultiThreads {
private Map<String, String> states;

public MultiThreads() {     states = new HashMap<>();     states.put("1", "周一");     states.put("2", "周二");     states.put("3", "周三");     states.put("4", "周四"); }  public Map<String, String> getStates() {     return states; } public static void main(String[] args) {     MultiThreads multiThreads = new MultiThreads();     Map<String, String> states = multiThreads.getStates();     System.out.println(states.get("1"));     states.remove("1");      Map<String, String> newStates = multiThreads.getStateImproved();     System.out.println(newStates.get("1")); } 

main函数执行如下:一个线程执行完毕后,把这个数据进行修改;其他线程获取到的数据就是错的,原因:getStates()方法,把本来只能在本类使用的map进行了返回;

周一 null 

如何解决:可以使用返回副本的方法,这样一个线程修改了,别的线程再获取到的也是原来的数据

public Map<String ,String> getStateImproved() {     return new HashMap<>(states); } 

6.总结

在线程中,对成员变量进行修改时,会导致线程不安全;然后,就是启用多线程后,对线程的执行顺序有要求的话,就会导致线程不安全的发生;启动多线程时,如果,要看看执行过程是否和顺序有关,如果,和顺序有关的话,也要注意线程不安全问题;写并发相关类时,不要提供修改实例变量的方法;

给大家推荐一个免费的学习交流群:

最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

Java开发交流君样:1142951706

版权声明:玥玥 发表于 2021-05-20 2:07:02。
转载请注明:线程不安全类的产生的原因是什么?我们来分析一下! | 女黑客导航