线程不安全现象出现的原因
开发者角度
1.多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据
2.至少有一个线程在修改这块共享数据,多个线程中至少有一个对共享数据做修改操作
系统角度
前置知识:
1.Java代码中的一条语句,很可能对应多条指令,r++实际上就是r=r+1
变成指令动作:
1.从内存中(r代表的内存区域)把数据加载到寄存器中
2.完成数据加一的操作
3.把寄存器中的值写回到内存中(r代表的内存区域)
2.线程调度是可能发生在任意时刻的,但是不会切割指令(一条指令只有执行完或者完全没有执行这两种状态)
原子性
程序员的预期是r++或者r–是一个原子性的操作(全部完成或者全部没完成),但实际执行起来,保证不了原子性,所以会出错。
原子性被破坏是线程不安全的最常见的原因
为什么COUNT越大,出错概率越大
COUNT越大,线程执行需要跨时间片的概率越大,导致中间出错的概率越大,碰到线程调度的概率也就越大
系统角度分析出现线程不安全的原因——内存可见性问题
前置知识:CPU中为了提升数据获取速度,一般在CPU中设置缓存 ,指令的执行速度》》内存的读写速度
主内存和工作内存
JVM规定了JVM内存模型,把一个线程想成一个CPU,主存储和主内存就是真实内存,工作存储或者工作内存就是CPU中缓存的模拟
1.把r从主内存放到当前线程的工作内存中
2.循环r++,完成1000次(在工作内存中完成),中间允许同步回主内存
3.在1000放回r
线程的所有数据操作必须:
1.从主内存加载到工作内存中
2.在工作内存中进行处理,允许在工作内存中处理很久
3.完成最终的处理之后,再把数据同步回主内存
内存可见性:
一个数据对线程的操作,很可能其他线程无法感知,甚至在某些情况下会被优化成完全看不到的结果
系统角度看线程不安全问题——代码重排序导致的问题
1.我们写程序,往往是经过中间很多环节优化的结果,并不保证最终执行的语言和我们写的语句是一模一样的,所谓的重排序就是指执行的指令和书写的指令不一致
2.JVM规定了一些重排序的基本原则:happend-before规则:
JVM要求,无论怎么优化,对于单线程的视角,结果不应该有改变,但并没有规定多线程环境的情况,导致在多线程环境下可能出问题
线程安全总结
1.什么是线程安全?
1)程序的线程安全——运行结果100%符合预期(无法实操,用来理解)
2)Java语境下,经常说某个类某个对象是线程安全的:这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是简单使用,可以不考虑线程安全的问题;ArrayList就不是线程安全的——ArrayList实现中,完全没考虑过线程安全的任何问题,无法直接使用在多线程环境(无法实现多个线程同时操作同一个ArrayList)
2.作为程序员如何考虑线程安全问题?
1.尽可能让几个线程之间不做数据共享,各干各的,就不需要考虑线程安全问题了;如归并排序中,四个线程虽然处理的是同一个数组,但提前画好范围,各做各的
2.如果非要有共享操作,尽可能不去修改,而是只读操作;static final int COUNT=,即使多个线程同时使用这个COUNT也无所谓的
3.一定会出现线程问题了,问题的原因从系统角度讲:
1)原子性被破坏了
2)由于内存可见性问题,导致某些线程读到脏
3)由于代码重排序导致的线程之间关于数据的配合出问题了
一些常见类的线程安全问题
ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet、StringBuilder都不是线程安全的
ArrayList为什么不是线程安全的
多个线程同时对一个ArrayList对象有修改操作时,结果会出错
最常见的违反原子性的场景
1.read-write场景
i++;
array[size]=e;
size++;
2.check-update场景
if(a==10){
a=…;
}
锁(lock)
synchronize
语法:
1.修饰方法(普通、静态方法)——》同步方法
synchronize int add(…){
…
}
2.同步代码块
synchronize(引用){
}
类名.class
是一个引用,指向关于这个类对象(不是这个类实例化出来的对象而是这个类数据表现出的对象,每个被加载进来的类都可以通过类名。class访问到)