30天学Java第九天——线程
并行与并发的区别
- 并行是多核 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{@Overridepublic 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 接口与 Runnable 类似,但它可以返回结果,并且可以抛出异常。
- 第一步:编写一个类实现 Callable 接口
- 第二步:实现接口中的 call方法
- 第三步:通过 Future 或 FutureTask 接收 Callable 的实现类,然后将 Future 或 FutureTask 对象作为参数创建 Thread 对象得到线程对象
- 第四步:调用线程对象的 start 方法,启动线程
- 线程池方法获取线程
Java 提供了 Executor 框架来简化线程的创建和管理。
使用线程池创建线程的主要步骤如下:
- 创建线程池: 通过 Executors 工具类创建不同类型的线程池,如固定大小的线程池(newFixedThreadPool)、可缓存的线程池(newCachedThreadPool)等。
- 提交任务: 通过 submit() 方法将实现了 Runnable 或 Callable 接口的任务提交给线程池。
- 关闭线程池: 在所有任务都完成后通过 shutdown() 方法关闭线程池,防止泄漏。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; public class ThreadPoolExample { public static void main(String[] args) { // 创建一个固定大小的线程池 ExecutorService executorService = Executors.newFixedThreadPool(3); // 提交多个任务 for (int i = 0; i < 5; i++) { executorService.submit(() -> { try { Thread.sleep(1000); // 模拟任务执行 System.out.println(Thread.currentThread().getName() + " is executing a task."); } catch (InterruptedException e) { e.printStackTrace(); } }); } executorService.shutdown(); // 关闭线程池 try { // 等待所有任务完成,或超时 if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { executorService.shutdownNow(); // 如果超时,则强制关闭 } } catch (InterruptedException e) { executorService.shutdownNow(); // 线程池被中断时也关闭 } System.out.println("All tasks completed."); }
}
线程常用的三个方法
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,设置最高最低优先级
线程安全
什么情况下需要考虑线程安全问题?
- 多线程的并发环境下
- 有共享的数据
- 共享数据涉及到修改操作
一般情况下,局部变量不存在线程安全问题,实例变量和静态变量可能存在线程安全问题。因为局部遍历存储在栈中,实例变量和静态变量存储在堆中,栈每个线程使用自己的,而堆是多线程共享的。
同步互斥
- 语法格式
- 局部同步代码块
确定共享对象很重要,找错了可能会无故扩大共享范围,导致效率变低。synchronized(必须是需要排队的几个线程的共享对象){// 需要同步的代码 }
- 在实例方法上添加 synchronized 关键字
如果对象锁恰好是 this ,并且整个方法都需要同步,可以在实例方法上添加 synchronized 关键字,更简洁。 - 在静态方法上添加 synchronized 关键字
在静态方法上添加 synchronized 关键字之后,线程会占有类锁
,类锁只有一把。
-
同步代码块执行原理
假设 obj 对象是 t1,t2 两个线程共享的,假设 t1 先抢到了 CPU 时间片开始执行同步代码块,t1 需要先拿到 obj 的对象锁,有了锁才有权力进入同步代码块,同步代码块执行完毕就会释放对象锁。若 t2 抢到了 CPU 时间片,若是 t1 的对象锁还没有释放,那么 t2 只能在同步代码块之外等待。 -
死锁
两个线程互相拥有对方需要的锁,但是又都不释放自身的锁,导致一直等待,形成死锁。
使用 wait() 和 notify() 方法实现线程通信
wait(), notify(), 和 notifyAll() 是对象监视器(monitor)中的重要方法,它们提供了一种在多个线程之间进行通信的机制。这几个方法都是 Object 类的方法。
- wait(): 当一个线程调用对象的 wait() 方法时,它将
释放该对象的锁
并进入无期限等待状态
,直到其他线程调用 notify() 或 notifyAll() 唤醒它。必须在同步块中使用该方法。
无参数的 wait() 方法会让线程进入无限期的等待状态,有参数的 wait() 方法会让线程进入超时等待状态。 - notify(): 唤醒在该对象监视器上等待的一个优先级较高的线程,如果优先级相同则随机唤醒。
- notifyAll(): 唤醒在该对象监视器上等待的所有线程。
调用 wait() 和 notify 相关方法不是线程对象调用,是共享对象调用的。
这几个方法都必须在同步代码块或者同步方法中使用
class SharedResource { private boolean ready = false; public synchronized void produce() throws InterruptedException { // 生产者生产数据 ready = true; System.out.println("商品生产了,来买吧..."); notify(); // 唤醒消费者 } public synchronized void consume() throws InterruptedException { while (!ready) { wait(); // 等待生产者生产数据 } System.out.println("商品卖完了,请等一下..."); ready = false; }
} public class WaitNotifyExample { public static void main(String[] args) { SharedResource resource = new SharedResource(); Thread producer = new Thread(() -> { try { resource.produce(); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread consumer = new Thread(() -> { try { resource.consume(); } catch (InterruptedException e) { e.printStackTrace(); } }); consumer.start(); producer.start(); }
}
wait() 与 sleep() 的区别
- 两个方法都会造成线程阻塞
- 不同的是
- wait 是 Object 的实例方法,sleep 是 Thread 的静态方法
- wait 只能用在同步代码块或者同步方法中,sleep 随意
- wait 方法执行会释放对象锁,sleep 不会
- wait 的结束时机是notify 唤醒,或到达指定时间(有参的wait),sleep 结束时机是到达指定时间
可重入锁 ReentrantLock 代替 synchronized
可重入锁(ReentrantLock)是 Java 中一种非常重要的锁机制,属于 java.util.concurrent.locks 包下的 Lock 接口。它允许同一线程在已获得锁的情况下重复获得该锁,同时避免死锁的发生。
- 重入性: 当一个线程已经持有锁时,它可以再次获取该锁而不会造成死锁。例如,Thread A 在执行某个方法时如果持有了锁,可以在同一方法内部再次调用该方法而无需等待锁释放。
- 可公平性: 可重入锁可以选择公平性设置,这意味着按请求锁的顺序分配锁,而不是随机选择。如果不设置为公平模式,可能导致某些线程长时间得不到锁。
- 显式锁和条件变量: 可重入锁提供了更丰富的功能,如可以监听条件(Condition),支持更为复杂的线程间通信。
可重入锁是通过 ReentrantLock 类实现的,通过 new ReentrantLock() 获得锁对象,通过lock()方法执行锁操作,通过unlock()执行解锁操作。
import java.util.concurrent.locks.ReentrantLock; class Example { private final ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); // 获得锁 try { // 需要同步控制的代码 System.out.println(Thread.currentThread().getName() + " is executing method."); // 可能会调用自己 method(); // 可重入 } finally { lock.unlock(); // 释放锁 } }
}
注:本文章源于学习动力节点老杜
的java教程视频后的笔记整理,方便自己复习的同时,也希望能给csdn的朋友们提供一点帮助。