目录
1.线程的状态
2.线程不安全的原因
2.1:原子性
2.2: 可见性
2.3:有序性
3.解决线程不安全问题
3.1:synchronized
3.1.1:互斥
3.1.2:可重入
3.2:volatile关键字
3.3:wait和notify
3.3.1:wait()方法
3.3.2:notify()
3.3.3notifyAll()方法
4.wait()和sleep()方法的对比(面试题)
前言:
我们如果要了解线程安全的话,首先要明白线程的状态,正如常言所说:知己知彼,方能,百战不殆。
1.线程的状态
1.new:创建了对象但未被调用start(),内核里还没有创建PCB。就好像安排了工作,还未开始行动。
2.RUNNABLE:可以工作的,又可以分成正在工作中(正在cpu上执行)和即将开始工作的。
3.TERMINATED:工作完成了,pcb执行完毕,但Thread对象还在。
我们常说阻塞状态,接下来,我们就要细说这个阻塞状态。
4.BLOCKED:排队等着其他事情的完成 ( 这是因为加锁的原因造成的。)
5.WAITING:排队等着其他事情的完成.(这是因为 wait ,join造成的)。
6.TIME_WAITING:排队等着其他事情的完成.(这是因为 sleep造成的)
2.线程不安全的原因
既然要谈到线程不安全的原因,我们首先要明白什么是线程安全,如果代码在多线程环境下运行的结果是符合我们预期的,即使在单线程环境应该的结果,则说这个程序是线程安全的。
2.1:原子性
原子性是指一个操作是不可中断的,那么全部执行成功,那么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程中断。
class count{
public int num=0;
public void add () {
num++;
}
}
public class ThreadDemo1 {
//原子性
public static void main(String[] args) {
count count=new count();
Thread t=new Thread(()->{
for (int i = 0; i <1000 ; i++) {
count.add();
}
});
Thread t1=new Thread(()->{
for (int i = 0; i <1000 ; i++) {
count.add();
}
});
t.start();
t1.start();
try {
t.join();
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(count.num);
}
}
看上面的代码,你会下意识认为count最后的值2000吗?这不是的哟,这是一个线程不安全的代码。我出来的结果是1515但每一次的结果都不一样。这是为啥勒?
2.2: 可见性
一个线程修改了公共变量的值,其他线程使用这个公共变量的时候,能够立即得到这个通知并修改了这个变量。
class Counter{
public int flag=0;
}
public class ThreadDemo2 {
public static int num = 0;
public static void main(String[] args) {
Counter counter=new Counter();
Thread t = new Thread(() -> {
while(counter.flag==0){
}
System.out.println("循环结束");
});
Thread t2 = new Thread(() -> {
Scanner scan=new Scanner(System.in);
System.out.println("请输入一个数字来修改flag值");
counter.flag=scan.nextInt();
});
t.start();
t2.start();
}
}
当t2线程修改了flag的值,首先是在自己的CPU内修改,之后再会修改内存中flag的值。之后t线程会因为编译器优化,认为flag的值不会轻易改变的,所以每次都看t线程的CPU中的flag值是否会发生变化,而不是比较内存中flag的值。编译器这种优化是为了提高速度。
2.3:有序性
有序性:代码重排序的本质就是编译器的优化。在单线程中,就是有可能发生顺序调整,但是多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码执行效果进行预测。因此激进的重排序很容易导致优化后的逻辑和之前不一样。
3.解决线程不安全问题
3.1:synchronized
为了解决原子性问题,提出了synchronized(加锁)
3.1.1:互斥
synchronized会起到互斥效果,莫个线程执行到莫个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。
synchronized如果修饰的普通方法,锁对象就调用者本身。
如果修饰静态方法,锁对象就是这个类对象。
如果修饰的代码块,就是自己收到设置的锁对象。
进入synchronized修饰的代码块,相当于加锁。
退出synchronized修饰的代码块,相当于解锁。
sychrnoized是不需要手动解锁的。
3.1.2:可重入
这个意思就是说一个线程对莫个对象或者方法加锁了,但还没有解锁,又一次进行加锁,不会死锁。
可以举一个例子,就是你给一个女生表白了,之后你们在一起了,但你再表白一次,不会有啥不好是的。
3.2:volatile关键字
volatile能保证内存可见性。
代码在写入volatile修饰的变量的时候:
会改变线程内存中volatile变量副本的值,将改变后的副本的值从该线程的cpu刷新到内存中。
代码在读入volatile修饰的变量的时候:
从内存中读取volatile变量的最新值到该线程的cpu中,从cpu中读取volatile变量的副本。
这个代码就是解决上面可见性代码。
3.3:wait和notify
这个是解决代码的有序性。
3.3.1:wait()方法
1.使当前执行代码进行等待(把线程放到等待队列中)
2.释放当前的锁,进行阻塞等待。
3.满足一定条件时被唤醒,重新尝试获取锁
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Thread t=new Thread(()->{
synchronized (lock){
System.out.println("我这个部分已经干完了");
System.out.println("我只等你一个小时");
try {
lock.wait(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(1000);//为了让t 线程先运行
System.out.println("别急,我正在努力赶");
}
}
wait(参数)是毫秒,wait等待时间超时的时候,它会自动结束等待条件。
还有一种让它就是结束等待,就是有其他线程调用该对象的notify()方法。
3.3.2:notify()
随机唤醒一个处于阻塞队列的线程(是使用同一个锁对象的线程)。
在notify()方法后,当前线程不会马上释放该对象的锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Thread t1=new Thread(()->{
synchronized (lock) {
System.out.println("你下班了吗?你下班了,我就来上班");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("那我来上班了");
}
});
Thread t2=new Thread(()->{
synchronized (lock){
System.out.println("我下班了");
lock.notify();
}
});
t1.start();
Thread.sleep(200);
t2.start();
}
这里为啥要t1线程启动之后,等待200毫秒,再启动线程t2了。接下来到了故事小课堂。
wait()方法就相当于一个坐校车上学的学生。notify()方法就相当于校车。如果notify()方法先启动,wait()方法后启动,就相当于校车来了,发现这个没有学生到就走了。但等到学生到的时候,就木有校车来接她。她就会一直在那里等校车来。(这就相当于这个线程一直处于阻塞队列,一直等别人来唤醒。)这样会出现线程不安全。
3.3.3notifyAll()方法
使用notifyAll()方法可以一次唤醒所有等待的线程(使用同一个锁对象的线程)。
4.wait()和sleep()方法的对比(面试题)
1.wait是object的方法,sleep是Thread的静态方法。
2.调用sleep()方法的线程不会释放对象锁,而调用wait()方法会释放对象锁。
3.使用wait()不会抛出异常,但需要搭配synchronized来使用。而sleep()会抛出异常 InterruptedException。
总结:
以上就是我总结的线程不安全的原因以及解决办法,若有错误的地方,希望各位铁子留言纠错,若感觉不错,请一键三连。