一、创建和运行线程
1. 方法一,直接使用 Thread
@Slf4j(topic = "c.Test1")
public class Demo {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
log.debug("running");
}
};
t.setName("t1");
t.start();
}
}
2. 方法二,使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开(1)Thread 代表线程(2)Runnable 可运行的任务(线程要执行的代码)
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
Runnable r = () -> log.debug("running");
Thread t = new Thread(r, "t2");
t.start();
}
}
3. 原理之 Thread 与 Runnable 的关系
小结 :(1)方法 1 是把线程和任务合并在了一起,方法 2 是把线程和任务分开了(2)用 Runnable 更容易与线程池等高级 API 配合(3)用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
4. 方法三,FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
@Slf4j(topic = "c.Test1")
public class Demo {
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running...");
Thread.sleep(2000);
return 100;
}
});
Thread t1 = new Thread(task, "t1");
t1.start();
}
}
二、查看进程线程的方法
1. windows
任务管理器可以查看进程和线程数,也可以用来杀死进程(1)tasklist: 查看进程(2)taskkill: 杀死进程
2. linux
(1)ps -fe :查看所有进程(2)ps - fT - p <PID> 查看某个进程( PID )的所有线程(3)kill : 杀死进程(4)top 按大写 H 切换是否显示线程(5)top - H - p <PID> 查看某个进程( PID )的所有线程
3. Java
(1)jps : 命令查看所有 Java 进程(2)jstack <PID> : 查看某个 Java 进程( PID )的所有线程状态(3)jconsole : 来查看某个 Java 进程中线程的运行情况(图形界面)
4. jconsole 远程监控配置
需要以如下方式运行你的 java 类java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 - Dcom.sun.management.jmxremote.authenticate=是否认证 java类
三、常见方法
1. start 与 run
(1)直接调用 run 是在主线程中执行了 run,没有启动新的线程
(2)使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
2. sleep 与 yield
2.1 sleep
(1)调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
(2)其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
(3)睡眠结束后的线程未必会立刻得到执行
(4)建议用 TimeUnit 的 sleep 代替 Thread 的sleep 来获得更好的可读性
@Slf4j(topic = "c.Test8") public class Test8 { public static void main(String[] args) throws InterruptedException { Thread.sleep(1000); TimeUnit.SECONDS.sleep(1); TimeUnit.DAYS.sleep(1); } }
2.2 yield
(1)调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其他线程
(2)具体的实现依赖于操作系统的任务调度器
2.3 线程优先级
(1)线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
(2)如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲,优先级几乎没作用。
3. join 方法详解
3.1 为什么需要 join
@Slf4j(topic = "c.Test10") public class Test10 { static int r = 0; public static void main(String[] args) throws InterruptedException { test1(); } private static void test1() throws InterruptedException { log.debug("开始"); Thread t1 = new Thread(() -> { log.debug("开始"); sleep(1); log.debug("结束"); r = 10; },"t1"); t1.start(); t1.join(); log.debug("结果为:{}", r); log.debug("结束"); } }
分析(1)因为主线程和线程 t1 是并行执行的, t1 线程需要 1 秒之后才能算出 r=10(2)而主线程一开始就要打印 r 的结果,所以只能打印出 r=0解决方法(1) 用 join ,加在 t1.start() 之后即可
3.2 等待多个结果
3.3 有时效的 join
等待线程运行结束,最多等待 n 毫秒。
4. interrupt 方法详解
4.1 打断 sleep、wait、join 的线程
@Slf4j
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());//打断标记:false
}
}
4.2 打断正常运行的线程
@Slf4j public class Demo { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(true) { boolean interrupted = Thread.currentThread().isInterrupted(); if(interrupted) { log.debug("被打断了, 退出循环"); break; } } }, "t1"); t1.start(); Thread.sleep(1000); log.debug("interrupt"); t1.interrupt(); } }
打断正常运行的线程, Thread.currentThread().isInterrupted() 会变成 true 。
4.3 模式之两阶段终止
在一个线程 T1 中如何 “ 优雅 ” 终止线程 T2 ?这里的【优雅】指的是给 T2 一个料理后事的机会。
错误思路:
(1)使用线程对象的 stop() 方法停止线程
stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
(2)使用 System.exit(int) 方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止。
@Slf4j(topic = "c.Test3") public class Test { public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); Thread.sleep(3500); tpt.stop(); } } @Slf4j(topic = "c.TwoPhaseTermination") class TwoPhaseTermination{ private Thread monitor; // 启动监控线程 public void start(){ monitor = new Thread(()->{ while (true){ Thread current = Thread.currentThread(); if (current.isInterrupted()){ log.debug("料理后事"); break; } try { Thread.sleep(1000); log.debug("执行监控记录"); } catch (InterruptedException e) { e.printStackTrace(); // 重新设置打断标记 current.interrupt(); } } }); monitor.start(); } // 停止监控线程 public void stop(){ monitor.interrupt(); } }
4.4 打断 park 线程
@Slf4j(topic = "c.Test14") public class Test14 { private static void test4() { Thread t1 = new Thread(() -> { for (int i = 0; i < 5; i++) { log.debug("park..."); LockSupport.park(); log.debug("打断状态:{}", Thread.interrupted()); } }); t1.start(); sleep(1); t1.interrupt(); } private static void test3() throws InterruptedException { Thread t1 = new Thread(() -> { log.debug("park..."); LockSupport.park(); log.debug("unpark..."); log.debug("打断状态:{}", Thread.currentThread().isInterrupted()); }, "t1"); t1.start(); sleep(1); t1.interrupt(); } public static void main(String[] args) throws InterruptedException { test4(); } }
test3:如果打断标记已经是 true, 则 park 会失效
test4:可以使用 Thread.interrupted() 清除打断状态。Thread.interrupted()会重置打断标记为 false
4.5 不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
四、主线程与守护线程
默认情况下, Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j(topic = "c.Test15")
public class Test15 {
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("结束");
}
}
注意(1)垃圾回收器线程就是一种守护线程(2)Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
五、五种状态
这是从 操作系统 层面来描述的(1)【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联(2)【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行(3)【运行状态】指获取了 CPU 时间片运行中的状态1️⃣当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换(4)【阻塞状态】1️⃣如果调用了阻塞 API ,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】2️⃣等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】3️⃣与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们(5)【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六、六种状态
这是从 Java API 层面来描述的根据 Thread.State 枚举,分为六种状态(1)NEW 线程刚被创建,但是还没有调用 start() 方法(2)RUNNABLE 当调用了 start() 方法之后,注意, Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)(3)BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述(4)TERMINATED 当线程代码运行结束