1.线程概述
1.1线程
- 线程(Thread)是一个程序的一条执行路径;
- 启动程序后,main方法的执行就是一条单独的执行路径;
- 程序如果只有一条执行路径,便是单线程程序
1.2.多线程
从软硬件上实现多条执行流程的技术;
2.创建线程的方法
2.1多线程实现方案一:继承Thread
/**
* 多线程实现方案一:继承Thread
* 重写run(),start()启动
* 优点:代码简单
* 缺点:无法继承其他类,不利于扩展
*/
public class ThreadDemo01 {
public static void main(String[] args) {
Thread thread = new MyThread();
//子线程必须放在主线程前,不然就认为是单一线程,主线程必然先跑
//启动线程;
thread.start();
for (int i = 0; i < 3; i++) {
System.out.println("主线程:"+i);
}
//每次线程执行的先后不一定
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程:"+i);
}
}
}
2.2多线程实现方案二:实现Runnable
/**
* 多线程实现方案二:实现Runnable
* 重写run(),创建一个任务对象交给Thread启动
* 优点:可以继承其他类和实现接口,利于扩展
* 缺点:多一层对象包装,无法直接返回值
*/
public class ThreadDemo02 {
public static void main(String[] args) {
//创建一个任务对象
MyRunnable myRunnable = new MyRunnable();
//任务对象交给Thread
Thread thread = new Thread(myRunnable);
//启动
thread.start();
for (int i = 0; i < 3; i++) {
System.out.println("主线程:"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程:"+i);
}
}
}
2.3线程实现方案二:Runnable(匿名内部类)
/**
* 多线程实现方案二:Runnable(匿名内部类)
* 语法形式 重写run()
* 优点:进一步简化
* 缺点:无法直接返回值,不适合需要返回执行结果的业务场景
*/
public class ThreadDemo02Other {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程:"+i);
}
}
};
Thread thread = new Thread(runnable);
thread.start();
//简化写法
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程2:"+i);
}
}
}).start();
//简化写法2(lambda表达式)
new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("子线程3:"+i);
}
}
).start();
for (int i = 0; i < 3; i++) {
System.out.println("主线程:"+i);
}
}
}
2.4多线程实现方案三:实现Callable接口,结合FutureTask完成
/**
* 多线程实现方案三:实现Callable接口,结合FutureTask完成
* 语法形式:
* 重写call(),创建创建Callable任务对象
* Callable任务对象交给FutureTask对象,转交给Thread
* 优点:扩展性强,可以得到线程执行结果
* 缺点:编程相对复杂
*/
public class ThreadDemo03 {
public static void main(String[] args) {
//1.创建Callable任务对象
MyCallable call = new MyCallable(10);
//2.Callable任务对象交给FutureTask对象
//FutureTask对象的作用1:是Runnable对象(实现Runnable接口),可以转交给Thread
//FutureTask对象的作用2:线程执行结束后,调用get方法得到返回结果
FutureTask<String> futureTask = new FutureTask<>(call);
Thread thread = new Thread(futureTask);
thread.start();
MyCallable call2 = new MyCallable(20);
FutureTask<String> futureTask2 = new FutureTask<>(call2);
Thread thread2 = new Thread(futureTask2);
thread2.start();
try {
//如果第一个子线程没执行完,会在此等待,直到拿到结果,继续顺序执行
String s = futureTask.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
try {
String s = futureTask2.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//泛型设置返回对象类型
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
//重写call()任务方法
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum+=i;
}
return "子线程的和为"+sum;
}
}
3.线程常用API
/**
* 线程常用API
*/
public class ThreadDemo01 {
public static void main(String[] args) {
Thread t1 = new MyThread();
//设置线程名
t1.setName("子线程1号");
System.out.println(t1.getName());
t1.start();
Thread t2 = new MyThread("子线程2号");
System.out.println(t2.getName());
t2.start();
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"输出:"+i);
if (i == 1){
try {
//休眠3s
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//每次线程执行的先后不一定
}
}
class MyThread extends Thread{
public MyThread() {
}
//设置命名构造器
public MyThread(String name) {
//为当前线程对象命名,送给父类有参构造器初始化名称
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
//获得线程名
System.out.println(Thread.currentThread().getName()+"输出:"+i);
}
}
}
4.线程安全
4.1线程安全概述
多个线程同时操作同一个资源,可能出现业务安全问题;
4.2 安全案例:取款
/**
* 安全案例:取款
*/
public class ThreadTest {
public static void main(String[] args) {
Account ace = new Account("ACE000", 100000);
new DrawThread("明",ace).start();
new DrawThread("红",ace).start();
/**
* 红取钱成功,突出:100000.0
* 账号剩余:0.0
* 明取钱成功,突出:100000.0
* 账号剩余:-100000.0
*/
}
}
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);
}
}
public class Account {
private String cardId;
private double money;
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
//取钱方法
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("账号剩余:"+this.money);
}else {
System.out.println(name+"取钱失败,余额不足");
}
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
5.线程同步
5.1 线程同步核心思想
- 加锁:
- 共享资源上锁,每次只能一个线程访问,结束后解锁,其他线程竞争再进一个;
- 达到多线程先后依次访问共享资源
5.2 同步代码块
-
作用:有线程安全问题的核心代码上锁;
-
原理:每次一个线程进入
-
synchronized(同步锁对象){ 使用共享资源的核心代码 } -
锁对象要求:对于同时执行的线程来说是同一个对象;
- 理论上可以选用任意的唯一对象(如常量),但不好会影响无关线程;
- 建议使用共享资源作为锁对象
- 实例方法建议使用this
- 静态方法建议用字节码(类名.class)对象作为锁对象
/**
* 静态方法建议用字节码(类名.class)对象作为锁对象
*/
public static void run(){
synchronized (Account.class) {
}
}
/**
* this == acc 共享账号
* 取钱方法
*/
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("账号剩余:"+this.money);
}else {
System.out.println(name+"取钱失败,余额不足");
}
}
}
/**
* 不好,会影响无关线程;
*/
//取钱方法
public void drawMoney(double money){
String name = Thread.currentThread().getName();
synchronized ("任意唯一对象:如常量") {
if (this.money >= money){
System.out.println(name+"取钱成功,突出:"+money);
this.money -= money;
System.out.println("账号剩余:"+this.money);
}else {
System.out.println(name+"取钱失败,余额不足");
}
}
}
5.3 同步方法
-
作用:有线程安全问题的核心方法上锁;
-
格式:
修饰符 synchronized 返回值类型 方法名称(形参列表){ 使用共享资源的核心代码 } -
同步方法底层原理:
- 底层有隐式锁对象的,锁的范围是整个方法
- 实例方法,默认用this作为锁对象,前提是代码要高度面向对象
- 静态方法:默认用字节码(类名.class)对象作为锁对象
5.4 同步代码块和同步方法比较
- 同步代码块:效率更高点
- 同步方法:更简洁明了,用的更多
5.5 Lock锁
- 为了更清晰表达加锁和释放锁,JDK5后增加了历新的对象Lock更灵活和方便;
- Lock可以提供更广泛的锁定操作;
- Lock是接口,采用它的实现类ReentrantLock()构建锁对象
public class AccountLock {
private String cardId;
private double money;
private final Lock lock = new ReentrantLock();
/**
* Lock锁更灵活
*/
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
//上锁
lock.lock();
try {
if (this.money >= money) {
System.out.println(name + "取钱成功,突出:" + money);
this.money -= money;
System.out.println("账号剩余:" + this.money);
} else {
System.out.println(name + "取钱失败,余额不足");
}
} finally {
//解锁
lock.unlock();
}
}
6.线程通信
6.1 线程通信概念
- 线程间互相发送数据,通常通过共享一个数据的方式实现;
- 线程间根据数据情况,判断执行声明,以及通知其他线程怎么做
- 线程通信前提:多个线程操作同一个共享资源时进行通信,且要保证线程安全
6.2 线程通信常见模型
- 生产者消费者模型:生产者生产数据,消费者消费数据
- 要求:
- 生产者生产完数据,唤醒消费者,等待自己;
- 消费者消费完,唤醒生产者,等待自己
6.3 案例:生产者消费者模型
| 方法名 | 作用 |
|---|---|
| void wait() | 让活动在当前对象的线程无限等待(释放之前占有的锁) |
| void notify() | 唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁) |
| void notifyAll() | 唤醒当前对象全部正在等待的线程(只提示唤醒,不会释放锁) |
/**
* 案例:生产者消费者模型
*/
public class ThreadDemo {
public static void main(String[] args) {
Account account = new Account("wind998", 0);
//两人取钱
new DrawThread("婷姐",account).start();
new DrawThread("小火柴",account).start();
//三人存钱
new DepositThread("婷哥",account).start();
new DepositThread("天机星",account).start();
new DepositThread("地魁星",account).start();
}
}
/**
* 取钱线程
*/
public class DrawThread extends Thread{
private Account account;
public DrawThread(String name, Account account) {
super(name);
this.account = account;
}
@Override
public void run() {
while (true) {
account.drawMoney(10000);
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 存钱线程
*/
public class DepositThread extends Thread{
private Account account;
public DepositThread(String name, Account account) {
super(name);
this.account = account;
}
@Override
public void run() {
while (true) {
account.deposit(10000);
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 实体和方法
*/
public class Account {
private String cardId;
private double money;
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public synchronized void drawMoney(double money) {
String name = Thread.currentThread().getName();
try {
if (this.money >= money) {
System.out.println(name+"取钱成功,取出:"+money);
this.money -= money;
System.out.println("账号剩余:"+this.money);
//唤起其他线程
this.notifyAll();
//必须用锁对象让线程挂起
this.wait();
} else {
//钱不够,花钱线程挂起,唤起,存钱线程
//注意先后顺序,先唤醒再挂起,不然无法后继
//唤起其他线程
this.notifyAll();
//必须用锁对象让线程挂起
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void deposit(double money) {
String name = Thread.currentThread().getName();
try {
if (this.money == 0) {
this.money += money;
System.out.println(name+"存钱成功,存入:"+money+"账号剩余:"+this.money);
//唤起其他线程
this.notifyAll();
//必须用锁对象让线程挂起
this.wait();
} else {
//钱够
//唤起其他线程
this.notifyAll();
//必须用锁对象让线程挂起
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
7.线程池
7.1 概述
线程池是一个可以复用线程的技术;
7.1.1使用线程池的原因
创建新线程开销很大,每个请求都创建新线程处理,严重影响系统性能
7.1.2 线程池工作原理
- 设置复数的核心线程(工作线程);
- 任务请求进入任务队列(WorkQueue)排队等待,最大可执行数量为核心线程的数量
- 任务接口
- Runnable;Callable
- 假设:有三台售货机,最多同时有三个人上前操作,其余人排队等待;
7.1.3 线程池代表接口
JDK5提供了代表线程池的接口:ExceutorService
7.1.4 得到线程池对象方法
- 方式一:
ExceutorService实现类ThreadPoolExecutor自创建一个线程池对象; - 方式二:Executors(线程池工具类)调用方法返回不同特点的线程池对象
7.1.5 构造器参数说明
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
| 参数 | 释义 | 注意 |
|---|---|---|
corePoolSize | 指定线程池的线程数量(核心线程) | 不能小于0; 需要随时应对派遣任务; |
maximumPoolSize | 指定线程池可支持的最大线程数 | 最大数量 >= 核心线程数量 |
keepAliveTime | 指定临时线程最大存活时间 | 不能小于0 临时线程 = 最大数量- 核心线程数量 |
| unit | 指定存活时间的单位(天、时、分、秒) | 时间单位 |
workQueue | 指定任务队列 | 不能为null 内部按需求设置可以等待的任务量 |
threadFactory | 指定用某线程工厂创建线程 | 不能null |
| handler | 拒绝任务的策略 | 不能为null 所有线程都忙,任务队列满后的拒绝新任务的方式 |
7.1.6 新任务拒绝策略
| 策略 | 意义 |
|---|---|
| ThreadPoolExecutor.AbortPolicy() | 丢弃任务并抛出异常,默认策略 |
| ThreadPoolExecutor.DiscardPolicy() | 丢弃任务,不抛异常,不推荐 |
| ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃队列中等待最久的任务,把当前任务加入队列中 |
| ThreadPoolExecutor.CallerRunsPolicy() | 由主线程(main老板直接服务)负责调用任务的run()方法,绕过线程池直接执行; |
7.2 常见面试题
7.2.1 临时线程什么时候创建
新任务提交时,核心线程都在忙,任务队列满了,还有临时线程创建名额,此时才会创建临时线程;
7.2.2 什么时候会拒绝任务?
所有线程都忙,任务队列满了,新任来了才会拒绝;
7.3 线程池处理Runnable任务
/**
* 线程池执行Runnable任务
*/
public class ThreadPoolDemo01 {
public static void main(String[] args) {
/**
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,销毁临时线程必须等线程不被占用开始计算
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,6
, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),new ThreadPoolExecutor.AbortPolicy());
MyRunnable target = new MyRunnable();
//pool.execute()执行任务,没有返回值,一般用来执行Runnable任务
pool.execute(target);
pool.execute(target);
pool.execute(target);
//到此,三个核心线程都工作
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
//上述存入等待队列
pool.execute(target);
pool.execute(target);
//上述创建临时线程
/**
* 所有线程都被占用,等待队列满了
* 新的11号任务来了,需要执行拒绝策略
* Task com.yanfeng.d8_threadpool.MyRunnable@6d6f6e28 rejected
* from java.util.concurrent.ThreadPoolExecutor@135fbaa4
*/
pool.execute(target);
/**
* 关闭线程(开发中一般不用)
*/
//立即关闭,即使线程没处理完,会丢失任务
pool.shutdownNow();
//全部任务执行完后,关闭(建议用这个)
pool.shutdown();
}
}
7.4线程池处理Callable任务
/**
* 线程池执行Callable任务
* 线程池开始执行,程序不会自行关闭
*/
public class ThreadPoolDemo02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,销毁临时线程必须等线程不被占用开始计算
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,6
, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),new ThreadPoolExecutor.AbortPolicy());
//submit()方法处理Callable任务,返回结果,有未来任务对象接收,用时从中取
Future<String> submit = pool.submit(new MyCallable(100));
Future<String> submit2 = pool.submit(new MyCallable(200));
Future<String> submit3 = pool.submit(new MyCallable(300));
Future<String> submit4 = pool.submit(new MyCallable(400));
String s = submit.get();
System.out.println(s);
System.out.println(submit2.get());
System.out.println(submit3.get());
System.out.println(submit4.get());
}
}
7.5 Executors工具类实现线程池
注意:Executors底层基于线程池的实现类ThreadPoolExecutor创建线程池对象
/**
* Executors工具类实现线程池
* 大型系统不允许使用Executors创建线程
*/
public class ThreadPoolDemo03 {
public static void main(String[] args) {
/**
* newFixedThreadPool
* newSingleThreadExecutor
* 存在问题:
* 没有任务限制,没有拒绝策略,任务队列无限排,会出现内存溢出
*/
//创建固定线程的线程池(全核心线程);某线程因异常结束,会创建新线程补上
ExecutorService executorService = Executors.newFixedThreadPool(3);
//只有一个固定线程,因异常结束,会创建新线程补上
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
/**
* newCachedThreadPool 线程无限增加,会出现内存溢出
* newScheduledThreadPool
* 存在问题:
* 没有控制任务数量,
*/
//来了任务直接创建线程
ExecutorService executorService2 = Executors.newCachedThreadPool();
//核心线程数量控制,但最大线程数量不定
ExecutorService executorService3 = Executors.newScheduledThreadPool(3);
}
}
8.定时器
- 定时器是一种控制任务延时调用,或者周期调用的技术;
- 作用:闹钟、定时邮件发送;
8.1 Timer定时器
| 特点 | Timer是单线程,处理多个任务顺序执行,存在延时与设置定时器的时间有出入; 到了设定执行时间,前面任务还没执行完,就会延后执行 |
|---|---|
| 存在问题 | 可能因某个任务异常是Timer线程死掉,影响后续任务执行 |
8.2 ScheduledExecutorService定时器
- ScheduledExecutorService是JDK1.5中引入并发包,目的是弥补Timer的缺陷,ScheduledExecutorService内部为线程池;
- 优点:基于线程池,某个任务执行情况不会影响其他定时任务执行;
/**
* ScheduledExecutorService定时器
* 定时任务间互不影响
*/
public class TimerDemo02 {
public static void main(String[] args) {
//1.创建ScheduledExecutorService线程池做定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
/**
* ScheduledFuture<?> scheduleAtFixedRate
* (Runnable command,long initialDelay,long period,TimeUnit unit);
* (任务,延时?后执行,隔?后重复执行,时间单位)
*/
//2.开启定时任务;周期调度方法
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行A:"+new Date());
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行B:"+new Date());
System.out.println(10/0);
}
},0,2, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行C:"+new Date());
}
},0,2, TimeUnit.SECONDS);
}
}
9.并发和并行
- 正在运行的程序是一个独立进程,线程属于进程,
- 多个进程其实是并发与并行同时进行;
- 并发是一次执行一个,再换下一个
- 并行是有多个在一起执行,可以看做执行一组线程
- 并发与并行同时进行;一次执行一组,再换下一组
9.1并发
- CPU同时处理线程的数量有限;
- CPU会轮询为系统每个线程服务,由于CPU切换速度很快,感觉线程在同时执行,这便是并发;
9.2 并行
- 同一时刻,同时有多个线程在被CPU处理;
- 依靠CPU逻辑处理器
9.3 并发和并行含义总结
- 并发:CPU分时轮询的执行线程
- 并行:同一时刻同时执行
10.线程的生命周期
10.1 Java线程的6中状态
10.2 注意:
| 注意 | |
|---|---|
| sleep | 不会让出自己的锁,时间到了,直接运行 |
| wait | 会让出锁,时间到,抢锁,抢到运行; 会让出锁,被唤醒后需要抢锁,抢到运行; |
RecordDate:2021/08/20