目录
系统角度分析出现线程不全的原因——内存的可见性问题(memory visible)
线程安全(Thread Safe)
不安全:单看代码“没问题”的情况下,但结果是错误的
安全:代码的运行结果应该是100%符合预期
线程不安全的原因
1.开发者角度
a.多个线程之间操作同一块数据(共享数据)——不仅仅是内存数据
b.至少有一个线程在修改这块共享数据
在多线程的代码中,哪些情况下不需要考虑线程安全问题?
①几个线程之间互相没有任何数据共享的情况下,天生是线程安全的;
②几个线程之间即使有共享数据,但都是做读操作,没有写操作时,也是天生线程安全的。
2.系统角度
前提知识:
①Java代码中的一条语句,很可能对应多条指令
②线程调度是可能发生在任意时刻的,但是不会切割指令
本质是没有保证原子性(actomic),所以才出错。
原子性被破坏是线程不安全的最常见的原因
为什么执行次数越多,出错率越大?
次数越多,线程执行需要卡时间片的概率越大(碰到线程调度的概率越大),导致中间出错的概率越大。
系统角度分析出现线程不全的原因——内存的可见性问题(memory visible)
CPU中为了提升数据获取速度,以=一般在CPU中设置缓存(Cache)
指令的执行速度>>内存的读写速度
JVM规定了JVM内存模型
主存储/主内存:真实内存。
工作存储/工作内存:CPU中缓存的模拟
线程的所有数据操作(读写)必须:
1.从主内存加载到工作内中
2.在工作内存中进行处理,允许在工作内存中处理很久
3.完成最终的处理之后,再 把数据同步回主内存
内存可见性:一个线程对数据的操作,很可能其他线程是无法感知的,甚至,某些情况下,会被优化成完全看不到的结果
系统角度分析出现线程不全的原因——代码重排导致的问题
所谓的重排序,就是指:执行的指令不和书写指令并不一致。
JVM规定了一些重排序的基本原则:happend-before规则
解释:JVM要求,无论怎么优化,对于单线程的视角,结果不应该有改变。但并没有规定多线程环境的情况下(并不是不想规定,而是不能规定),导致在多线程环境下可能出问题
ArrayList ,LinkedList, PriorityQueue,TreeMap,TreeSet,HashMap,HashSet,StringBilder都不是线程安全。
线程安全的: Vector,Stack,Dictionary ,StringBuffer.
为什么不是线程安全(以ArrayList为例)——多个线程同时对一个ArrayList对象有修改操作时,结果会出错。
锁(lock)
synchronized锁:同步锁/monitor锁
语法:
1.修饰方法(普通,静态方法)->同步方法
synchronized int add(……){}
修饰普通方法视为对“当前对象”加锁
修饰静态方法视为所在的类加锁
2.同步代码块
synchronized(引用){}
类名.class
是一个引用,指向关于这个类对象,不是这个类实例化出来的对象,而是这个类数据表现出的对象,每个被加载进来的类,都可以通过 类名.class访问到。
每个被加载的类有且仅有一个Class对象。
锁理论上,就是一段数据(一段被多个线程之间共享的数据)。 实现了代码的互斥
尝试加锁的内部操作:
1.整个尝试加锁的操作已经被JVM保证了原子性
当多个线程:1.都有加锁操作时2.并且申请的是同一把锁时。
会造成,加锁,代码s(临界区代码),解锁
和join()相比锁更灵活
为什么会互斥?
互斥的必要条件:线程都有加锁操作&&同一把锁,锁的是同一个对象
public class SomeClass {
synchronized void m1() { }
synchronized static void m2() { }
void m3() { }
void m4() {
synchronized (this) { }
}
void m5() {
synchronized (SomeClass.class) { }
}
Object o1 = new Object();
void m6() {
synchronized (o1) { }
}
static Object o2 = new Object();
void m7() {
synchronized (o2) { }
}
}
SomeClass s1=new SomeClass();
SomeClass s2=new SomeClass();
SomeClass s3=s1;
synchronized(ref){}当ref==null的时候,一定会有NullPointerException
正确使用synchronized的作用
1.保证了临界区的原子性
加锁的“粒度”不同,会影响性能,但最好值是一个需要工程测量的取值,
2.在有限程度上可以保证内存可见性
加锁:加锁成功之前清空当前线程的工作内存
解锁:解锁前保证把工作内存中的数据全部同步回主内存。
3.可以给代码重排增加一定的约束
举个例子
用法:
Lock lock=new ReentrantLock();
lock.lock();
try{
//临界代码
}finally{
lock.unlock();
}
该方法的结束以两种形式出现:
1,正常结束,并返回
2.异常结束,抛出异常。
可能导致该方法结束的情况:
1.超时时间内(这里的5s),加锁成功了正常返回,返回true。
2.超时时间到了(5s),加锁失败,正常返回,返回false。
3.超市时间内(5s),加锁还没成功但是线程被终止了,异常返回,捕获到InterrptedExceprion.
这个方法结束的情况:
1.不限时间等待过程中,加锁成功了,等待过程可能是永远。
2.在请求锁过程中,由于线程被终止了导致方法结束,以抛出InterrptedExceprion形式体现
interrupt():让线程停止,没有释放锁。
juc下的锁的优点
1.书写灵活
可以一个方法加锁,到另一个方法中解锁
2.锁的类型更灵活
3.锁的加锁策略更灵活
a.一直请求锁
b.带中断
c.尝试请求
d.带超时的尝试
缺点:
容易忘记写lock.unlock()导致锁一直不释放。但这也是synchronized锁的优点(一定会带有锁的释放)
线程状态——阻塞状态
blocked,waiting,timed_waiting
blocked:专指请求synchronized锁失败时的状态
直接上状态转移图