有些代码在多线程环境下执行会出现 bug .这样的问题就称为 “线程不安全”
线程不安全的原因
1.抢占式执行
2.多个线程修改同一变量
3.修改操作不是原子的
例如:count++ => 1.读取 2.加加 3.写入
4.内存可见性问题
* 编译器可能会把代码转为另外一种执行逻辑,等价转换后逻辑不变,但是效率变高了
5.指令重排序
例如:
class Counter {
public int count = 0;
public void increase() {
count++;
}
}
public class test6 {
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0; i < 50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < 50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
预期输出 100000,很明显我们的多线程出现了问题
保障线程安全
锁(synchronized)
1.修饰方法(锁对象相当于 this)
对代码进行加锁后结果显示正确
2.修饰代码块
括号内需填写加锁的对象,如果填写 “this”,就是对当前对象加锁 (谁调用的 increase 方法谁就是 this)
任意对象都可以在 synchronized 里面作为锁对象,我们往往更关注两个线程是否锁同一个对象,是否会发生锁竞争
3. 修饰静态方法 (锁对象相当于类对象(不是锁整个类))
* 无论锁对象是什么形态,什么类型,核心原则都是两个线程争一个锁对象就会有竞争,不同锁对象就没有竞争
可重入锁
如同上述代码,一个线程针对一把锁连续加锁两次:
第一次加锁,可以加锁成功
第二次加锁,就会加锁失败(锁已经被占用)
然而若是想让第一把锁解开,就必须加上第二把锁,因此锁死。
那这时候就会有个问题,既然构成了 “死锁” 为什么 IDEA 不给我们一点提示呢?
这时候就要引入一种概念 —— 可重入锁
我们可以简单认为:
针对上述情况,不会产生死锁的锁,就可以称为 “可重入锁”
针对上述情况,会产生死锁的锁,称为 “不可重入锁”
很明显我们并不能在这个位置进行解锁,那我们的编译器又是怎么知道什么时候该干什么的呢?
这时候我们就要谈到 可重入锁的实现要点:
1. 让锁离持有线程对象,记录是谁加了锁
2. 维护一个计数器,用来平衡什么时候是真加锁,什么时候是真解锁,什么时候是直接放行
Java 标准库中的线程安全类
java 标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁的措施
* ArrayList
* LinkedList
* HashMap
* TreeMap
* HashSet
* TreeSet
* StringBulider
但是还是一些是线程安全,使用了一些锁机制来控制
* Vector(不推荐使用)
* HashTable(不推荐使用)
* ConcurrentHashMap
* StringBuffer
还有的虽然没有加锁,但是不涉及 “修改”,仍然是线程安全的
* String
Volatile
观察上述代码和运行结果可以发现,当我们对 counter.count 进行修改之后 t1 线程的循环并没有结束,这就是内存可见性引来的线程安全问题
由于比较这个操作需要在内存中读取变量的值来进行比较,但是这个操作的开销又很大,并且 while 循环执行速度很快,因此编译器发现在 while 循环中没有人改 count 的内存(也会有编译器不优化的情况,在这里不进行过多分析),就读代码进行了 “优化” ,不再每次循环都读取内存,而是只读取一次,后续的读内存的操作则简化成直接读寄存器,所以没有发现在另一个线程中已经对 count 的内存进行了修改
那么优化方案便很明显了,就是不让编译器对我们的代码进行优化
做法非常简单,就是在我们的变量前面加上 volatile 关键字
可以看见代码正确了!!!
注意:volatile 起到的效果是 “保证内存可见性”,但并不保证原子性,也就是说:针对一个线程读,一个线程改,使用 volatile 这个是合适的;而针对两个线程修改,volatile 是无能为力的
站在 JMM (Java 内存模型) 的角度来看待 volatile:
正常程序执行的过程中,会把主内存的数据先加载到工作内存中,再进行计算处理,编译器优化可能会导致不是每次都真正的读取主内存,而是直接取工作内存中的缓存数据. (就可能导致内存可见性问题),volatile 起到的效果就是保证每次读取内存都是真正的从主内存重新获取
wait 和 notify
public class test9 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
// 第一个线程,进行 wait 操作
Thread t1 = new Thread(() -> {
while (true) {
synchronized (object) {
System.out.println("wait 之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 之后");
}
}
});
t1.start();
// 第二个线程,进行 notify
Thread t2 = new Thread(() -> {
while (true) {
synchronized (object) {
System.out.println("notify 之前");
object.notify();
System.out.println("notify 之后");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t2.start();
}
}
wait / notify 控制多线程之间的执行先后顺序
1. 都要搭配 synchronized 来进行使用
2. wait 和 notify 得使用同一个对象才有效
3. 用来加锁的对象和 wait / notify 对象也得一致
4. 即使当前没有线程在 wait,直接 notify 也不会有副作用
wait 和 sleep 的对比
都是让线程进入阻塞等待的状态
sleep 是通过时间来控制何时唤醒的
wait 则是由其他线程通过 notify 来唤醒的
wait 其实还有一个重载版本,参数可以传时间,表示等待的最大时间