深入理解java多线程
1、线程与进程
线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程与进程的区别
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程: 线程是程序执行的最小单位。也就是,进程可以包含多个线程,而线程是程序执行的最小单位。
线程的状态

- NEW:线程刚创建
- RUNNABLE: 在JVM中正在运行的线程,其中运行状态可以有运行中RUNNING和READY两种状态,由系统调度进行状态改变。
- BLOCKED:线程处于阻塞状态,如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 。
- WAITING : 等待状态
- TIMED_WAITING: 调用sleep() join() wait()方法可能导致线程处于等待状态
- TERMINATED: 线程执行完毕,已经退出
2、上下文切换
既然操作系统要在多个进程(线程)之间进行调度,而每个线程在使用CPU时总是要使用CPU 中的资源,比如CPU寄存器和程序计数器。这就意味着,操作系统要保证线程在调度前后的正常执 行,所以,操作系统中就有上下文切换的概念,它是指CPU(中央处理单元)从一个进程或线程到另一个 进程或线程的切换。
1. 暂停一个进程/线程的处理,并将该进程/线程的CPU状态(即上下文)存储在内存中的某个地方。
2. 从内存中获取下一个进程/线程的上下文,并在CPU的寄存器中恢复它。
3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
3、并发和并行的区别
并发:是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状。
并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,CPU同时处理这些线程请求的能力。区别就在于CPU是否能同时处理所有任务,并发不能,并行能。
4、java中创建与启动线程
方式1:使用 Thread类或继承Thread类。
// 构造方法的参数是给线程指定名字,推荐 Thread t1 = new Thread("t1") { @Override // run 方法内实现了要执行的任务 public void run() { log.debug("Hello Thread"); } }; t1.start();
方式2:实现 Runnable 接口配合Thread。
把【线程】和【任务】(要执行的代码)分开。Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)。
// 创建任务对象 Runnable task2 = new Runnable() { @Override public void run() { log.debug("hello"); } }; // 参数1 是任务对象; 参数2 是线程名字,推荐 Thread t2 = new Thread(task2, "t2"); 0 t2.start();
方式3:使用FutureTask 配合 Thread。
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。
// 创建任务对象 FutureTask<Integer> task3 = new FutureTask<>(() -> { log.debug("hello"); return 100; }); // 参数1 是任务对象; 参数2 是线程名字,推荐 new Thread(task3, "t3").start(); // 主线程阻塞,同步等待 task 执行完毕的结果 Integer result = task3.get(); log.debug("结果是:{}", result);
FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接 口。所以它既可以作为Runnable被线程执行,又可以作为 Future得到Callable的返回值。因此我们通过一个线程运行Callable,但是Thread不支持构造方法中传递Callable的实例,所 以我们需要通过FutureTask把一个Callable包装成Runnable,然后再通过这个FutureTask拿到Callable 运行后的返回值。
5、线程的常见方法
- start(): 启动一个线程。start方法只是让线程进入就绪,里面代码不一定立刻运行。CPU的时间片还没分给它。 每个线程对象的start方法只能调用一次,如果调用了多次会出现legalThreadStateException。
- run(): 新线程启动后会执行的方法。
- join(): 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能 继续运行的场景。
- getName(): 获取当前线程名称。
- getPriority(): 获取线程的优先级。
- setPriority(): 修改线程的优先级。线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。优先级的范围从1~10,默认优先级是5。
- getState(): 获取线程的状态。 NEW、RUNNABLE、BlOCKED、WAITING、TIMED_WAITING、TERMINATED。
- interrupted(): 中断线程。如果被中断线程正在sleep,wait,join 会导致被中断的线程抛出InterruptedException,并清除 中断标记 ;如果中断的正在运行的线程,则会设置中断标记。
- stop(): 中断线程,JDK已经废弃。调用stop方法无论run()中的逻辑是否执行完,都会释放CPU资源,释放锁 资源。安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作。
- isInterrupted(): 判断是否被中断。不会清除中断标记。
- interrupted(): 判断当前线程是否被中断。 会清除中断标记。
- currentThread(): 获取当前正在执行的线程。
- sleep(): 让当前执行的线程休眠n毫秒,休眠时让出CPU的时间片给其他线程。但是不会释放同步锁。
- yield(): 提示线程调度器让出当前线程对CPU的使用。
sleep 与 yield
Sleep方法:
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),不会释放对象锁 。
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException。
- 睡眠结束后的线程未必会立刻得到执行。
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。
- sleep当传入参数为0时,和yield相同。
yield 方法
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执 行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
Wait和Sleep的区别:
- 它们最大本质的区别是,Sleep()不释放同步锁,Wait()释放同步锁。
- 还有用法的上的不同是:Sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用Interreput()来强行打断;Wait()可以用Notify()直接唤起。
- 这两个方法来自不同的类分别是Thread和Object
- 最主要是Sleep方法没有释放锁,而Wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
6、守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程, 只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
守护线程的应用场景
- 比如在JVM中垃圾回收器就采用了守护线程,如果一个程序中没有任何用户线程,那么就不会产生垃圾,垃圾回 收器也就不需要工作了。
- 在一些中间件的心跳检测、事件监听等涉及定时异步执行的场景中也可以使用守护线程,因为这些都是在后台不 断执行的任务,当进程退出时,这些任务也不需要存在,而守护线程可以自动结束自己的生命周期。
7、Object 对象的wait/notify/notifyAll
等待通知机制可以基于对象的wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。
- notify():通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得 锁的线程重新进入WAITING状态。
- notifyAll():通知所有等待在该对象上的线程。尽可能用notifyAll(),谨慎使用notify(),因为notify()只会唤醒一个线 程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。 wait(): 调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方 法后,会释放对象的锁。
- wait(long): 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
- wait (long,int): 对于超时时间更细粒度的控制,可以达到纳秒 。
示例:
public class WaitDemo { public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { try { System.out.println("wait开始"); synchronized (locker) { locker.wait(); } System.out.println("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); //保证t1先启动,wait()先执行 Thread.sleep(1000); Thread t2 = new Thread(() -> { synchronized (locker) { System.out.println("notify开始"); locker.notifyAll(); System.out.println("notify结束"); } }); t2.start(); } }
8、死锁的概念
死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁产生的四个必要条件:
- 互斥条件:顾名思义,线程对资源的访问是排他性,当该线程释放资源后下一线程才可进行占用。
- 请求和保持:简单来说就是自己拿的不放手又等待新的资源到手。线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
- 不可剥夺:在没有使用完资源时,其他线性不能进行剥夺。
- 循环等待:一直等待对方线程释放资源。

浙公网安备 33010602011771号