Java初学者笔记-10、多线程线程池
线程(Thread)是一个程序内部的一条执行流程。
程序中如果只有一条执行流程,那这个程序就是单线程的程序。
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
创建线程
方式一:继承Thread类
使用步骤
- 继承Thread类重写run()。
- 创建对象。
- 启动start()。
main方法本身是由一条主线程运行的。
优点:编码简单。
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
注意事项
- 启动线程必须是调用start方法,不是调用run方法。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
- start()是向CPU注册,然后执行run()。
- 不要把主线程任务放在启动子线程之前。
方式二:实现Runnable接口
使用步骤
- 实现Runnable接口重写run()。
- 创建对象。
- 交给Thread对象。
- 启动start()。
Runnable r = new MyRunnable();
new Thread(r).start();
//=========使用匿名内部类简化==========
new Thread(new Runnable(){run(){}}).start();
//=============继续简化==============
new Thread(
()->{
"1".sout;
}
).start();
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多一个Runnable对象。
方式三:实现Callable接口
前两种创建方式的问题:假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。因为重写的run()方法的返回值是void。
jdk5之后提供了Callable接口和FutureTask类来实现。即方式三。
最大的优点:可返回线程执行完毕后的结果。
使用步骤
- 实现Callable接口重写
call()方法。 - 创建对象交给FutureTask(线程任务对象,取结果)。
- 把FutureTask对象交给Thread对象。
- 启动start()。
- 线程执行完毕后,通过FutureTask对象的
get()方法获取线程任务执行的结果。
注意:FutureTask本质是一个Runnable线程任务对象。
class CallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
sum += i;
}
}
return sum;
}
}
CallableThread callableThread = new CallableThread();
FutureTask<Integer> futureTask = new FutureTask<>(callableThread);
new Thread(futureTask, "name").start();
try {
Integer sum = futureTask.get();
System.out.println("计算 sum = " + sum);
} catch (Exception e) {
e.printStackTrace();
}
总结:先实现Callable接口。创建对象并传给FutureTask对象,FutureTask对象传给Thread对象,Thread对象启动start()。
线程的常用方法
| Thread提供的常用方法 | 说明 |
|---|---|
| public void run() | 线程的任务方法 |
| public void start) | 启动线程 |
| public String getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
| public void setName(String name) | 为线程设置名称 |
| public static Thread currentThread () | 获取当前执行的线程对象 |
| public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
| public final void join()... | 让调用当前这个方法的线程先执行完! |
| Thread提供的常见构造器 | 说明 |
|---|---|
| public Thread (String name) | 可以为当前线程指定名称 |
| public Thread (Runnable target) | 封装Runnable对象成为线程对象 |
| public Thread(Runnable target, String name) | 封装Runnable对象成为线程对象,并指定线程名称 |
线程安全与线程同步
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
银行的线程安全问题。存在修改资源的时候才会出现线程安全问题。
线程同步:线程安全问题的解决方案。
线程同步的核心思想:让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
加锁方式一:同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
使用步骤:
先找核心代码,ctrl+alt+T,选9,在括号里填一个对于线程来说唯一的对象。对于实例方法,使用this作为锁对象,对于静态方法使用字节码(如:Account.class)文件作为锁对象。
加锁方式二:同步方法
作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
相比于方式一,锁的范围更整体一些,即直接把整个方法锁上。
理论上来说,同步代码块好,只锁核心代码。但同步方法可读性好。
使用步骤:
只需要在方法加上synchronized关键字。
加锁方式三:Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
使用步骤:
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
ReentrantLock对象提供的方法有lock()和unlock()。
使用细节:
- 给锁对象用final关键字进行保护。
public class Account {
private String cardId; // 卡号
private double money; // 余额
private final Lock lk = new ReentrantLock();
...
}
- 将
unlock()放到finally里面。IDEA中ctrl+alt+T,选第七个。
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
lk.Lock();// 上锁
try {
//判断余额是否足够
if(this.money >= money){
// 余额足够,取钱
system.out.println(name + "取钱成功,吐出了" + money +"元");
// 更新余额
this.money -= money;
System.out.println(name +"取钱成功,取钱后,余额剩余"+ this.money +"元");
} else {
// 余额不足
System.out.println(name + "取钱失败,余额不足");
}
} finally {
lk.unlock();// 解锁
}
线程池(使用)
线程池就是一个可以复用线程的技术。
不使用线程池的问题:
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
线程池的组成
线程池由工作线程(WorkThread)和任务队列(WorkQueue)组成。
任务队列只能放Runnable线程和Callable线程。
创建线程池
JDK 5.0起提供了代表线程池的接口:ExecutorService。
对应的实现类是ThreadPoolExecutor。
方式一:通过 ThreadPoolExecutor 创建线程池
创建方法
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue <Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- 参数一:corePoolSize:指定线程池的核心线程的数量。(正式工)
- 参数二:maximumPoolSize:指定线程池的最大线程数量。(最大员工数)
- 参数三:keepAliveTime:指定临时线程的存活时间。 (临时工多久被开除)
- 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)。
- 参数五:workQueue:指定线程池的任务队列。(客人排队的地方)
- 参数六:threadFactory:指定线程池的线程工厂。(HR)
- 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
ExecutorService pool = new ThreadPoolExecutor (3, 5, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory), new ThreadPoolExecutor.AbortPolicy());
ExecutorService 的常用方法
| 方法名称 | 说明 |
|---|---|
void execute (Runnable command) |
执行 Runnable 任务 |
Future<T> submit(Callable<T> task) |
执行 Callable任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() |
等全部任务执行完毕后,再关闭线程池! |
List <Runnable> shutdownNow() |
立刻关闭线程池,停止正在执行的任务,并返回队列中来执行的任务 |
一般不关闭线程池!
线程池的注意事项
- 什么时候开始创建临时线程?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 什么时候会拒绝新任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
- 任务队列装的是还未处理的任务。
任务拒绝策略
| 策略 | 说明 |
|---|---|
| ThreadPoolExecutor.AbortPolicy() | 丢弃任务并抛出RejectedExeCutionException异常。是默认的策略 |
| ThreadPoolExecutor. DiscardPolicy() | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
| ThreadPoolExecutor. DiscardOldestPolicy() | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
| ThreadPoolExecutor. CallerRunsPolicy() | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
方式二:通过Executors创建线程池(不推荐)
是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
注意:大型并发系统环境中使用Executors如果不注意可能会出现系统风险。阿里的Java开发手册里明确规定禁止通过Executors创建线程池。
| 方法名称 | 说明 |
|---|---|
| public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程致量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程代替它。 |
| public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出現异常而结束,那么线程池会补充一个新线程。 |
| public static ExecutorService newCachedThreadPool() | 线程數量随着任务增加而增加,如果线星任务执行完毕且空闲了60s则会被回收掉。 |
| public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |

浙公网安备 33010602011771号