一、 java进程与线程
- 运行一个Java程序会产生一个Java进程,每一个Java进程至少包含三个线程:
main()主线程、gc()线程、异常处理线程 - 每一个进程对应一个JVM实例,多个线程共享JVM里面的堆,每一个线程都有自己私有的栈
- Java采用单线程编程模型,如果程序里面没有创建线程的话,只会自动创建一个线程->主线程
- Java程序启动时,主线程立刻运行,在执行完各种子线程的关闭动作后才能完成执行
从 JVM 角度说进程和线程之间的关系
如上图:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的 程序计数器、虚拟机栈 和 本地方法栈。
1. 为什么要使用多线程
- 提高计算机系统CPU的利用率
- 提高应用程序的响应,增强用户体验
2. 使用多线程可能带来那些问题?
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程可能会遇到很多问题。比如:内存泄漏、死锁、线程不安全等等
3.单核CPU与多核CPU
- 单核CPU:在一个时间单元内,只能执行一个线程的任务,其实是一种假的多线程。然而在单核的情况下,让用户看起来像同一时刻并发执行多个任务的原因是,CPU分配给单一任务执行的时间片很短、任务切换的频次高,造成所有任务都在并发执行的假象。
- 多核CPU:多核时代多线程主要是为了提高 CPU 利用率,同一时刻多个任务同时进行。
4.并行与并发
- 并行:多个CPU同时执行多个任务
- 并发:单个CPU同时执行多个任务 (如:秒杀、抢票)
5. java 守护线程与用户线程
Java提供了两种类型的线程:守护线程 和 用户线程
- 用户线程 是高优先级线程。JVM 会在终止之前等待任何用户线程完成其任务。
- 用户线程 是低优先级线程。其唯一作用是为用户线程提供服务。常见的做法,就是将守护线程用于后台支持任务,比如垃圾回收、释放未使用对象的内存、从缓存中删除不需要的条目。
由于守护线程的作用是为用户线程提供服务,并且仅在用户线程运行时才需要,因此一旦所有用户线程完成执行,JVM 就会终止。也就是说 守护线程不会阻止 JVM 退出。
这也是为什么通常存在于守护线程中的无限循环不会导致问题,因为任何代码(包括 finally 块 )都不会在所有用户线程完成执行后执行。这也是为什么我们并不推荐 在守护线程中执行 I/O 任务 。因为可能导致无法正确关闭资源。
但是,守护线程并不是 100% 不能阻止 JVM 退出的。守护线程中设计不良的代码可能会阻止 JVM 退出。例如,在正在运行的守护线程上调用Thread.join() 可以阻止应用程序的关闭。
二、线程的创建及使用
创建线程四种方式
- 继承于
Thread类 - 实现
Runnable接口 - 实现
Callable接口的方式 - 线程池创建
1.继承于Thread类
- 创建一个继承于
Thread类的子类 - 重写
Thread类的run()方法 ==> 将此线程需要执行的操作声明在run()中 - 创建
Thread类的子类的对象 ==> 主线程 - 通过此对象调用
start()方法
1 | class MyThread extends Thread { |
1.1 Thread类的常用方法
start()启动当前线程;调用当前线程的run()run()通常需要重写Thread类的此方法,将创建的线程要执行的操作声明在此方法中currentThread()静态方法,返回执行当前代码的线程getName()获取当前线程的名字setName()设置当前线程的名字getPriority()获取线程的优先级setPriority()设置线程优先级yield()释放当前线程CPU的执行权 => 很可能下一刻该线程又抢到CPU执行权join()在某个程序执行流程中调用其他线程的join()时,当前线程将会被阻塞,直到调用的线程执行完毕为止(插队)sleep(long millitime)让当前线程睡眠指定millitime时间,此时该线程处于阻塞状态stop()_中断线程 已经过时_:当执行此方法时,强制结束当前线程interrupt()目前使用的通知线程中断的方法 :1. 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,抛出一个InterruptedException2. 如果线程处于正常活动状态,那么会将该线程的终端标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
1.2线程的优先级
MIN_PRIORITY= 1NORM_PRIORITY= 5MAX_PRIORITY= 10
注意:高优先级的线程要抢占低优先级线程的cup执行权,只是概率上高优先级的线程有更高的概率被执行,并不意味着只有在高优先级的线程执行完后低线程的线程才执行。
1.3 线程start() 方法与 run() 方法的区别
start():启动当前线程并调用当前线程的run()。new 一个 Thread,线程进入了新建状态,调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。这是真正的多线程工作。同时一个线程对象只能调用一次start()方法,如果重复调用将抛出 异常IllegalThreadStateException因为在执行完start()后该线程已经处于终止状态了。run():直接执行run()方法,会把run()方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
1.4 如何给run()传参
- 构造函数传参
- 成员变量传参
- 回调函数传参
2.实现 Runnable接口
- 创建一个实现了
Runnable接口的类 - 实现类去实现Runnable中的抽象方法
run() - 创建实现类的对象
- 将此对象作为参数传递到
Thread类的构造器中,创建Thread类的对象 - 通过
Thread类的对象调用start()
1 | class MyThread2 implements Runnable { |
2.1继承于Thread类与实现 Runnable接口 两种创建线程方式的联系与区别
public class Thread implements Runnable==>Thread类本身也实现了Runnable接口。都需要重写run()方法,将线程要执行的逻辑声明在run()中。- 开发中我们优先选择实现
Runnable接口的方式:1.实现的方式没有类的单继承的局限性;2.实现的方式更适合来处理多个线程共享数据的情况
3.实现 Callable 接口的方式
- 创建一个实现了
Callable接口的实现类 - 实现
call(),将此线程需要执行的方法声明到call()中 - 创建
Callable接口实现类的对象 - 将此
Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象 - 将
FutureTask对象作为参数传递到Thread类的构造器中,创建Thread类的对象并调用start(); - 获取
Callable中的call方法的返回值
1 | class NumThread implements Callable { |
3.1 如何实现处理线程的返回值
- 主线程等待法:当我们在
run()方法中写返回值的时候,由于主线程往往比子线程先执行完毕所以根本无法接受子线程的返回值,所以我们可以在主线程中写循环等待算法,当子线程执行完毕后再执行主线程。缺点:当需要等待的变量很多则不合适,并且需要等待多久也不确定。 - 使用
Thread类的join()方法阻塞当前线程等待子线程处理完毕后获取返回值。优点:是无需处理主线程循环等待算法,更精确;缺点:粒度不够细。 - 通过
Callable接口实现:1. 通过Future Task2. 线程池获取
3.2如何理解实现Callable接口的方式要比实现Runnable接口创建多线程的方式更好
call()可以有返回值call()可以抛出异常,被外面的异常捕获,获取异常的信息Callable支持泛型
4. 线程池
4.1ThreadPoolExecutor 类
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数:
keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;unit:keepAliveTime参数的时间单位。threadFactory:executor 创建新线程的时候会用到。handler:饱和策略。
使用线程池创建线程
1 | class NumberThread implements Runnable { |
4.2为什么要使用线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
4.3. 执行 execute()方法和 submit()方法的区别
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
三、线程的生命周期及状态
java 线程在运行的生命周期中一共有下面六种状态:

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示:
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片后就处于 RUNNING(运行) 状态。
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程不会被分配CPU执行时间,要等待被其他线程显式的唤醒后才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。
当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行完 run()方法之后将会进入到 TERMINATED(终止) 状态,并且一旦终止就不能再复生。
四、线程的同步
1.多线程的安全问题
问题的引出
假设当前机场有100张票,我们创建3个线程来模拟三个窗口卖票
代码实现
1 | class Window implements Runnable { |
运行结果如下
1 | 窗口2卖票,票号为100 |
问题分析
通过上面运行结果我们发现,卖票的过程中出现了重票和错票的问题。原因是当某个线程操作卖票的过程中,尚未操作完成,其他的线程参与了进来,所以出现了多线程的安全问题。
问题解决
当一个线程w操作 ticket的时候其他的线程不允许操作进来,直到线程w操作完ticket其他的线程才允许操作ticket,即使线程 w 阻塞也不允许改变。这样便引出了线程同步的概念。
2.通过同步机制来解决线程的同步问题
同步锁机制
在并发操作中,当多个任务进行共享资源竞争时,为了防止出现线程同步的问题,就是当资源被一个任务使用时,在其上加锁。当资源被锁定后,其他任务在其被解锁前就无法访问它了,在其被解锁后,另一个任务才可以锁定并使用它。
java中我们可以通过同步机制来解决线程的同步问题
- 同步代码块
- 同步方法
- 重入锁
ReentrantLock
2.1同步代码块
1 | synchronized(同步监视器){ |
- 操作共享数据的代码即为同步的代码
- 共享数据==>多个线程共同操作的数据
- 同步监视器 俗称==> 锁 任何一个对象都可以充当锁
- 多个线程必须共用同一把锁 (同步监视器声明的位置不可以在
run()中) - 在实现
Runnable接口创建的多线程中可以考虑使用this做同步监视器
1 | class Window2 implements Runnable { |
2.2 同步方法
如果共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的
- 同步方法仍然涉及到同步监视器,只是不需要显示的声明
- 非静态的同步方法,同步器是
this - 静态的同步方法,同步器是 当前类本身
1 | class Window3 implements Runnable { |
2.3 重入锁ReentrantLock
ReentrantLock类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义。在实现线程安全的控制中,ReentrantLock可以显式加锁、释放锁。
ReentrantLock常用方法
lock()获得锁unlock()释放锁
1 | class Window4 implements Runnable { |
2.4 synchronized 与 Lock 的区别
- 两者都可以解决线程的安全问题
Lock是显式锁,手动开启和关闭锁;synchronized是隐式锁,出了作用域自动释放锁Lock只有代码块锁,synchronized有代码块锁和方法锁- 使用
Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性
五、线程间通信
1.锁池EntryList与等待池WaitSet
锁池EntryList :假设线程A已经拥有了某个对象的锁,而其他线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的所目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方就是该对象的锁池。
等待池WaitSet:假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁
- 锁池中的线程就会去竞争该对象的锁
- 优先级高的线程竞争到锁的概率就会变高
- 没有竞争到的线程就会留在锁池当中,不会进入到等待池当中
- 竞争到的线程就会运行直到执行完
synchronized或者是遇到异常,然后释放锁 - 被
notify()或notifyAll()唤醒的线程会进入到锁池当中
2.线程通信涉及的方法
wait():一旦执行此方法当前线程就进入阻塞状态,释放同步监视器,并进入到等待池中等待被唤醒notify():随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会notifyAll():会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
注意
三个方法执行线程通信时必须使用在同步代码块或同步方法中; 三个方法的调用者必须是 同步代码块或者同步方法的 同步监视器;三个方法都定义在 java.lang.Object中
3. sleep()方法和wait() 方法的区别
sleep()是Thread类的方法;wait()是Object类中定义的方法sleep()方法可以在任何地方使用;wait()只能在synchronized方法或者synchronized块中使用Thread.sleep只会让出 CPU,不会导致锁行为的改变;Object.wait不仅会让出CPU,还会释放已经占有的同步资源锁
4. 实现线程间通信
实现两个线程交替打印 1 - 100
代码实现
1 | class Number implements Runnable { |
5.线程死锁
5.1什么是死锁
不同的线程分别占用对方需要的同步资源不放弃,都等待对方先放弃自己需要的同步资源,就形成了死锁
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

注意:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态无法继续执行。
5.2死锁演示
1 | public class DeadLockTest { |
参考资料