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,快速升职加薪,走上人生巅峰。