节制,是比仁慈更高尚的品德。|

subeipo

园龄:1个月粉丝:0关注:0

📂Java
🔖Java
2025-01-22 10:01阅读: 13评论: 0推荐: 0

Java初学者笔记-10、多线程线程池

线程(Thread)是一个程序内部的一条执行流程。
程序中如果只有一条执行流程,那这个程序就是单线程的程序。
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

创建线程

方式一:继承Thread类

使用步骤

  1. 继承Thread类重写run()。
  2. 创建对象。
  3. 启动start()。
    main方法本身是由一条主线程运行的。
    优点:编码简单。
    缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

注意事项

  1. 启动线程必须是调用start方法,不是调用run方法。
  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
  • 只有调用start方法才是启动一个新的线程执行。
  1. start()是向CPU注册,然后执行run()。
  2. 不要把主线程任务放在启动子线程之前。

方式二:实现Runnable接口

使用步骤

  1. 实现Runnable接口重写run()。
  2. 创建对象。
  3. 交给Thread对象。
  4. 启动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类来实现。即方式三。
最大的优点:可返回线程执行完毕后的结果。

使用步骤

  1. 实现Callable接口重写call()方法。
  2. 创建对象交给FutureTask(线程任务对象,取结果)。
  3. 把FutureTask对象交给Thread对象。
  4. 启动start()。
  5. 线程执行完毕后,通过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()
使用细节:

  1. 给锁对象用final关键字进行保护。
public class Account {
private String cardId; // 卡号
private double money; // 余额
private final Lock lk = new ReentrantLock();
...
}
  1. 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() 立刻关闭线程池,停止正在执行的任务,并返回队列中来执行的任务

一般不关闭线程池!

线程池的注意事项
  1. 什么时候开始创建临时线程?
  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  1. 什么时候会拒绝新任务?
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
  1. 任务队列装的是还未处理的任务。
任务拒绝策略
策略 说明
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) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

本文作者:subeipo

本文链接:https://www.cnblogs.com/subeipo/p/18685139/java-chu-xue-zhe-bi-ji10-duo-xian-cheng-xian-cheng

版权声明:本作品采用署名—非商业性使用—相同方式共享 4.0 协议许可协议进行许可。

posted @   subeipo  阅读(13)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起