文章目录[隐藏]
线程
1. 创建线程
1.1 继承 Thread 类
步骤
- 定义一个线程类继承 Thread 类
- 重写 run 方法,里面是定义线程以后要干啥
- new 一个新线程对象
- 调用 start 方法启动线程(执行的还是run方法)
/**
* 方法一、通过继承Thread
*
* @author Enndfp
*/
public class ThreadDemo {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
// 1、定义一个线程类继承Thread类
class MyThread extends Thread {
// 2、重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
优缺点
- 优点:编码简单
- 缺点:
- 线程类已经继承了
Thread
类无法继承其他类了,功能不能通过继承拓展(单继承的局限性) - 每个继承
Thread
类的对象都是一个独立的线程对象,占用一定的系统资源
- 线程类已经继承了
1.2 实现 Runnable 接口
步骤
- 定义一个线程任务类,实现 Runnable 接口
- 重写 run 方法,定义线程的执行任务
- 创建一个任务对象
- 把任务对象交给Thread处理
- 启动线程
/**
* 方法二、通过实现Runnable接口
*
* @author Enndfp
*/
public class RunnableDemo {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// 5、启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
// 1、定义一个线程任务类,实现Runnable接口
class MyRunnable implements Runnable {
// 2、重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
Thread 类本身也是实现了 Runnable 接口,Thread 类中持有 Runnable 的属性,执行线程 run 方法底层是调用 Runnable 的 run:
public class Thread implements Runnable {
private Runnable target;
public void run() {
if (target != null) {
// 底层调用的是 Runnable 的 run 方法
target.run();
}
}
}
优缺点
- 缺点:代码复杂一点
- 优点:
- 线程任务类只是实现了
Runnable
接口,可以继续继承其他类,避免了单继承的局限性 - 多个线程可以共享同一个
Runnable
对象,减少了资源占用 - 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
- 线程任务类只是实现了
1.3 实现 Callable 接口
步骤
- 定义一个任务类,实现 Callable 接口(应该声明线程任务执行完毕后结果的数据类型)
- 重写 call 方法(任务方法)
- 创建 Callable 任务对象
- 把 Callable 任务对象 交给 FutureTask 对象
- 交给线程处理
- 启动线程
补充
FutureTask对象的作用1: 是 Runnable 的对象(实现了 Runnable 接口),可以交给 Thread 了
FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其 get 方法得到线程执行完成的结果
/**
* 方法三、通过实现Callable接口
*
* @author Enndfp
*/
public class CallableDemo {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable();
// 4、把Callable任务对象 交给 FutureTask 对象
FutureTask<String> task = new FutureTask<>(call);
// 5、交给线程处理
Thread t = new Thread(task);
// 6、启动线程
t.start();
try {
// 如果task任务没有执行完毕,这里的代码会等待,直到线程跑完才提取结果
String result = task.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 1、定义一个任务类,实现Callable接口
class MyCallable implements Callable {
// 2、重写call方法(任务方法)
@Override
public String call() throws Exception {
return "子线程执行的结果是:Hello World!";
}
}
优缺点
- 优点:同
Runnable
,并且可以得到线程执行的结果 - 缺点:编码复杂
2. 查看进程和线程的方法
2.1 Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist
查看进程taskkill
杀死进程
2.2 Linux
ps -ef
查看所有进程top
定位哪个进程对 CPU 占用过多ps H -eo pid,tid,%cpu | grep PID
定位是哪个线程引起的 CPU 占用过高jstack PID
根据进程 id 找到有问题的线程kill
杀死线程
2.3 Java
jps
查看所有Java进程jstack
查看某个 Java 进程(PID)的所有线程状态jconsole
来查看某个 Java 进程中线程的运行情况(图形界面)
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类
- 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
如果要认证访问,还需要做如下步骤
- 复制 jmxremote.password 文件
- 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
- 连接时填入 controlRole(用户名),R&D(密码)
3. 线程常见 API
方法 | 说明 |
---|---|
public void start() | 启动一个新线程,在新的线程运行 run 方法中的代码 |
public void run() | 线程启动后调用该方法 |
public void setName(String name) | 给当前线程取名字 |
public void getName() | 获取当前线程的名字 线程存在默认名称:子线程是 Thread-索引,主线程是 main |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争 |
public static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用 |
public final int getPriority() | 返回此线程的优先级 |
public final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10 |
public void interrupt() | 中断这个线程,异常处理机制 |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
public final void join() | 等待这个线程结束 |
public final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
3.1 start 与 run
start() 方法:
start()
是Thread
类的方法,用于启动一个新的线程。当调用start()
方法时,会创建一个新的线程,并且在新线程中调用run()
方法- 在新线程中执行
run()
方法,不会阻塞主线程的继续执行。这使得程序可以同时执行多个任务 start()
方法的调用后,会导致系统自动安排新线程的执行。线程调度器决定了各个线程的执行顺序和时间片分配
run() 方法:
run()
方法是Thread
类中用于定义线程任务的方法。当直接调用run()
方法时,它会在当前线程(通常是主线程)中执行,而不会创建新的线程- 如果你调用了
run()
方法而不是start()
方法,那么run()
方法会在当前线程中运行,会阻塞主线程,直到run()
方法执行完毕才会继续执行主线程 run()
方法通常被重写,用于定义线程的实际逻辑,例如,在一个多线程应用程序中,不同线程可能会执行不同的任务
补充
start() 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start() 方法只能调用一次,如果调用了多次会出现
IllegalThreadStateException
当使用
start()
方法启动线程时,不会直接传递给调用start()
的地方,线程的异常处理需要使用特定的机制(Thread.UncaughtExceptionHandler
)来捕获未捕获的异常在使用
run()
方法执行线程任务时,异常会传播到调用方,可以使用常规的 try-catch 块来捕获和处理异常
3.2 sleep 与 yield
sleep () 方法:
- 调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞) - 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出
InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- sleep() 方法的过程中,线程不会释放对象锁
yield() 方法:
- 调用 yield 会让当前线程从
Running
进入Runnable
就绪状态,然后调度执行其它线程 - 具体的实现依赖于操作系统的任务调度器
- 会放弃 CPU 资源,锁资源不会释放
补充
sleep()
方法会导致调用线程进入阻塞状态,暂停执行指定的时间。在此期间,调用线程不会占用 CPU 资源,但其他线程仍然可以继续执行
yield()
方法通常用于鼓励公平竞争和避免某些线程长时间占用 CPU 而导致其他线程无法执行,但并不保证其他线程一定会执行
3.3 join
join () 方法:
join()
方法是一个阻塞方法,会阻塞当前线程的执行,直到被调用的线程执行完成。如果被调用的线程在执行过程中发生异常被中断,也会抛出InterruptedException
异常
3.4 interrupt
interrupt() 方法:
-
interrupt()
方法用于中断一个正在运行的线程,即向目标线程发送一个中断信号。这个信号会被目标线程接收到,并可以在适当的时机中断线程的执行。但是需要注意,interrupt()
方法并不会强制终止线程,而是提供一种协作机制,线程可以检查自己是否被中断,然后自行决定是否终止 -
打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
-
sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
System.out.println(" 打断状态: " + t1.isInterrupted());// 打断状态: false
}
- 打断正常运行的线程:不会清空打断状态(true)
public static void main(String[] args) throws Exception {
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
System.out.println(" 打断状态: " + interrupted);//打断状态: true
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
3.5 park
park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted()); //打断状态:true
}, "t1");
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
如果打断标记已经是 true, 则 park 会失效
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted()); //打断状态:true
LockSupport.park(); //失效,不会阻塞
log.debug("unpark..."); //和上一个unpark同时执行
}, "t1");
可以修改获取打断状态方法,使用 Thread.interrupted()
,清除打断标记
3.6 daemon
public final void setDaemon(boolean on)
:如果是 true ,将此线程标记为守护线程
线程启动前调用此方法:
Thread t = new Thread() {
@Override
public void run() {
log.debug("running...");
}
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();
用户线程:平常创建的普通线程
守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示
说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去
常见的守护线程:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
3.7 过时
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
-
public final void stop()
:停止线程运行废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面
-
public final void suspend()
:挂起(暂停)线程运行废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁
-
public final void resume()
:恢复线程运行
4. 线程状态
从操作系统层面:初始状态、可运行状态、运行状态、阻塞状态、终止状态
线程由生到死的完整过程(生命周期):当线程被创建并启动以后,并不是一启动就进入了运行状态,也不是一直处于运行状态,在 Java API 中 java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 |
Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) |
Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 |
Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
Teminated(终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |
- NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE
- RUNNABLE <--> WAITING:
- 调用 obj.wait() 方法时
- 调用 obj.notify()、obj.notifyAll()、t.interrupt():
- 竞争锁成功,t 线程从 WAITING → RUNNABLE
- 竞争锁失败,t 线程从 WAITING → BLOCKED
- 当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待
- 当前线程调用 LockSupport.park() 方法
- RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)
- RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败