Java学习笔记@多线程
笔者:unirithe
日期:11/10/2021
参考资料:
线程
线程(Thread) 是一个程序内部的一条执行路径
启动程序执行后,Main方法执行的是一条单独的执行路径
public static viod main(String[] args){
// ...
}
程序中若只有一条执行路径,那么这个程序就是单线程的程序
多线程
- 指软硬件上实现多条执行流程的技术
消息通信、购物系统都离不开多线程技术
多线程的创建
java.lang.Thread类
public class Thread extends Object implements Runnable
当Java虚拟机启动时,通常有一个非守护进程线程(通常调用某些指定类的名为
main的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:
- 已经调用了
Runtime类的exit方法,并且安全管理器已经允许进行退出操作。- 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到
run方法还是抛出超出run方法的run
Thread类的构造器
| 构造器 | 描述 |
|---|---|
| Thread(String name); | 指定线程名称 |
| Thread(Runnable target) | 封装Runnable对象成为线程对象 |
| Thread(Runnable target, String name) | 综合上面的两个构造方法 |
方式一: 继承Thread类
实现步骤:
- 定义子类PrimeThread继承
java.lang.Thread,重写run()方法 - 创建
PrimeThread类的对象 - 调用线程对象的
start()方法启动线程(启动后会执行run方法)
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
new PrimeThread().start();
优点:编程简单
缺点:存在单继承的局限性,线程类已经继承Thread,无法继承其他类,不利于扩展
Q : 为什么不直接调用 run 方法?
W: 若直接调用run方法会当成普通方法执行,此时相当于还是单线程
只有调用start方法才是启动一个新的线程执行
尽量把start()方法的调用放在主线程的前面
方式二:实现Runnable接口
实现步骤:
- 定义一个线程任务类
RunnableImpl实现Runnable接口重写run()方法 - 创建
RunnableImpl任务对象 - 把
RunnableImpl任务对象交给Thread处理 - 调用线程对象的
start()方法启动线程
class RunnableImpl implements Runnable {
long minPrime;
RunnableImpl(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
new Thread(new RunnableImpl()).start();
优点:只实现了Runnable接口,可继续继承类和实现其他接口,扩展性强
缺点:编程多一层对象包装,如果有线程执行则结果不可以直接返回
范例:使用匿名内部类实现Runnable接口
new Thread(new Runnable(){
@Override
public void run(){
//...
}
}).start();
// Lambda表达式简化
new Thread() -> {
@Override
public void run(){
//...
}
}).start();
方式三:实现Callable接口(JDK5)
前两种方式都存在的问题:
-
重写的run方法均不能直接返回结果
-
不适合需要返回线程执行结果的业务场景
可使用JDK5.0 提供的
java.util.concurrent.Callable和java.util.concurrent.FutureTask解决
FutureTask 作用:
-
是Runnable的对象(实现了Runnable接口),可交给Thread
-
可以在线程执行完毕后通过调用其get方法得到线程执行完成的结果
@FunctionalInterface
public interface Callable<V>
public class FutureTask<V>
extends Object
implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask的API
| 方法名称 | 描述 |
|---|---|
public FutureTask<>(Callable call) |
把Callable对象封装成FutureTask对象 |
public V get() throws Exception |
获取线程执行call方法返回的结果 |
实现步骤:
- 定义类实现
Callable接口,重写call方法 - 用
FutureTask把 Callable 对象封装成线程任务对象 - 调用
Thread的start方法启动线程,执行任务 - 线程执行完毕后,通过
FutureTask的get方法去获取任务执行的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Demo{
public static void main(String[] args) throws Exception {
Callable<String> call = new CallableImpl(100);
FutureTask<String> f1 = new FutureTask<>(call);
Thread t1 = new Thread(f1);
t1.start();
System.out.println(f1.get()); // get 方法会一直等待,直到该线程结束
System.out.println("所有线程执行完毕.");
}
}
class CallableImpl implements Callable<String>{
private int n ;
public CallableImpl(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return "子线程执行的结果是: " + sum;
}
}
优点:
- 扩展性强,线程任务类只是实现接口,可继续继承类和实现接口
- 可在线程执行完毕后获取线程执行的结果
缺点:
- 编程较复杂
不同创建方式对比
| 方式 | 复杂性 | 扩展性 | 继承性 | 可获取结果 |
|---|---|---|---|---|
| 继承Thread | 简单 | 弱 | 单继承 | 否 |
| 实现Runnable接口 | 较复杂 | 强 | 可继承 | 否 |
| 实现Callable接口 | 最复杂 | 强 | 科技城 | 是 |
常用方法
| 方法 | 描述 |
|---|---|
| public static Thread currentThread() | 获取当前线程 |
| void setName(String name) | 设置线程的名称 |
| getName() | 获取线程的名称 |
| public static void sleep(long time) | 当前线程休眠,单位:毫秒 |
| public void run() | 线程任务方法 |
| public void start() | 线程启动方法 |
范例:返回主线程的名称
class Demo{
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
范例:设置线程名称
class myThread extends Thread{
myThread(String name){
super(name);
}
public void run(){
//...
}
}
class Demo{
public static void main(String[] args) {
new myThread("子线程").start();
}
}
线程安全问题
多个线程同时操作同一个共享资源时可能会出现业务安全问题,称为线程安全问题
出现原因:
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
案例:两个人有一个共同账户,余额是1万元,他们同时取钱1万
分析:
- 账户类,表示两个人的共享账户
- 线程类,处理账户对象
- 创建两个线程对象,传入同一个账户对象
- 启动两个线程,去同一个账户对象中取1万
demo.java
public class Demo{
public static void main(String[] args) {
Account acc = new Account("银行卡账户", 10000);
new WithdrawThread(acc, "甲").start();
new WithdrawThread(acc, "乙").start();
}
}
Account.java
public class Account{
private String cardId;
private double money;
public Account(){}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void withdrawMoney(double money){
// 获取当前线程的名称,即取钱的用户
String name = Thread.currentThread().getName();
if (this.money >= money) {
// 取钱
System.out.println(name + "成功取出: " + money + " 元");
// 更新数据
this.money -= money;
System.out.println("存款剩余: " + this.money);
} else{
System.out.println(name +"因余额不足取钱失败. ");
}
}
}
WithdrawThread.java
public class WithdrawThread extends Thread{
private Account acc;
public WithdrawThread(Account acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
acc.withdrawMoney(10000);
}
}
运行结果
乙成功取出: 10000.0 元
甲成功取出: 10000.0 元
存款剩余: 0.0
存款剩余: -10000.0
线程同步
- 为了解决线程安全问题
取钱案例出现问题的原因
- 多个线程同时执行,发现账户余额都足够的
如何保证线程安全? 让多个线程先后依次访问共享资源。
核心思想
加锁,把共享资源上锁,每次只允许单个线程访问,访问后才解锁
方式一:同步代码块
作用:把出现线程安全问题的核心代码上锁
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
synchronized(同步锁对象){
//操作共享资源的代码(核心代码)
}
锁对象要求,理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可
基于上一个取钱案例,修改Account.java的代码如下:
public void withdrawMoney(double money){
// 获取当前线程的名称,即取钱的用户
String name = Thread.currentThread().getName();
// 同步代码块
synchronized (this) {
if (this.money >= money) {
// 取钱
System.out.println(name + "成功取出: " + money + " 元");
// 更新数据
this.money -= money;
System.out.println("存款剩余: " + this.money);
} else{
System.out.println(name +"因余额不足取钱失败. ");
}
}
}
运行结果
甲成功取出: 10000.0 元
存款剩余: 0.0
乙因余额不足取钱失败.
锁对象用任意唯一的对象好坏?
任意对象即 synchronized 里的参数为任意对象
- 坏,会影响其他无关线程的执行
锁对象的规范要求
-
规范上:建议使用共享资源作为锁对象
-
对于实例方法建议使用 this 作为锁对象, 即
synchronized(this) -
对于静态方法建议使用字节码 (类名.class) 对象作为锁对象
public static void run(){ synchronized(Account.class){ //... } }
总结
-
同步代码块如何实现线程安全?
- 对核心代码使用
synchronized进行加锁 - 每次只能一个线程占锁进入访问
- 对核心代码使用
-
同步代码块的同步锁对象有什么要求?
- 对于实例对象建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)作为锁对象
方式二: 同步方法
作用:把出现线程安全问题的核心方法给上锁
原理:每次只允许单个线程进入,执行完毕后自动解锁
格式:
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享的代码
}
范例:public synchronized void withdrawMoney(double money)
底层原理:
- 同步方法底层有隐式锁对象,锁的范围是整个方法代码
- 实例方法默认使用
this作为锁对象 - 静态方法默认用
类名.class作为锁对象
同步代码块锁范围 比 同步方法锁的范围更大
方式三:Lock 锁
public interface Lock
实现类:
ReentrantLock的官方使用范例:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
作用:
-
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了新的锁对象Lock,更加灵活、方便
-
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
-
Lock是接口不能直接实例化,则使用其实现类
ReentrantLock来构建Lock锁对象public ReentrantLock()获得Lock锁的实现类对象
Lock的API
| 方法 | 描述 |
|---|---|
| void lock() | 获得锁 |
| void unlock() | 释放锁 |
范例:修改取钱案例中Account的代码
public class Account{
private final Lock lock = new ReentrantLock();
public void withdrawMoney(double money){
// 获取当前线程的名称,即取钱的用户
// 上锁
lock.lock();
try {
if (this.money >= money)
// 取钱成功
else
// 取钱失败
} finally {
// 释放锁
lock.unlock();
}
}
}
*线程池
线程池指一个可复用线程的技术
使用线程池以前,存在的问题:若用户没发起请求,后台就创建一个新线程来处理,等待新任务又要创建新线程,增大了开销,影响系统性能
JDK5起提供了代表线程池的接口ExecutorService
public interface ExecutorService
extends Executor
实现类:
创建线程池对象
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
构造器:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
| 参数 | 说明 |
|---|---|
| int corePoolSize | 线程池的线程数量(核心线程) >= 0 |
| int maximumPoolSize | 线程池可支持的最大线程数,>= corePoolSize |
| long keepAliveTime | 临时线程的最大存活时间 >= 0 |
| TimeUnit unit | 存活时间的单位(秒、分、时、天)时间单位 |
| BlockingQueue |
时间任务队列,不能为null |
| ThreadFactory threadFactory | 创建线程的线程工厂,不能为null |
| RejectedExecutionHandler handler | 线程忙、任务满时候,处理的策略,不能为null |
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
常见问题
-
临时线程什么时候创建
新任务提交时,发现核心线程都在忙,任务队列也满,并且还可以创建临时线程时才会创建临时线程
-
什么时候开始拒绝任务
核心线程和临时线程都在忙,任务队列已满,新的任务过来的时候才开始任务拒绝
线程池处理Runnable任务
实现步骤:
- 使用 ExecutorService的方法
- void execute(Runnable runnable)
ExecutorService 常用方法
| 方法 | 描述 |
|---|---|
void execute(Runnable command) |
一般用于执行Runnable任务 |
Future<T> submit(Callable<T> task) |
执行任务,返回未来任务对象获取线程结果,一般用于执行Callable任务 |
void shutdown() |
等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() |
立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
import java.util.concurrent.*;
class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出了: HelloWorld ==> " + i);
}
}
}
public class ThreadPoolDemo1 {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(3,
5,
6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
RunnableImpl runnable = new RunnableImpl();
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
}
}
新任务拒绝策略
| 策略 | 详解 |
|---|---|
ThreadPoolExecutor.AbortPolicy |
丢弃任务并抛出RejectedExceutionException异常。默认策略 |
ThreadPoolExecutor.DiscardPolicy |
丢弃任务,但是不抛出异常,不推荐 |
ThreadPoolExecutor.DiscardOldestPolicy |
抛弃队列中等待最久的任务,然后把当前任务加入队列 |
ThreadPoolExecutor.CallerRunsPolicy |
由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
线程池处理Callable任务
实现步骤:
- 使用
ExecutorService方法 Futrue<T> submit(Callable<T> command)
import java.util.concurrent.*;
class CallableImpl implements Callable<String>{
private int n;
public CallableImpl(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return Thread.currentThread().getName() + " 执行 1-" + n + " 的结果为: " + sum;
}
}
class Demo{
public static void main(String[] args) throws Exception {
ExecutorService pool = new ThreadPoolExecutor(3,
5,
6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Future<String> f1 = pool.submit(new CallableImpl(100));
Future<String> f2 = pool.submit(new CallableImpl(200));
Future<String> f3 = pool.submit(new CallableImpl(300));
Future<String> f4 = pool.submit(new CallableImpl(400));
Future<String> f5 = pool.submit(new CallableImpl(500));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
System.out.println(f5.get());
}
}
运行结果
pool-1-thread-1 执行 1-100 的结果为: 4950
pool-1-thread-2 执行 1-200 的结果为: 19900
pool-1-thread-3 执行 1-300 的结果为: 44850
pool-1-thread-2 执行 1-400 的结果为: 79800
pool-1-thread-2 执行 1-500 的结果为: 124750
Executors 工具类实现线程池
java.util.concurrent.Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
常用方法
| 方法 | 描述 |
|---|---|
public static ExecutorService newCachedThreadPool() |
线程数量随着任务增加而增加,若线程任务执行完毕且空闲了一段时间则会被回收 |
public static ExecutorServce newFixedThreadPool(int nThreads) |
创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程池替换它 |
public static ExecutorService newSingleThreadExecutor() |
创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程 |
public static ScheduleExecutorService new ScheduledThreadPool(int corePoolSize) |
创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务 |
注:Executors的底层是基于线程类的实现类ThreadPoolExecutor 创建线程池对象
缺陷
大型并发系统环境中使用Executors若不注意可能会出现系统风险
newFixedThreadPool(int nThreads)和newSingleThreadExecutor()存在的问题:
允许请求的任务队列长度为Integer.MAX_VALUE ,可能出现java.lang.OutOfMemoryError异常
new CachedThreadPool()和newScheduledThreadPool(int corePoolSize)存在的问题:
创建的线程数量最大上限是Integer.MAX_VALUE ,线程数可能会随着任务 1 : 1 增长,也可能出现上面的异常
阿里云Java开发手册
【强制】线程池不允许使用
Executors创建,而通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险说明:
Executors返回的线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadPool
允许的请求队列长度为
Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM异常CachedThreadPool 和 ScheduledThreadPool
允许的创建线程数量为
Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM异常
定时器
一种控制任务延时调用,或周期调用的技术
作用:闹钟、定时邮件发送
实现方式:
- Timer
- ScheduledExecutorService
Timer 定时器
构造器:public Timer() 创建Timer 定时器对象
public void schedule(TimerTask task, long delay, long period) 开启一个定时器,按照计划处理TimerTask任务
Timer定时器的特点和存在的问题
- Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
- 可能因为其中的某个任务的异常使Timer线程挂掉,从而影响后续任务执行
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +"执行了一次");
}
}, 1000, 2000);
ScheduledExecutorService定时器
public interface ScheduledExecutorService
extends ExecutorService
- ScheduledExecutoreService是JDK5引入的并发包,为了弥补Timer的缺陷,其内部为线程池
public static ScheduledExecutorService new ScheduledThreadPool(int corePoolSize) 得到线程池对象
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 周期调度方法
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
// 开启定时任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行输出: AAA");
}
}, 0, 2, TimeUnit.SECONDS);
优点:基于线程池,某个任务的执行情况不会影响其他定时任务的执行
并发、并行
正在运行的程序(软件)是一个独立的进程,线程是属于进程的,多个线程其实是并发与并发同时进行的
并发的理解:
- CPU同时处理线程的数量有限
- CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,所以感觉是在同时执行,这就是并发
并行的理解:
- 在同一个时刻上,同时有多个线程在被CPU处理并执行
并发: CPU分时轮询的执行线程
并行:同一个时刻同时在执行
线程的生命周期
线程的状态:线程从生到死的过程,以及中间经历的各种状态及状态转换
Java 总共定义了 6种状态, 它们都在Thread类的内部枚举类中
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程的六种状态互相转换

| 线程状态 | 描述 |
|---|---|
NEW(新建) |
线程刚被创建,但是未被启动 |
Runnable(可运行) |
线程已经调用了start()等待cpu调度 |
Blocked(锁阻塞) |
线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 |
Waiting(无限等待) |
一个线程进入Waiting状态,另一个线程调用notify或notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) |
同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep、Object.wati |
Timinated(被终止) |
因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |

浙公网安备 33010602011771号