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

13 篇文章 0 订阅
8 篇文章 1 订阅

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 、CountDownLatch\Semaphore ;

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

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值