主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
package Daemon; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.demo1") public class demo1 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { break; } } log.debug("结束"); }, "t1"); t1.setDaemon(true); t1.start(); Thread.sleep(1000); log.debug("结束"); } }
输出:
15:08:26 [main] c.demo1 - 结束 Process finished with exit code 0
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
五种状态
这是从 操作系统 层面来描述的
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六种状态
这是从 Java API 层面来描述的
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意, Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
共享模型之管程
共享带来的问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
package gc; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.demo1") public class demo1 { static int counter = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for(int i=0;i<5000;i++){ counter++; } },"t1"); Thread t2 = new Thread(() -> { for(int i=0;i<5000;i++){ counter--; } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } }
输出:
16:03:58 [main] c.demo1 - -1238
问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问 共享资源
- 多个线程读 共享资源 其实也没有问题
- 在多个线程对 共享资源 读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对 共享资源 的多线程读写操作,称这段代码块为 临界区
竞态条件
多个线程在临界区内执行,由于代码的 执行序列不同 而导致结果无法预测,称之为发生了 竞态条件
synchronized 解决方案
使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
package gc; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.demo1") public class demo1 { static int counter = 0; static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for(int i=0;i<5000;i++){ synchronized (lock){ counter++; } } },"t1"); Thread t2 = new Thread(() -> { for(int i=0;i<5000;i++){ synchronized (lock){ counter--; } } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } }
输出:
16:14:15 [main] c.demo1 - 0
改进:由面向过程改为面向对象
package gc; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.demo1") public class demo1 { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { for(int i=0;i<5000;i++){ room.increment(); } },"t1"); Thread t2 = new Thread(() -> { for(int i=0;i<5000;i++){ room.decrement(); } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",room.getCounter()); } } class Room{ private int counter = 0; public void increment(){ synchronized (this){ counter++; } } public void decrement(){ synchronized (this){ counter--; } } public int getCounter(){ synchronized (this){ return counter; } } }
输出:
16:18:22 [main] c.demo1 - 0
方法上的 synchronized
- 加在成员方法上,锁住的是 this
- 加在静态方法上,锁住的是 类名.class