一、多线程的基础概念:
多线程的概念:线程是程序内部的一条执行流程,由Thread对象代表;多线程是从软硬件上实现多条执行流程的技术,由CPU负责调度,应用场景包括12306购票、百度网盘的上传下载、消息通信等。
创建多线程的第一种方式:通过继承Thread类实现,步骤为:定义一个子类继承Thread类;重写Thread类的run方法,在该方法中编写线程要执行的任务代码;创建该子类的实例对象;调用实例对象的start方法启动线程。
第一种创建方式的优缺点:优点是编码简单;缺点是线程类已继承Thread类,无法再继承其他类,不利于功能扩展(但该缺点并非绝对,需根据实际需求判断)。
注意事项:启动线程必须调用start方法,直接调用run方法会将线程对象当作普通对象处理,仍为单线程;不要将主线程的任务放在启动子线程之前,否则主线程会先执行完,无法体现多线程同时执行的效果。
二、多线程实现Runnable接口创建:
多线程的第二种创建方式:实现Runnable接口
步骤为:
1、定义一个线程任务类实现Runnable接口;
2、重写Runnable接口的run方法,编写线程要执行的任务代码,示例代码如下:
// 定义线程任务类实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的任务代码
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}3、创建该线程任务类的实例对象;
4、将该实例对象作为参数交给Thread对象,包装成线程对象;
5、调用线程对象的start方法启动线程,示例代码如下:
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建线程任务对象
Runnable task = new MyRunnable();
// 将任务对象包装成线程对象
Thread t1 = new Thread(task);
// 启动线程
t1.start();
// 主线程任务
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出:" + i);
}
}
}第二种创建方式的优缺点
优点:线程任务类仅实现接口,可继续继承其他类和实现其他接口,扩展性强,对类的功能阉割少。
缺点:需要额外创建一个任务对象,且线程执行完的run方法不能直接返回结果。
第二种创建方式的简化写法:匿名内部类
无需定义Runnable接口的实现类,直接创建Runnable匿名内部类对象作为线程任务对象,再包装成线程对象并启动。可进一步简化为链式编程,甚至利用Lambda表达式简化(因Runnable是函数式接口),示例代码如下:
// 匿名内部类写法
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程一输出:" + i);
}
}
}).start();
// Lambda表达式简化写法
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程二输出:" + i);
}
}).start();三、多线程实现Callable接口创建:
多线程的第三种创建方式:实现Callable接口:步骤为定义一个线程任务类实现Callable接口,并指定泛型类型(该类型为线程执行完返回的结果类型);重写Callable接口的call方法,编写线程要执行的任务代码,并在方法中返回结果;创建该线程任务类的实例对象;将该实例对象作为参数交给FutureTask对象,用于接收线程执行后的结果;将FutureTask对象作为参数交给Thread对象,包装成线程对象;调用线程对象的start方法启动线程;通过FutureTask对象的get方法获取线程执行后的结果。
第三种创建方式的示例代码:
// 定义线程任务类实现Callable接口
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
// 测试类
public class ThreadDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程任务对象
Callable<Integer> callable = new MyCallable();
// 创建FutureTask对象接收结果
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 包装成线程对象并启动
new Thread(futureTask).start();
// 获取线程执行结果
Integer result = futureTask.get();
System.out.println("结果是:" + result);
}
}第三种创建方式的优缺点:优点是线程任务类仅实现接口,可继续继承其他类和实现其他接口,扩展性强;call方法可以直接返回结果,方便获取线程执行后的结果。缺点是需要额外创建任务对象和FutureTask对象,编码相对复杂;get方法可能会阻塞主线程,需要处理异常。
三种创建方式的对比与选择:继承Thread类方式编码简单,但扩展性差;实现Runnable接口方式扩展性强,但无法直接返回结果;实现Callable接口方式扩展性强且能返回结果,但编码复杂。实际开发中,根据是否需要返回结果和扩展性需求选择,优先考虑实现接口的方式,需要返回结果时选Callable,否则选Runnable。
四、多线程的常用方法
线程名称相关方法
获取线程名称:getName()
设置线程名称:setName()
说明:线程默认名称为“Thread-索引”,主线程默认名称为“main”。设置名称需在启动线程之前,也可通过有参构造器设置。
获取当前线程对象方法
方法:currentThread()
说明:这是Thread类的静态方法,用于获取当前正在执行的线程对象,哪个线程调用该方法,就返回哪个线程的对象。
线程休眠方法
方法:sleep(long millis)
说明:这是Thread类的静态方法,作用是让当前执行的线程进入休眠状态,参数为休眠时间(单位为毫秒),时间到后线程继续执行,需处理异常。
线程插队方法
方法:join()
说明:该方法用于让调用此方法的线程先执行完毕,即实现线程插队,待该线程执行完后,其他线程再继续执行,可用于控制线程执行顺序。
其他线程方法说明
如yield()、interrupt()、线程守护、线程优先级等方法在开发中较少使用,入门阶段掌握上述方法即可,后续用到再详细讲解。
线程安全问题的定义:指多个线程同时操作同一个共享资源,且对该资源进行修改时,可能出现的业务安全问题。
线程安全问题的示例:以小明和小红从共同账户(初始余额10万元)同时取10万元为例,可能出现两人都成功取出10万元,最终账户余额变为-10万元的安全问题,造成银行亏损。
线程安全问题出现的原因
存在多个线程同时执行;
这些线程同时访问同一个共享资源;
线程对共享资源进行了修改(仅读取资源不会产生安全问题)。
模拟线程安全问题的设计思路:采用面向对象思想设计
创建账户类(Account):描述共享账户,包含卡号、余额等信息,提供取钱方法;
创建取钱线程类(DrawThread):继承Thread类,持有账户对象,重写run方法实现取钱逻辑;
创建线程对象:代表小明和小红,传入同一个账户对象并启动,模拟同时取钱场景。
模拟线程安全问题的代码实现
账户类(Account)
public class Account {
private String cardId;
private double money;
// 构造器、getter、setter方法(略)
// 取钱方法
public void drawMoney(double money) {
// 获取当前取钱的线程名称
String name = Thread.currentThread().getName();
// 判断余额是否充足
if (this.money >= money) {
System.out.println(name + "吐出" + money + "元,取钱成功");
// 更新余额
this.money -= money;
System.out.println(name + "取钱后,余额剩余:" + this.money + "元");
} else {
System.out.println(name + "取钱失败,余额不足");
}
}
}取钱线程类(DrawThread)
public class DrawThread extends Thread {
private Account account;
// 有参构造器,接收账户对象和线程名称
public DrawThread(Account account, String name) {
super(name);
this.account = account;
}
@Override
public void run() {
// 调用账户的取钱方法,取10万元
account.drawMoney(100000);
}
}主类(测试类)
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建共享账户对象,初始余额10万元
Account account = new Account("ICBC-110", 100000);
// 创建小明和小红的线程对象,传入同一个账户
new DrawThread(account, "小明").start();
new DrawThread(account, "小红").start();
}
}模拟结果及分析:代码大概率会出现线程安全问题(两人都取出10万元)。原因是两个线程可能同时判断余额充足(均为10万元),随后先后执行取钱和更新余额操作,导致账户余额异常。设计时将“更新余额”放在“吐出钱”之后,目的是给其他线程留下判断余额的时间窗口,更易模拟出问题。
五、解决线程安全问题:
线程同步的核心思想:让多个线程先后依次访问共享资源,避免线程安全问题。通过加锁机制,每次只允许一个线程加锁并访问共享资源,访问完毕后自动解锁,其他线程才能加锁进入。
同步代码块:使用synchronized关键字,将访问共享资源的核心代码块上锁。需要声明一个锁对象,该对象对于所有竞争的线程必须是同一把锁。实例方法建议用this作为锁,静态方法建议用字节码对象(类名.class)作为锁,以避免锁的范围过大影响性能。
同步方法:在方法上使用synchronized关键字,将整个方法上锁。其原理与同步代码块类似,实例方法默认用this作为锁,静态方法默认用类名.class作为锁。同步方法锁的范围更广,可读性好但性能相对较差,同步代码块锁的范围小,性能更优。
Lock锁:JDK5提供的锁定操作,通过ReentrantLock创建具体的锁对象,使用lock()方法上锁,unlock()方法解锁。建议将unlock()放在finally块中,确保无论是否出现异常都能解锁,且锁对象建议用final修饰以防止被篡改。
六、线程池的相关知识:
线程池的概念:线程池是一种可以复用线程的技术,能避免频繁创建线程导致的内存占用过多和CPU负担过重等问题。
线程池的工作原理:线程池创建后,新任务会被放入任务队列,由固定的工作线程处理,线程处理完当前任务后可处理后续任务,从而控制线程数量,实现线程复用,提高系统性能。
创建线程池的方式:
第一种是使用ExecutorService的实现类ThreadPoolExecutor,通过其有参构造器指定七个参数创建,七个参数分别是核心线程数量、最大线程数量、临时线程存活时间、时间单位、任务队列、线程工厂、任务拒绝策略。
第二种是使用工具类Executors,调用其静态方法返回不同的线程池对象(后续课程讲解)。
线程池处理Runnable任务:通过execute方法执行Runnable任务,线程池会复用线程处理任务,还介绍了关闭线程池的shutdown(等所有任务执行完毕后关闭)和shutdownNow(立即关闭,不管任务是否执行完毕)方法。
临时线程的创建时机:新任务提交时,核心线程都在忙,任务队列已满,且可以创建临时线程时,才会创建临时线程。
任务拒绝策略:当核心线程、临时线程都在忙,任务队列也满了,新任务过来时会触发拒绝策略,常见的有直接抛异常、丢弃任务不抛异常、丢弃等待最久的任务并加入新任务、由主线程执行任务等。
线程池处理Callable任务:通过submit方法提交Callable任务,会返回一个Future对象,可通过该对象获取任务执行后的结果,线程池同样会复用线程处理任务。
七、Executors工具类创建线程池
Executors工具类创建线程池
Executors是Java提供的线程池工具类,通过静态方法返回不同特点的线程池,具体如下:
newFixedThreadPool(n):返回固定线程数量的线程池,核心线程数和最大线程数均为n,无临时线程,任务队列不限制长度。
newSingleThreadExecutor():返回单个线程的线程池,仅1个核心线程,线程若因异常死亡会自动补充新线程。
newCachedThreadPool():返回可伸缩的线程池,线程数量随任务增加而增加,空闲60秒的线程会被回收。
newScheduledThreadPool(n):返回可执行定时/延时任务的线程池,核心线程数为n,属于ScheduledExecutorService类型。
Executors工具类底层原理
底层通过调用ThreadPoolExecutor的构造器(含7个核心参数)实现线程池创建,本质是对线程池创建过程的封装。
示例:newFixedThreadPool(3)的底层会将核心线程数、最大线程数均设为3,临时线程存活时间为0,任务队列采用无界链表(不限制任务数量)。
Executors工具类的风险
阿里巴巴开发手册明确不建议在大型系统中使用,主要风险包括:
newFixedThreadPool()和newSingleThreadExecutor():任务队列无界,大量任务堆积可能导致OOM(内存溢出)。
newCachedThreadPool()和newScheduledThreadPool():线程数量可无限增长,可能因线程过多导致资源耗尽。
建议:通过ThreadPoolExecutor手动配置7个参数(核心线程数、最大线程数、空闲时间等),灵活控制资源占用,小型系统可酌情使用Executors。
线程池参数配置依据
核心线程数和最大线程数的配置需结合业务类型:
计算密集型(如大量循环、数学运算):线程数建议为CPU核心数或核心数+1,减少上下文切换开销。
IO密集型(如网络请求、磁盘读写):线程数建议为CPU核心数的2倍,利用CPU等待IO的时间提升效率。
进程与线程的关系
进程:正在运行的程序,动态占用CPU、内存、网络等资源。
线程:进程内的执行单元,一个进程可包含多个线程,线程共享进程资源。
并发与并行的概念
并发:CPU通过分时轮询为多个线程服务,因切换速度极快,产生“同时执行”的错觉(如单核CPU处理多线程)。
并行:同一时刻多个线程被CPU调度执行,依赖CPU核心数(如4核CPU可并行处理4个线程)。
实际多线程执行是并发与并行的结合(如20核CPU在6800个线程中,每20个线程并行执行,整体通过并发切换处理所有线程)。
八、抢红包游戏案例
案例需求介绍
某企业100名员工(工号1-100)参与抢红包活动,需发出200个红包:
小红包(1-30元)占80%(160个),大红包(31-100元)占20%(40个)。
需模拟抢红包过程并输出详情,活动结束时提示“活动结束”,最终可对员工总金额降序排序(视频中主要完成前两步)。
开发核心思路
100名员工对应100个线程,竞争同一批红包资源(200个红包存于集合中),通过多线程同步机制保证线程安全。
关键步骤与方法实现
步骤1:生成红包集合
使用Random类生成随机金额,按比例划分小红包和大红包,存入List集合。
// 生成200个红包的方法
public static List<Integer> generateRedPackets() {
List<Integer> redPackets = new ArrayList<>();
Random random = new Random();
// 生成160个小红包(1-30元)
for (int i = 0; i < 160; i++) {
int amount = random.nextInt(30) + 1; // 1-30
redPackets.add(amount);
}
// 生成40个大红包(31-100元)
for (int i = 0; i < 40; i++) {
int amount = random.nextInt(70) + 31; // 31-100
redPackets.add(amount);
}
return redPackets;
}步骤2:定义线程类(模拟员工抢红包)
线程类实现Runnable接口或继承Thread类,通过构造器接收红包集合和员工工号,重写run()方法实现抢红包逻辑。
// 抢红包线程类
class GrabRedPacketThread extends Thread {
private List<Integer> redPackets; // 共享的红包集合
public GrabRedPacketThread(List<Integer> redPackets, String name) {
super(name); // 线程名即员工工号
this.redPackets = redPackets;
}
@Override
public void run() {
while (true) {
synchronized (redPackets) { // 同步代码块保证线程安全
if (redPackets.isEmpty()) {
System.out.println("活动结束");
break;
}
// 随机获取一个红包并移除
int index = new Random().nextInt(redPackets.size());
int amount = redPackets.remove(index);
System.out.println(Thread.currentThread().getName() + "抢到" + amount + "元");
}
// 休眠10ms模拟抢红包间隔
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}步骤3:启动线程与执行流程
在主方法中生成红包集合,创建100个线程(员工)并启动,线程竞争抢红包直至红包耗尽。
public static void main(String[] args) {
List<Integer> redPackets = generateRedPackets(); // 生成红包
// 创建100个员工线程并启动
for (int i = 1; i <= 100; i++) {
new GrabRedPacketThread(redPackets, "员工" + i).start();
}
}核心技术点
线程安全:通过synchronized同步代码块锁定红包集合,避免同一红包被重复抢夺。
多线程竞争:100个线程并发访问共享集合,模拟真实抢红包的随机性。
流程控制:通过死循环和集合判空,控制抢红包过程直至红包耗尽。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。