目录
1.概念相关
1.1什么是线程
1.2什么是多线程
2.创建线程
2.1方式一:继承Thread类
2.1.1实现步骤
2.1.2优缺点
2.1.3注意事项
2.2方式二:实现Runnable接口
2.2.1实现步骤
2.2.2优缺点
2.2.3匿名内部类写法
2.3方式三:实现callable接口
2.3.1callable接口解决了什么问题
2.3.2实现步骤
2.3.3FutureTask的API
2.3.4优缺点
3.线程的常用方法
4.线程安全
4.1什么是线程安全
4.2线程安全问题出现的原因
4.3模拟线程安全问题场景 :取钱
5.线程同步
5.1什么是线程同步
5.2线程同步的核心思想
5.3常见方案
5.4方式一:同步代码块
5.4.1作用:
5.4.2原理:
5.4.3注意事项:
5.4.4锁对象的使用规范
5.4.5如何实现线程安全的
5.5方式二:同步方法
5.5.1作用
5.5.2原理
5.5.3底层原理
5.5.4同步代码块和同步方法哪种好
5.6lock锁
5.6.1lock锁是什么
5.6.2lock锁的构造器、常用方法
5.6.3锁对象建议加上什么修饰
5.6.4释放锁的操作建议放到哪里
6.线程池
6.1认识线程池
6.2不使用线程池的后果
6.3创建线程池
6.4任务拒绝策略
6.5线程池的注意事项
6.6处理runnable任务
6.4.1ExecutorService的常用方法
6.7处理callable
6.8通过Executors创建线程池
6.8.1方法
6.8.2Executors使用可能存在的陷阱
7.并发、并行
7.1进程
7.2并发的含义
7.3并行的含义
1.概念相关
1.1什么是线程
线程(Thread)是一个程序内部的一条执行流程。程序中如果只有一条执行流程,那这个程序就是单线程的程序。
1.2什么是多线程
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
2.创建线程
2.1方式一:继承Thread类
2.1.1实现步骤
【1】定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
【2】创建MyThread类的对象
【3】调用线程对象的start()方法启动线程(启动后还是执行run方法的)
2.1.2优缺点
【1】优点:编码简单
【2】缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
2.1.3注意事项
【1】启动线程必须是调用start方法,不是调用run方法。
【2】不要把主线程任务放在启动子线程之前。
public class Test {
public static void main(String[] args) {
Thread t1=new MyThread();
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 10; i++){
System.out.println("子线程" + i);
}
}
}
2.2方式二:实现Runnable接口
2.2.1实现步骤
【1】定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
【2】创建MyRunnable任务对象
【3】把MyRunnable任务对象交给Thread处理。
【4】调用线程对象的start()方法启动线程
2.2.2优缺点
【1】优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
【2】缺点:需要多一个Runnable对象。
2.2.3匿名内部类写法
【1】可以创建Runnable的匿名内部类对象。
【2】再交给Thread线程对象。
【3】再调用线程对象的start()启动线程。
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+i);
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("子线程2"+i);
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程"+i);
}
}
2.3方式三:实现callable接口
2.3.1callable接口解决了什么问题
假如线程执行完毕后有一些数据需要返回,前两种重写的run方法均不能直接返回结果。使用Callable接口和FutureTask类来实现创建,可以返回线程执行完毕后的结果。
2.3.2实现步骤
【1】定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
【2】把Callable类型的对象封装成FutureTask(线程任务对象)。
【3】把线程任务对象交给Thread对象。
【4】调用Thread对象的start方法启动线程。
【5】线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
2.3.3FutureTask的API
FutureTask提供的构造器 | 说明 |
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
FutureTask提供的方法 | 说明 |
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
2.3.4优缺点
【1】优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
【2】缺点:编码复杂一点。
public class ThreadDemo3 {
public static void main(String[] args) {
// 目标:掌握多线程的创建方式三:实现Callable接口,方式三的优势:可以获取线程执行完毕后的结果的。
// 3、创建一个Callable接口的实现类对象。
Callable<String> c1 = new MyCallable(100);
// 4、把Callable对象封装成一个真正的线程任务对象FutureTask对象。
/**
* 未来任务对象的作用?
* a、本质是一个Runnable线程任务对象,可以交给Thread线程对象处理。
* b、可以获取线程执行完毕后的结果。
*/
FutureTask<String> f1 = new FutureTask<>(c1); // public FutureTask(Callable<V> callable)
// 5、把FutureTask对象作为参数传递给Thread线程对象。
Thread t1 = new Thread(f1);
// 6、启动线程。
t1.start();
Callable<String> c2 = new MyCallable(50);
FutureTask<String> f2 = new FutureTask<>(c2); // public FutureTask(Callable<V> callable)
Thread t2 = new Thread(f2);
t2.start();
// 获取线程执行完毕后返回的结果
try {
// 如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
System.out.println(f1.get());
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
System.out.println(f2.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方法,定义线程执行体
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程计算1-" + n + "的和是:" + sum;
}
}
3.线程的常用方法
Tread提供的常用方法 | 说明 |
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()... | 让调用当前这个方法的线程先执行完! |
public class ThreadApiDemo1 {
public static void main(String[] args) {
// 目标:搞清楚线程的常用方法。
Thread t1 = new MyThread("1号线程");
// t1.setName("1号线程");
t1.start();
System.out.println(t1.getName()); // 线程默认名称是:Thread-索引
Thread t2 = new MyThread("2号线程");
// t2.setName("2号线程");
t2.start();
System.out.println(t2.getName()); // 线程默认名称是:Thread-索引
// 哪个线程调用这个代码,这个代码就拿到哪个线程
Thread m = Thread.currentThread(); // 主线程
m.setName("主线程");
System.out.println(m.getName()); // main
}
}
// 1、定义一个子类继承Thread类,成为一个线程类。
class MyThread extends Thread {
public MyThread(String name) {
super(name); // public Thread(String name)
}
// 2、重写Thread类的run方法
@Override
public void run() {
// 3、在run方法中编写线程的任务代码(线程要干的活儿)
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);
}
}
}
public static void main(String[] args) {
// 目标:搞清楚Thread类的Sleep方法(线程休眠)
for (int i = 1; i <= 10; i++) {
System.out.println(i);
try {
// 让当前执行的线程进入休眠状态,直到时间到了,才会继续执行。
// 项目经理让我加上这行代码,如果用户交钱了,我就注释掉。
Thread.sleep(1000); // 1000ms = 1s
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ThreadApiDemo3 {
public static void main(String[] args) {
// 目标:搞清楚线程的join方法:线程插队:让调用这个方法线程先执行完毕。
MyThread2 t1 = new MyThread2();
t1.start();
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() +"线程输出:" + i);
if(i == 1){
try {
t1.join(); // 插队 让t1线程先执行完毕,然后继续执行主线程
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);
}
}
}
4.线程安全
4.1什么是线程安全
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
4.2线程安全问题出现的原因
【1】存在多个线程在同时执行
【2】同时访问一个共享资源
【3】存在修改该共享资源
4.3模拟线程安全问题场景 :取钱
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private String cardId; // 卡号
private double money; // 余额
// 小明和小红都到这里来了取钱
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 + "取钱失败,余额不足");
}
}
}
// 取钱线程类
public class DrawThread extends Thread{
private Account acc; // 记住线程对象要处理的账户对象。
public DrawThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 小明 小红 取钱
acc.drawMoney(100000);
}
}
public static void main(String[] args) {
// 目标:模拟线程安全问题。
// 1、设计一个账户类:用于创建小明和小红的共同账户对象,存入10万。
Account acc = new Account("ICBC-110", 100000);
// 2、设计线程类:创建小明和小红两个线程,模拟小明和小红同时去同一个账户取款10万。
new DrawThread("小明", acc).start();
new DrawThread("小红", acc).start();
}
5.线程同步
5.1什么是线程同步
线程同步是线程安全问题的解决方案。
5.2线程同步的核心思想
让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。
5.3常见方案
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
5.4方式一:同步代码块
5.4.1作用:
把访问共享资源的核心代码给上锁,以此保证线程安全。
5.4.2原理:
每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
5.4.3注意事项:
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
5.4.4锁对象的使用规范
【1】建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
【2】对于静态方法建议使用字节码(类名.class)对象作为锁对象。
5.4.5如何实现线程安全的
【1】对出现问题的核心代码使用synchronized进行加锁
【2】每次只能一个线程占锁进入访问
// 小明和小红都到这里来了取钱
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 + "取钱失败,余额不足");
}
}
}
5.5方式二:同步方法
5.5.1作用
把访问共享资源的核心方法给上锁,以此保证线程安全。
5.5.2原理
每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
5.5.3底层原理
【1】同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
【2】如果方法是实例方法:同步方法默认用this作为的锁对象。
【3】如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
5.5.4同步代码块和同步方法哪种好
同步代码块锁的范围更小,同步方法锁的范围更大。同步方法可读性更好
// 小明和小红都到这里来了取钱
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 + "取钱失败,余额不足");
}
}
5.6lock锁
5.6.1lock锁是什么
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
5.6.2lock锁的构造器、常用方法
构造器 | 说明 |
public ReentrantLock() | 获得Lock锁的实现类对象 |
方法名称 | 说明 |
void lock() | 获得锁 |
void unlock() | 释放锁 |
5.6.3锁对象建议加上什么修饰
建议使用final修饰,防止被别人篡改
5.6.4释放锁的操作建议放到哪里
建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放
private final Lock lk = new ReentrantLock(); // 保护锁对象
// 小明和小红都到这里来了取钱
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();// 解锁
}
}
6.线程池
6.1认识线程池
线程池就是一个可以复用线程的技术。
6.2不使用线程池的后果
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的, 创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
6.3创建线程池
通过ThreadPoolExecutor创建线程池。使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
【1】参数一:corePoolSize : 指定线程池的核心线程的数量。
【2】参数二:maximumPoolSize:指定线程池的最大线程数量。
【3】参数三:keepAliveTime :指定临时线程的存活时间。
【4】参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
【5】参数五:workQueue:指定线程池的任务队列。
【6】参数六:threadFactory:指定线程池的线程工厂。
【7】参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
6.4任务拒绝策略
策略 | 说明 |
ThreadPoolExecutor.AbortPolicy() | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor. DiscardPolicy() | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor. DiscardOldestPolicy() | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor. CallerRunsPolicy() | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
6.5线程池的注意事项
// 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();
}
}
}
}
public static void main(String[] args) {
// 目标:创建线程池对象来使用。
// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。
ExecutorService pool = new ThreadPoolExecutor(3, 5,
10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
// 2、使用线程池处理任务!看会不会复用线程?
Runnable target = new MyRunnable();
pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务
pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务
pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target); // 到了临时线程的创建时机了
pool.execute(target); // 到了临时线程的创建时机了
pool.execute(target); // 到了任务拒绝策略了,忙不过来
// 3、关闭线程池 :一般不关闭线程池。
// pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!
// pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!
}
6.6处理runnable任务
6.4.1ExecutorService的常用方法
方法名称 | 说明 |
void execute(Runnable command) | 执行 Runnable 任务 |
Future<T> submit(Callable<T> task) | 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
List<Runnable> shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
// 2、使用线程池处理任务!看会不会复用线程?
Runnable target = new MyRunnable();
pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务
pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务
pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target); // 到了临时线程的创建时机了
pool.execute(target); // 到了临时线程的创建时机了
pool.execute(target); // 到了任务拒绝策略了,忙不过来
// 3、关闭线程池 :一般不关闭线程池。
pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!
pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!
6.7处理callable
线程池如何处理Callable任务,并得到任务执行完后返回的结果?
Future<T> submit(Callable<T> command)
6.8通过Executors创建线程池
6.8.1方法
是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
方法名称 | 说明 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
6.8.2Executors使用可能存在的陷阱
【1】大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
【2】不适合做大型互联网场景的线程池方案
【3】建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。
7.并发、并行
7.1进程
【1】正在运行的程序(软件)就是一个独立的进程。
【2】线程是属于进程的,一个进程中可以同时运行很多个线程。
【3】进程中的多个线程其实是并发和并行执行的。
7.2并发的含义
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
7.3并行的含义
在同一个时刻上,同时有多个线程在被CPU调度执行。