JavaEE——多线程的状态及线程安全问题
目录
一、线程的状态
1、NEW
2、 TERMINATED
3、RUNNABLE
4、TIMED_WAITING
5、 BLOCKED
6、WAITING
二、线程安全问题
1、线程不安全的原因
2、一个线程不安全的实例
3、加锁操作
4、产生线程不安全的原因
什么是内存可见性呢?
解决方案?
5、指令重排序
NEW: 安排了工作 , 还未开始行动 . 新创建的线程 , 还没有调用 start 方法时处在这个状态 .RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作 . 调用 start 方法之后 , 并正在CPU 上运行 / 在即将准备运行 的状态 .BLOCKED: 使用 synchronized 的时候 , 如果锁被其他线程占用 , 就会阻塞等待 , 从而进入该状态 .WAITING: 调用 wait 方法会进入该状态 .TIMED_WAITING: 调用 sleep 方法或者 wait( 超时时间 ) 会进入该状态 .TERMINATED: 工作完成了 . 当线程 run 方法执行完毕后 , 会处于这个状态 .
一、线程的状态
1、NEW
new表示安排了工作还未开始行动。
看代码:while()循环里面啥也不用给,我们直接通过t.getState()这个方法获取指定线程的状态
public class demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
}
});
System.out.println(t.getState());//通过这个方法获取指定线程的状态
t.start();
}
}
2、 TERMINATED
TERMINATED:表示工作完成了。
代码:这里我们直接去掉while()循环,啥也不用干,那肯定代码是执行完了,先调用start方法,然后获取我们的线程状态。
public class demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
3、RUNNABLE
RUNNABLE:表示就绪状态。处于这个状态的线程,就是在就绪队列中,随叫随到,随时可以到CPU上执行。
代码:直接给一个空的while循环,不用加sleep等操作,方便我们观察代码的就绪状态。
public class demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
4、TIMED_WAITING
即代码中调用了sleep就会进入到TIMED_WAITING,意思就是当前线程在一定时间内是阻塞状态。
public class demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
5、 BLOCKED
表示排队等待其他事情。详情参考本文下面的3、加锁操作。
6、WAITING
也表示排队等待其他事情。
OK,到这里就可以总结出线程状态转换的简易图。
二、线程安全问题
谈到线程安全,那么这里是很重要的一个板块,在面试中如果遇到多线程的问题,基本都会涉及到线程安全问题,同时也是一个难点,希望大家认真阅读。
1、线程不安全的原因
操作系统调度线程的时候是随机的,线程之间是抢占式执行,正因为这种特性很可能会导致程序出现一些bug。
2、一个线程不安全的实例
这里我们使用两个线程,对同一个整型变量各自-自增5w次,看最终的结果。
class Counter{
public int count = 0;
public void increase(){
count++;
}
public class demo15 {
private 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();
//必须要在t1,t2都执行完了之后,再打印count
//否则,t1,t2,main都是并发执行的关系,导致t1,t2还没执行完,就先执行了下面的打印操作
//在main中打印两个线程执行自增完成之后,得到的count结果
System.out.println(counter.count);
}
}
注意这里我们需要等待线程t1,t2都结束之后再打印结果,因为main线程和t1,t2线程是并发执行的关系,所以使用到了join函数
我们发现结果并不是10w,而是一个介于5w - 10w之间的数字,那么为什么会这样呢?
要想知道其中原因,我们就要来了解一个count++到低是如何实现的。
站在CPU的角度:count++实际上是三个CPU指令。
1、把内存中的count的值,加载到CPU寄存器中。(load)
2、把寄存器中的值+1。(add)
3、把寄存器的值写回到内存的count中。(save)
由于线程调度是抢占式执行的,并且这个count++操作是分为3步来完成的,那么t1,t2同时执行这三个操作的时候,顺序上是有很多种可能的,这就可能导致我们最终的结果达不到10w。
结果为什么在5w - 10w之间?
这5w并发相加中,有时候可能是串行的(+2),有时候可能是交错的,串行与交错多少次咋们不知道,这都是随机的,极端情况下,所有操作都是串行的,结果就是10w,所有操作都是交错的,结果就是5w,不过都是小概率事件。
3、加锁操作
那么如何解决上述线程不安全问题呢?
答案:加锁!
这里举个例子:比如李华准备去ATM机里面取钱,那么ATM机器一般是在一个小房间里面,当李华走进去的时候他就可以给这个房间的门上锁,这样赵四,王五等等想取钱的话只能等李华取完(这个等待操作就是阻塞),李华取完钱后便可以给这个房间解锁,下一个人才能进去取钱。
如何加锁?
那么java中加锁的方式有很多种,最常使用的是 synchronized 关键字。我们可以给上述代码重大的自增函数increase()前面加上synchronized 关键字就是加锁成功了。
public int count = 0;
synchronized public void increase(){
count++;
}
我们在看运行结果,这下便是10w,符合我们的预期。
那么给方法加上 synchronized 关键字之后,此时进入方法就会自动加锁,离开方法就会自动锁,当一个线程加锁成功的时候,其他线程尝试加锁就会触发阻塞等待。这个时候线程的状态就称为BLOCKED。
4、产生线程不安全的原因
1、线程是抢占式执行的,线程间的调度充满随机性。(线程不安全的根本原因)
2、多个线程对同一个变量进行修改操作。
3、针对变量的操作不是原子的,通过加锁操作就是把几个指令打包成一个原子的。
4、内存可见性。
什么是内存可见性呢?
假设我们这里有两个线程t1,t2;t1只进行读操作,t2在合适的时候会进行修改操作。
t1 这个线程在循环读这个变量。那么读取内存操作,相比于读取寄存器,是一个非常低效的操作作。 (慢 3-4 个数量级)
因此在 t1 中频繁的读取这里内存的值就会非常低效。
而且如果 t2 线程迟迟不修改,t1 线程读到的值又始终是一样的值!!
因此,t1 可能会 直接从寄存器里读(即不执行 load )一旦 t1 做出了这种大胆的假设,此时万一 t2 修改了 count 值, t1 就不能感知到了。
import java.util.Scanner;
public class demo16 {
public static int isquit = 0;
public static void main(String[] args) {
Thread t = new Thread(()-> {
while (isquit == 0){
}
System.out.println("循环结束,t线程退出!");
});
t.start();
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个isquit的值:");
isquit = sc.nextInt();
System.out.println("main线程执行完毕!");
}
}
那么这个案例就很好的说明了上述内存可见性的问题,我们手动输入isquit的值为2,那么while循环结束按理说应该打印 System.out.println("循环结束,t线程退出!");正是因为t线程一直反复读isquit的值,于是它直接从寄存器读,那么我们修改了isquit的值,它也就感知不到了。所以就没有打印 循环结束,t线程退出! 这条语句。
解决方案?
1、使用synchronized 关键字。synchronized 不光能够保证原子性,同时也能够保证内存可见性。被synchronized 包裹起来的代码,编译器就不敢轻易做出上述假设,就相当于手动禁止了编译器的优化。
2、使用volatile关键字。volatile和原子性无关,但是能够保证内存可见性。使得编译器每次都重新从内存中读取isquit的值。
public static volatile int isquit = 0;
5、指令重排序
这个操作也是编译器优化的一种操作,编译器会智能的调整代码的前后顺序从而提高程序的效率。
保证逻辑不变的前提,再去调整顺序。使用synchronized 也能够禁止指令从排序!!