线程安全问题是因为在多线程的调度下,存在随机性,也正是因为随机性导致多个线程共同争抢资源,从而出现BUG
一.什么是线程安全
在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这种随机性,才会让代码中产生很多bug如果因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的,如果这样的调度,并没有让代码产生ug。我们则认为线程是安全的。
本质上线程不安全是因为线程之间的调度顺序是不确定的。
二 线程不安全的原因
1.抢占式执行;
2.多个线程修改同一个变量;
3.修改操作,不是原子的;
4.内存可见性,引起的线程不安全;(volatile)
5.指令重排序,引起的线程不安全
三 解决线程安全问题
3.1 加锁sychronized
加锁能够起到保证“原子性”的效果。锁的核心操作有两个:加锁和解锁。一但某个线程加锁了之后,其他线程也想加锁就不能直接加上,就需要阻塞等待,一直等到拿到锁的线程释放了锁为止。当1号释放锁之后,等待锁的2号和3号线程,谁能抢先一步拿到锁,谁就成功加锁。
加锁本质上是把并发的变成了串行的
package asd;
/**
* 使用两个线程来累加 count 的值
* 每个线程循环 1w 次,累加变量 count 的值,count 默认值为 0,注意线程安全问题。
*/
class Counter{
private int count = 0;
public void add(){
synchronized (this){
count++;
}
}
public int get(){
return count;
}
}
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.get());
}
}
在上述代码中,这两个线程是在竞争同一个锁对象,就会产生锁竞争(t1拿到锁,t2就会阻塞)此时就可以保证++操作就是原子的相互不会影响了.
()内的是锁对象:
1.如果两个线程,针对同一个对象加锁,此时就会出现"锁竞争"(一个线程先拿到了锁,另一个线程阻塞等待)
2.如果两个线程,针对不同对象加锁,此时不会存在锁竞争,各自获取各自的锁即可.
package Threading;
//线程不安全
class Counter{
private int count = 0;
private Object locker = new Object();
public void add(){
synchronized (locker){
count++;
}
}
public int get(){
return count;
}
}
public class ThreadDemo13 {
public static void main(String[] args) throws InterruptedException{
Counter counter = new Counter();
//搞两个线程,两个线程分别对这个counter自增5w次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
//等待两个线程执行结束,然后看结果
t1.join();
t2.join();
System.out.println(counter.get());
}
}
()里的锁对象,可以使任意一个Object对象(基本数据不行),
这两种写法都一样,直接给对方使用synchronized修饰,此时就相当于一this为锁对象.
如果synchronized修饰静态方法(static)此时就不是给this加锁,而是给类对象加锁
手动指定一个锁对象:
如果多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争针对不同对象加锁,就不会有锁竞争.
区别:join加锁
join只是让两个线程完整的进行串行
加锁是让两个线程的某个小部分串行了,大部分都是并发的
3.2 由于线程可见性,引起的线程不安全
所谓的内存可见性,就是多线程环境下,编译器对于代码要求优化,产生了误判,从而引起bug,进一步导致代码的bug.
代码编写完毕:
预期结果:t1通过falg == 0作为条件进行循环,初始情况,将循环t2通过控制台输入一个整数.一旦用户输入了非0的值,此时t1的循环就会立即结束,从而t1线程退出
实际结果:输入非0的值之后t1线程并没有退出,循环没有结束.通过jconsole可以看到t1线程仍然在执行,处在RUNNABLE状态.
3.2.1 处理方式:让编译器针对这个场景暂停优化
使用关键字volatile被volatile修饰的变量,此时编译器就会禁止上述优化,能够保证每次都是从内存重新读取数据.
package Threading;
import java.util.Scanner;
public class ThreadDemo14 {
volatile public static int flag = 0;//解决
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0){
//空着
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
System.out.println("循环结束!t1结束!");
});
Thread t2 = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
加上volatile关键词之后,此时编译器就能够保证每次都是从新从内存读取flag变量的值.此时t2修改falg,t1就可以立即感知到,t1就可以正确退出了.
volatile不保证原子性
volatile适用的场景:一个是线程读,一个线程写的情况.
synchronized:是多个线程写...
volatile的这个效果,称为"保证内存可见性",还有一个效果:禁止指令重排序
"synchronized也能保证内存可见性"存在争议
指令重排序:(也是编译器优化的策略),调整了代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变
3.3 wait和notify
线程的额度是无序的,随机的,但是也是有一定的需求场景,希望线程有序执行
join:算是一种控制顺序的方式.(功效有限)
wait():就是让某个现成先暂停下来等一等(条件不满足或时机不成熟,就先阻塞等待)
notify():就是把该线程唤醒,能够继续执行(其他线程构造了一个成熟的条件,就可以唤醒1号线程)
wait和notify是Object的方法:只要你是个类对象(不是内置类型),都是可以使用wait和notify
IllegalMonitorStateException:不正确的 监视器/显示器(指的是synchronized监视器锁)
非法的锁状态异常:
wait()主要做三件事:1.解锁;2.阻塞等待;3.当收到通知的时候,就唤醒同时尝试重新获取锁
wait()必须写到synchronized代码块里面
加锁的对象必须和wait的对象是同一个
notify也是要放到synchronized中使用
package Threading;
public class ThreadDemo16 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() ->{
try {
System.out.println("wait开始");
synchronized (locker){
locker.wait();
}
System.out.println("wait结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() ->{
synchronized (locker){
System.out.println("notify开始");
locker.notify();
System.out.println("notify结束");
}
});
t2.start();
}
}
t1先执行,执行到了wait就阻塞,t1之后t2开始执行,执行到notify就会通知t1线程唤醒
注意:notify是在synchronized内部,就需要t2释放锁t1才能继续往下走
使用wait阻塞状态会让线程进入WAITING状态,wait也提供了一个带参数的版本,参数指定的是最大等待时间
Object.notifyAll:会把所有进入阻塞状态的线程都唤醒