多线程
线程的创建:利用Callable接口、FutureTask类来实现:(项目常用)
步骤:
- 创建任务对象
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
- 把Callable类型的对象封装成FutureTask(线程任务对象)。
- 把线程任务对象交给Thread对象。
- 调用Thread对象的start方法启动线程。
- 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
FutureTask的API
优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
- 缺点:编码复杂一点。
代码:
package com.carat.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo1 {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
Callable<String> c1 = new MyCallable(100);
//4.把Callable对象封装成一个真正的线程任务对象FutureTask对象
/**
* 未来任务对象的作用?
* a.本身是一个Runnable线程任务对象,可以交给Thread线程对象处理
* b.可以获取到线程任务的执行结果
*/
//Runnable ft1 = new FutureTask<>(c1);
FutureTask<String> ft1 = new FutureTask<>(c1);
//5.把FutureTask对象交给Thread线程对象处理
Thread t1 = new Thread(ft1);
//6.启动线程
t1.start();
Callable<String> c2 = new MyCallable(50);
FutureTask<String> ft2 = new FutureTask<>(c2);
Thread t2 = new Thread(ft2);
t2.start();
//7.获取线程执行完毕后返回的结果
try {
//如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下继续执行
System.out.println(ft1.get());
} catch (Exception e) {
e.printStackTrace();
}
try {
//如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第二个线程执行完毕后,才会往下继续执行
System.out.println(ft2.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.定义一个实现类实现Callable接口
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
//2.实现call方法,定义线程执行体
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程计算1~" + n + "的和为: " + sum;
}
}
输出结果:
线程的常用方法:
Thread的常用方法
代码:
package com.carat.demo;
public class ThreadApiDemo {
public static void main(String[] args) {
//4.创建线程类的对象,代表线程
Thread t1 = new MyThread("线程1");
//t1.setName("线程1");
//5.调用start方法,启动线程,还是调用run方法执行的
t1.start();//启动线程,让线程执行run方法
System.out.println(t1.getName());//线程默认名字是: Thread-索引
Thread t2 = new MyThread("线程2");
//t2.setName("线程2");
t2.start();
System.out.println(t2.getName());//线程默认名字是: Thread-索引
//哪个线程调用这个代码,这个代码就拿到哪个线程
Thread thread = Thread.currentThread();//主线程
thread.setName("主线程");
System.out.println(thread.getName());//main
}
}
//1.定义一个子类继承Thread类,成为一个线程类
class MyThread extends Thread {
public MyThread(String name) {
super(name);//调用父类的构造方法
}
//2.重写Thread类的run方法
public void run(){
//3.在run方法中输出线程的任务代码(线程要干的活儿)
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "子线程输出: " + i);
}
}
}
输出结果:
线程安全:
认识线程安全问题
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
线程安全问题的场景:取钱:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元
代码:
测试类:
package com.carat.demosynchronized;
public class ThreadDemo {
public static void main(String[] args) {
//模拟线程安全问题
//1.设计一个账户类:用于创建小明和小红的共同账号对象,存入10万
Account acc = new Account(100000, "123456");
//2.创建一个线程类,模拟小明和小红同时取款
new DrawThread("小明", acc).start();
new DrawThread("小红", acc).start();
}
}
账户类:
package com.carat.demosynchronized;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private double money;//余额
private String cardId;//卡号
//小明和小红都到这里来取钱
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 + "取钱失败,余额不足");
}
}
}
线程类:
package com.carat.demosynchronized;
//取钱线程类
public class DrawThread extends Thread{
private Account account;//记住线程对象要处理的账户对象
public DrawThread(String name, Account account) {
super(name);
this.account = account;
}
@Override
public void run() {
//模拟小明 小红取钱
account.drawMoney(100000);
}
}
输出结果:
线程同步(解决线程安全问题)
认识线程同步
线程同步的核心思想:
让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。
线程同步的常见方案:
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
方式一:同步代码块
- 作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
synchronized(同步锁) {访问共享资源的核心代码}
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
- 同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
- 锁对象的使用规范:
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
- 对于静态方法static建议使用字节码(类名.class)对象作为锁对象。
改进代码
账户类:
package com.carat.demosynchronized;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private double money;//余额
private String cardId;//卡号
//小明和小红都到这里来取钱
public void drawMoney(double money) {
//拿到当前是谁来取钱
String name = Thread.currentThread().getName();
synchronized (this) {//同步代码块
//判断余额是否足够
if(this.money >= money){
//余额足够,取钱
System.out.println(name + "取钱成功,成功吐出了:" + money + "元");
//更新余额
this.money -= money;
System.out.println(name + "取钱后,余额为:" + this.money + "元");
}else{
//余额不足
System.out.println(name + "取钱失败,余额不足");
}
}
}
}
输出结果:
方式二:同步方法
- 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表) {操作共享资源的代码}
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
- 同步方法底层原理:
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
同步代码块好还是同步方法好?
- 范围上:同步代码块锁的范围更小,同步方法锁的范围更大
- 可读性:同步方法更好
改进代码
账户类:
package com.carat.demo1safe;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private double money;//余额
private String cardId;//卡号
//小明和小红都到这里来取钱
public synchronized 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 + "取钱失败,余额不足");
}
}
}
方式三:lock锁
- Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
- Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
Lock的常用方法:
改进代码
账户类:
package com.carat.demolock;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private double money;//余额
private String cardId;//卡号
private final Lock lk = new ReentrantLock();//创建一个锁对象,final保护锁对象不能被修改
//小明和小红都到这里来取钱
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();//解锁
}
}
}
小结:
- 锁对象建议加上什么修饰?
建议使用final修饰,防止被别人篡改 - 释放锁的操作建议放到哪里?
建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放
线程池(项目常用)
认识线程池
- 线程池就是一个可以复用线程的技术。
不使用线程池的问题 - 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
创建线程池(重点)
- JDK 5.0起提供了代表线程池的接口:ExecutorService。
方式一:通过ThreadPoolExecutor创建线程池
处理Runnable任务
ExecutorService的常用方法:
代码:
测试类:
package com.carat.demo4ExecutorService;
import java.util.concurrent.*;
public class ExecutorServiceDemo {
public static void main(String[] args) {
//目标:创建线程池对象来使用
//1.使用线程池1的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//2.使用线程池处理Runnable任务,看会不会服用线程
Runnable task = new MyRunnable();//创建runnable任务
pool.execute(task);//提交第一个任务 创建第一个线程 自动启动线程处理这个任务
pool.execute(task);//提交第二个任务 创建第二个线程 自动启动线程处理这个任务
pool.execute(task);//提交第三个任务 创建第三个线程 自动启动线程处理这个任务
pool.execute(task);//复用线程
//3.关闭线程池:一般不关闭线程池
//pool.shutdown();//等所有任务执行完毕后再关闭线程池
//pool.shutdownNow();//立即关闭,不管任务是否执行完毕
}
}
线程任务类实现Runnable接口:
package com.carat.demo4ExecutorService;
//1.定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable{
//2.重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出: " + i);
}
}
}
输出结果:
有线程复用的情况.
线程池的注意事项:
什么时候开始创建临时线程?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
任务拒绝策略
代码:
测试类:
package com.carat.demo4ExecutorService;
import java.util.concurrent.*;
public class ExecutorServiceDemo {
public static void main(String[] args) {
//目标:创建线程池对象来使用
//1.使用线程池1的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//2.使用线程池处理Runnable任务,看会不会服用线程
Runnable task = new MyRunnable();//创建runnable任务
pool.execute(task);//提交第一个任务 创建第一个线程 自动启动线程处理这个任务
pool.execute(task);//提交第二个任务 创建第二个线程 自动启动线程处理这个任务
pool.execute(task);//提交第三个任务 创建第三个线程 自动启动线程处理这个任务
pool.execute(task);
pool.execute(task);
pool.execute(task);
pool.execute(task);//到了临时线程的创建时机
pool.execute(task);//到了临时线程的创建时机
pool.execute(task);//到了任务拒绝策略,忙不过来
}
}
线程任务类实现Runnable接口:
package com.carat.demo4ExecutorService;
//1.定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable{
//2.重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出: " + i);
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
输出结果:
小结:
线程池如何处理Runnable任务?
- 使用ExecutorService的方法:
- void execute(Runnable target)
处理Callable任务
代码:
测试类:
package com.carat.demo4ExecutorService;
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//目标:创建线程池对象来使用
//1.使用线程池1的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//2.使用线程池处理Callable任务,看会不会服用线程
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));
try {
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
线程实现类实现Callable接口:
package com.carat.demo4ExecutorService;
import java.util.concurrent.Callable;
//1.定义一个线程实现类实现Callable接口
public class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
//2.实现call方法,定义线程执行体
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "计算1~" + n + "的和为: " + sum;
}
}
输出结果:
小结:
线程池如何处理Callable任务,并得到任务执行完后返回的结果?
- 使用ExecutorService的方法:
- Future
submit(Callable command)
方式二:通过Executors创建线程池
- 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。代码:
package com.carat.demo4ExecutorService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExectorsDemo {
public static void main(String[] args) {
//目标:通过线程池工具类:Executors,调用其静态方法直接得到线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));
try {
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
#### 小结:
Executors是否适合做大型互联网场景的线程池方案?
- 不合适。
- 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。
## 并发、并行
简单说说多线程是怎么执行的?
- 并发和并行同时进行的
- 并发:CPU分时轮询的执行线程。
- 并行:同一个时刻同时在执行。