目录
1.观察线程的所有状态
2、线程转换简图
3.线程安全
3.1不安全状态
3.2安全状态(加锁synchronized)
4.线程不安全原因
4.1线程是抢占式执行的,线程间的调度充满随机性(根本原因)
4.3多个线程对同一个变量进行修改操作
4.3针对变量的操作不是原子行的
4.内存不可见性
5.指令重排序
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
1. NEW: 安排了工作 , 还未开始行动
把Thread 类对象创建好了,但是还没有调用start,如下
public static void main(String[] args) {
Thread t = new Thread(() ->{
});
System.out.println(t.getName());
}
通过 this.getName() 方法获取到指定线程的状态,通过 t 这个对象调用 getStare, 就是获取到了 t 的状态
2. TERMINATED: 工作完成了.
操作系统中的线程执行完毕,销毁了,但是 Thread 对象还在,通过t.getState() 获取到的状态
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
3、RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
线程处于就绪状态,就是在就绪队列中,随时可以被调度到 CPU 上,如果代码中没有进行 sleep,也没有进行其他的可能导致阻塞的操作,代码可能处于 Runnable 状态
4、 TIMED_WAITING: 这几个都表示排队等着其他事情
代码中,调用了sleep 就会进入到 TIMED_WAITING,即在当前的线程一定时间内,处于阻塞状态
5、BLOCKED: 这几个都表示排队等着其他事情
当前线程在等待锁,导致了阻塞。
6、WAITING: 这几个都表示排队等着其他事情
当前线程在等待唤醒,导致了阻塞。
在操作系统中,调度线程的时候是随机的(抢占式执行),所以会导致程序执行出现一些 bug。
如果因为这样的调度随机性引入了bug ,就认为代码是线程不安全的。
如果因为这样的调度随机性没有带来 bug ,就认为代码是线程安全的。
例子:使用两个线程,对同一个整型变量进行自增,每个线程自增 5w
因为两个线程是并发执行,即在下面的例子是两个抢占式执行的,有两个都在同时相加,因为使用的调用的是同一个 increse()方法进行 count ++;所以最终结果不准确
//两个线程对同一个变量进行自增,
//可能两个线程同时自增,而不是一个线程增完再到另一个,所以不准
//线程不安全
class Counter {
public int count;
public void increase() {
count++;
}
}
public class Test14 {
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();
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t2.start();
//必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();//让main线程进入阻塞状态,等t1、t2线程执行完
System.out.println(counter.count);//main线程
}
}
通过对一个线程进行加锁(其他线程处于阻塞),即先执行完第一个线程再到下一个线程(阻塞解除),按顺序执行就不会出现错乱情况。
虽然加锁之后,并发执行程度降低了,但是数据更靠谱了
class Counter {
public int count;
/*public void increase() {
count++;
}*/
//加锁 synchronized 即可解决线程安全
synchronized public void increase() {
count++;
}
}
public class Test14 {
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();
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t2.start();
//必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();//让main线程进入阻塞状态,等t1、t2线程执行完
System.out.println(counter.count);//main线程
}
}
给方法加上 synchronized 关键字,当一个线程进程此方法就会自动加锁,成功后,其他线程进入尝试加锁时就会触发阻塞等待(处于BLOCKED状态),阻塞会一直持续到占用锁的线程释放为止
通过加锁,得出的结果是正确的。
原子性:
线程之间的共享变量存在主内存 (Main Memory).每一个线程都有自己的 " 工作内存 " (Working Memory) .当线程要读取一个共享变量的时候 , 会先把变量从主内存拷贝到工作内存 , 再从工作内存读取数据 .当线程要修改一个共享变量的时候 , 也会先修改工作内存中的副本 , 再同步回主内存 .
因此 t1 就不在从内存读取数据了,而是直接从寄存器里面读取。因此一旦 t1 执行了该操作,t2进行了修改, t1 就感知不到了
内存不可见性代码示例
public class Test15 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0) {
}
System.out.println("循环结束,t 线程退出");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个isQuit 的值:");
isQuit = scanner.nextInt();
System.out.println("main线程执行完毕");
}
}
t 线程一直在读取,感知不到内存的修改,即内存不可见性
(1)可以使用synchronized 关键字,既能保证指令的原子性同时也保证内存可见性
(2)使用 volatile 关键字,volatile和原子性无关,但能保证内存可见性
即内存可见性
指令重排序会影响到线程安全问题,也是编译器优化中的一种操作
编译器对于指令重排序的前提是 " 保持逻辑不发生变化 ". 这一点在单线程环境下比较容易判断 , 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高 , 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价 .