1.数据共享原则
线程共享的内存区域:堆区(对象实例)、方法区、运行时常量池区(被加载的类)
线程私有的区域:PC、栈(局部变量)
局部变量是线程私有的,类(静态属性)是线程之间共享的,对象(属性)是线程之间共享的(需要线程持有引用)
线程安全Thread safe:运行结果100%符合预期
java经常说某个类、对象线程是安全的:这个类、对象中的代码已经考虑了处理多线程的问题,如果只是“简单”使用,可以不考虑线程安全问题。
ArrayList就不是线程安全的,完全没考虑过线程安全的任何问题,无法直接使用在多线程环境(多个线程同时操作同一个ArrayList)。
如何考虑线程安全问题?
1.尽可能让几个线程之间不做数据共享,各干各的,就不需要考虑线程安全问题了。
2.如果非要有共享操作,尽可能不去修改,而是只读操作。
一定会出现线程问题
1.原子性被破坏
2.由于内存可见性问题,导致某些线程读到了脏数据(当前数据与试试不匹配)
3.由于代码重排序导致的线程之间关于数据的配合出问题了。
所以,需要学习一些机制,目标与JVM沟通,避免上述问题发生
什么是线程不安全?
public class phenomenon {
//定义一个共享属性 ——静态属性的方式来体现
static int r =0;
//定义加减的次数
//COUNT越大,出错的概率越大
static final int COUNT = 1000000;
//定义两个线程,分别对r进行加减法操作
static class Add extends Thread{
@Override
public void run() {
for(int i =0 ;i< COUNT; i++){
r++;
}
}
}
static class Sub extends Thread{
@Override
public void run() {
for(int i =0;i<COUNT; i++){
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
add.start();
Sub sub = new Sub();
sub.start();
add.join();
sub.join();
//理论上,被加减了count次,结果应该是0
System.out.println(r);
}
}
每次出现的结果都不相同,代码运行的结果不符合预期
线程不安全现象出现的原因:
1.开发者角度
(1)多个线程之间操作同一块数据了(数据共享)——不仅仅是内存数据
(2)至少有一个线程在修改这块共享数据
即使在多线程的代码中,那些情况不需要考虑线程安全问题?
a.线程之间互相没有任何数据共享的情况下,天生线程是安全的
b.线程之间即使有共享数据,但是都做读操作,没有写操作,也是天生线程安全的
2.系统角度解释
(1)java代码中的一条语句,很肯对应多条指令
(2)线程调度可能发生在任意时刻,但是不会切割指令(一条指令只有执行完/完全没有执行)
为啥count越大,出错概率越大?
COUNT越大,线程执行需要跨时间片的概率越大,导致中间出错的概率越大
原子性被破坏是线程不安全的常见的原因
系统角度分析出现线程不安全的原因 ——内存可见性的问题
CPU为了提升数据获取速度,一般在CPU中设置缓存cache
指令的执行速度 >> 内存的读写速度
主存储、主内存:真实内存
工作存储/工作内容:CPU中缓存的模拟(不需要区分L几缓存)
内存可见性:一个线程对数据的操作,很可能其他线程是无法感知的,甚至,在某些情况下,会被优化成完全看不到的结果!
系统角度看线程不安全问题 —— 代码重排序导致的问题
程序:状态机
我们写的程序,往往是经过中间很多环节优化的结果,并不保证最终执行的语言和我们写的语句是一模一样的。
JVM规定了一些重排序的基本原则:happend-before规则
JVM要求,无论怎么优化,对于单线程视角,结果不应该有改变。
学习过的常见类的线程不安全:
ArrayList、LinkedLitst、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet都不是安全的
Vector、Stack、Dictionary、StringBuffer
这几个都是java设计失败的产品
StringBuilder VS StringBuffer的区别:一个线程安全,一个不安全
最违反原子性的场景:
1.read - write场景
i++;
arrat[size] = e;size++;
2.check-update场景
if(a ==10){
a=...;
}
锁lock(synchronized) 也称同步锁
1.语法
修饰方法(普通、静态方法) ——被修饰后称为同步方法
synchronized int add(){}
2.同步代码块
sychrnoized(引用){
}
加锁机制:
sync void methord(){} 等价于 void methord(){ sync(this){} }
static sync void methord(){} 等价于static void methord(){ sync(类.class){} }
只要解释清楚同步代码块是如何工作的,就能理解同步方法是怎么工作的
2.锁
理论上是一段数据(一段被多个线程之间互相共享的数据)
static boolean lock = false;
所以一共两种状态{锁上(locked)、打开(unlocked)}
false:unlocked true:locked
当多个线程都有加锁操作、并且申请的是同一把锁时,会造成 加锁 代码s(临界区) 解锁
临界区代码会互斥着进行