并行与并发的区别
- 并行是多核 CPU 上的多任务处理,多个任务在同一时间真正的同时执行
- 并发是单核 CPU 上的多任务处理,多个任务在同一时间段内交替执行,通过时间片轮转实现交替执行,用于解决 IO 密集型任务的瓶颈
线程的创建方式
Thread 的构造方法,可以在创建线程的时候为线程起名字
- 第一种方法:继承 Thread 类
- 第一步:编写一个类继承 Thread
- 第二步:重写 run 方法
- 第三步:new 线程对象
- 第四步:调用线程对象的 start 方法,启动线程
一定调用的是 start 方法,不是 run 方法。start 方法的作用就是启动一个线程,线程启动完成该方法就结束。
public class MyThread {
public static void main(String[] args) {
NewThread nt = new NewThread();
// 启动 start 方法启动线程,而不是 run 方法
nt.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
class NewThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
对比一下 run 方法与 start 方法的内存图
调用 run 方法的内存图
调用 run 方法并没有启动新线程,代码都是在 main 方法中执行的,内存只有一个主线程的栈,因此必须 run 方法中的代码执行玩,才能执行后续的代码
调用 start 方法的内存图
调用 start 方法会启动一个新线程,内存会分配一个新的栈空间给新线程(分配完成start方法就结束了,main 方法中的代码继续向下执行),新线程的代码在新的栈空间执行,main 方法的代码在 main 方法的栈空间执行,两个线程抢夺 CPU 时间片交替执行。
2. 第二种方法:实现 Runnable 接口
- 第一步:编写一个类实现 Runnable 接口
- 第二步:实现接口中的 run 方法
- 第三步:new 线程对象
使用Thread的带有 Runnable 参数的构造方法创建对象
- 第四步:调用线程对象的 start 方法,启动线程
推荐使用这种而不是第一种,因为第一种方式使用继承,而Java只能单继承,因此失去了继承其他类的能力,而 实现 Runnable 接口则还能继承其他类
- 实现 Callable 接口
- 第一步:编写一个类实现 Callable 接口
- 第二步:实现接口中的 run 方法
- 第三步:new 线程对象
- 第四步:调用线程对象的 start 方法,启动线程
线程常用的三个方法
final String getName()
:返回此线程的名称。final void setName(String name)
:将此线程的名称更改为等于参数 name 。static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
线程七个生命周期
- NEW:新建状态
当线程被创建但尚未开始运行时,它处于新建状态。在这个阶段,线程对象被实例化,但尚未调用 start() 方法。 - RUNNABLE:可运行状态
- 就绪状态
一旦调用了 start() 方法,线程进入就绪状态。在这个状态下,线程准备好运行,并等待线程调度程序的分配。此状态并不一定代表线程正在运行,可能会处于等待获取CPU时间片的状态。 - 运行状态
线程获得CPU资源并开始执行其任务时,线程进入运行状态。在这个状态下,线程执行其代码。
- 就绪状态
- BLOCKED:阻塞状态
当线程尝试获取一个已经被其他线程持有的锁时,它会进入阻塞状态。在这个状态下,线程无法继续执行,直到它获得所需的锁。 - WAITING:等待状态
如果线程调用 wait()、join() 或 LockSupport.park() 方法,它会进入等待状态。在这种状态下,线程会等待其他线程通知或唤醒它。 - TIMED_WAITING:超时等待状态
线程在等待的同时设置了时间限制(例如,调用 sleep(milliseconds) 或 wait(milliseconds)),将进入超时等待状态。如果在超时时间到达之前线程未被唤醒,则该线程会返回到就绪状态。 - TERMINATED:终止/死亡状态
当线程的 run() 方法执行完毕或者因异常终止时,线程进入终止状态。在这个状态下,线程完成了它的生命周期,无法重新启动。
线程常用的调度方法
- start()
用于启动线程,使其进入就绪状态。 - sleep(long millis)
使当前正在执行的线程暂停指定的时间,被调用的线程会进入阻塞状态
,在指定的毫秒数后,线程会回到就绪状态。 - yield()
暂时让出当前线程的执行权,该方法提示线程调度器允许其他同等优先级的线程获得执行时间。
并不保证在调用后立即释放控制权。
让位后的线程进入就绪状态,并不会阻塞。 - join()
等待一个线程完成,如果线程 A 调用线程 B 的 join() 方法,线程 A 会阻塞,直到线程 B 执行完毕并终止。
join(long millis) 方法也可以指定时间,指的是加入 A 线程的时间或者说阻塞 A 线程的时间,时间一到就退出。如果在指定的 millis 时间内,B 线程结束了,被阻塞的 A 线程也会结束阻塞状态。 - interrupt()
发送一个中断信号给线程。如果线程正处于等待、睡眠或阻塞状态,将会抛出InterruptedException。抛出异常就会导致线程退出等待、睡眠或阻塞状态,从而达到中断的目的,利用了异常的机制。 - setPriority(int newPriority)
设置线程的优先级。线程的优先级是一个整数值,范围从1(最低)到10(最高)。这并不保证线程会按优先级执行,但可以指示调度器的优先级。 - wait() 和 notify()
用于线程间的通信和协作。wait() 使线程在对象监视器上等待,直到其他线程调用 notify() 或 notifyAll() 来唤醒它。
如何强制结束一个线程
Thread.stop() 方法可以强制结束线程,但是在 Java1.2 之后就被弃用了,因为使用 stop() 方法会导致线程立即终止,这可能导致锁未解锁、文件未正确关闭等情况,因此强烈不推荐。更好的办法是使用标志位或者 interrupt() 方法。
- 设置标志位方法
通过使用一个共享的标志位来控制线程的生命周期是推荐的做法。主线程可以通过设置标志位来通知子线程应停止执行。class CustomThread extends Thread { private volatile boolean running = true; // 使用 volatile 关键字确保可见性 public void run() { while (running) { // 执行任务 System.out.println("Thread is running..."); try { Thread.sleep(500); // 模拟工作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 } } System.out.println("Thread is stopping..."); } public void stopRunning() { running = false; // 设置标志位 } } public class Main { public static void main(String[] args) throws InterruptedException { CustomThread thread = new CustomThread(); thread.start(); Thread.sleep(2000); // 让线程运行2秒 thread.stopRunning(); // 请求线程停止 thread.join(); // 等待线程结束 System.out.println("Main thread finished."); } }
- 使用 interrupt() 方法
在Java中,interrupt() 方法可以用于中断一个线程。线程在被中断时可以选择捕捉异常或者检查中断状态,从而优雅地结束自己。class InterruptibleThread extends Thread { public void run() { try { while (!Thread.currentThread().isInterrupted()) { // 执行任务 System.out.println("Thread is running..."); Thread.sleep(500); // 模拟工作 } } catch (InterruptedException e) { // 捕捉到中断异常,线程可以选择停止 System.out.println("Thread was interrupted."); Thread.currentThread().interrupt(); // 重新设置中断状态 } System.out.println("Thread is stopping..."); } } public class Main { public static void main(String[] args) throws InterruptedException { InterruptibleThread thread = new InterruptibleThread(); thread.start(); Thread.sleep(2000); // 让线程运行2秒 thread.interrupt(); // 中断线程 thread.join(); // 等待线程结束 System.out.println("Main thread finished."); } }
守护线程
在 Java 中,线程被分为两大类,一类是用户线程,一类是守护线程。
在 JVM 中,有一个隐藏的守护线程就是 GC 线程
守护线程的特点:
- 后台运行: 守护线程通常是后台执行的,用于执行一些辅助任务,比如垃圾回收、线程池中的工作线程等。
- 生命周期受限: JVM 会在所有非守护线程结束后自动结束守护线程。如果没有非守护线程在运行,JVM会退出。
- 优先级: 守护线程的优先级与普通线程相同,但它们的作用通常是协助非守护线程。
如何创建守护线程
三个步骤创建守护线程:
- 创建一个线程实例
- 调用
setDaemon(true)
方法,将该线程设置为守护线程 - 启动线程
class DaemonThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("Daemon thread is running...");
try {
Thread.sleep(1000); // 模拟一些工作
} catch (InterruptedException e) {
System.out.println("Daemon thread interrupted.");
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread daemonThread = new DaemonThread();
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
try {
Thread.sleep(3000); // 主线程睡眠3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is ending...");
// 主线程结束,Daemon线程会随之结束
}
}
定时任务
在Java中,Timer 是一个用于调度任务的工具类,允许开发者在指定的时间间隔内重复执行任务或在特定的时间点执行任务。Timer 类通常与 TimerTask 类一起使用,其功能足够简单,适合于许多基本的定时任务需求。
Timer: 一个定时器,负责调度任务。
TimerTask: 一个抽象类,所有需要被调度的任务都需要继承这个类并重写 run() 方法。
创建定时任务
使用 Timer 和 TimerTask 来创建定时任务的基本步骤如下:
- 创建一个 Timer 实例。
- 创建一个继承自 TimerTask 的类,并实现 run() 方法。
- 使用 Timer 的 schedule() 或 scheduleAtFixedRate() 方法将任务和执行时间关联。
常用方法
- schedule(TimerTask task, long delay): 在指定的延迟后调度任务。
- schedule(TimerTask task, Date time): 在指定的时间执行任务。
- schedule(TimerTask task, long delay, long period): 设定任务在指定的延迟后每隔一个时间段再次执行。
- scheduleAtFixedRate(TimerTask task, long delay, long period): 类似于 schedule,但是以固定率运行,适用于需要固定间隔执行的任务。
import java.util.Timer;
import java.util.TimerTask;
public class TimerExample {
public static void main(String[] args) {
Timer timer = new Timer(); // 创建定时器
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("Task executed at: " + System.currentTimeMillis());
}
};
// 在延迟 1 秒后执行任务,每隔 2 秒重复执行
timer.scheduleAtFixedRate(task, 1000, 2000);
// 主线程睡眠 10 秒,以便观察输出
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 取消定时器
timer.cancel();
System.out.println("Timer canceled.");
}
}
注意事项
- 单线程中: Timer 是单线程的,如果一个 TimerTask 执行时间超过下一个任务的调度时间,后续的任务会被延迟执行。
- 异常处理: 如果 TimerTask 中的代码抛出未处理的异常,Timer 将停止执行所有后续任务。应确保 run() 方法内的代码是异常安全的。
- 使用 ScheduledExecutorService: 对于更复杂的定时任务需求(例如线程池,多线程调度等),建议使用 ScheduledExecutorService,它提供了更强大的功能和灵活性。
import java.util.concurrent.*; public class ScheduledExecutorExample { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Runnable task = () -> { System.out.println("Task executed at: " + System.currentTimeMillis()); }; // 在延迟 1 秒后执行,每隔 2 秒重复执行 scheduler.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS); // 主线程睡眠 10 秒,以便观察输出 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } // 关闭调度器 scheduler.shutdown(); System.out.println("Scheduler shut down."); } }
- 开发中一般不使用 Timer ,有一些更好的框架可以设置定时任务。
线程优先级
- 线程是可以设置优先级的,优先级高的,获得CPU时间片的概率会高一些
- JVM 采用的是抢占式调度模式。谁的优先级高,获取 CPU 的概率就会高
- 默认情况下,一个线程的优先级是 5
- 线程优先级最低是 1,最高是 10
- Thread 类的字段属性
可以通过 MAX_PRIORITY 和 MIN_PRIORITY,设置最高最低优先级
线程安全
什么情况下需要考虑线程安全问题?
- 多线程的并发环境下
- 有共享的数据
- 共享数据涉及到修改操作
一般情况下,局部变量不存在线程安全问题,实例变量和静态变量可能存在线程安全问题。因为局部遍历存储在栈中,实例变量和静态变量存储在堆中,栈每个线程使用自己的,而堆是多线程共享的。