目录
1、线程概述
1.1 什么是线程?
1.2 什么是多线程?
2、多线程的创建
2.1 继承Thread类
2.2 实现Runnable接口
2.3 实现Callable接口
2.4 总结
3、Thread的常用方法
3.1 区分线程
1、给线程设置名称
2、得到线程名称
3、得到当前线程对象,然后再获取名称
4、Thread的构造器
3.2 休眠方法
3.3 总结
4、线程安全
4.1 取钱模型演示
4.2 总结
5、线程同步
5.1 同步代码块
5.2 同步方法
5.3 Lock锁
6、线程池*
6.1 线程池概述
6.2 线程池处理Runnable任务
6.3 线程池处理Callable任务
6.4 Executors工具类实现线程池
6.5 总结
7、定时器
7.1 实现方式一:Timer
7.2 方式二: ScheduledExecutorService
8、并发、并行
9、线程生命周期
首先我们要知道进程是系统进行资源分配和调度的基本单位,而线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
3.程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
1、多线程是指从软硬件上实现多条执行路径的技术。
2、多线程用在哪里,有什么好处?
例如铁路12306购票系统。
例如过年回家抢票,不可能只有你一个人在买票,那每个人进来的时候都要有一个执行路径,那这个之后就需要用到多线程。
Java是通过java.lang.Thread 类来代表线程的。
按照面向对象的思想,Thread类应该提供了实现多线程的方式。
实现过程:
定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法。
创建MyThread类的对象。
调用线程对象的start()方法启动线程(启动后还是执行run方法的)。
代码实现:
package com.jie.multithreading;
/**
* @description:多线程创建方式
* @author: jie
* @time: 2022/3/20 9:30
*/
public class ThreadDemo01 {
public static void main(String[] args) {
//3、new一个新线程对象
Thread t = new MyThread();
//4、调用start方法启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出"+i);
}
}
}
/**
* @description:1、定义一个线程类继承Thread
* @author: jie
* @time: 2022/3/20 9:32
*/
class MyThread extends Thread {
/**
* @description:2、重写run方法,里面定义线程以后要干啥
* @author: jie
* @time: 2022/3/20 12:26
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出" + i);
}
}
}
执行结果:
优缺点:
优点:编码简单 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
问题:
1、为什么不直接调用了run方法,而是调用start启动线程。
如果直接调用run方法,执行结果如下
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。 只有调用start方法才是启动一个新的线程执行。
2、把主线程任务放在子线程之前了。
这样主线程一直是先跑完的,相当于是一个单线程的效果了。
小结:
1、继承Thread是如何实现多线程的?
继承Thread类
重写run方法
创建线程对象
调用start()方法启动。
2、优缺点是什么?
优点:编码简单
缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展。
实现过程:
定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
创建MyRunnable任务对象。
把MyRunnable任务对象交给Thread处理。
调用线程对象的start()方法启动线程。
代码实现:
package com.jie.multithreading;
/**
* @description:多线程创建方式2
* @author: jie
* @time: 2022/3/20 12:50
*/
public class ThreadDemo02 {
public static void main(String[] args) {
//3、创建一个任务对象
Runnable target = new MyRunnable();
//4、把任务对象交给Thread处理
Thread t = new Thread(target);
//5、启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
* @description:1、定义一个线程类实现Runnable接口
* @author: jie
* @time: 2022/3/20 12:53
*/
class MyRunnable implements Runnable {
/**
* @description:2、重写run方法,定义线程执行任务的
* @author: jie
* @time: 2022/3/20 12:51
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
执行结果:
优缺点:
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
小结:
1、实现Runnable接口方式是如何创建线程的?
定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
创建MyRunnable对象。
把MyRunnable任务对象交给Thread线程对象处理。
调用线程对象的start()方法启动线程。
2、第二种方式的优点。
优点:线程任务类只是实现了Runnale接口,可以继续继承和实现。
缺点:如果线程有执行结果是不能直接返回的。
1、前2种线程创建方式都存在一个问题:
他们重写的run方法均不能直接返回结果。
不适合需要返回线程执行结果的业务场景。
2、怎么解决这个问题呢?
JDK 5.0以后提供了Callable和FutureTask来实现,这种方式的优点是:可以得到线程执行的结果。
实现过程:
得到任务对象 1、定义类实现Callable接口,重写call方法,封装要做的事情。 2、用FutureTask把Callable对象封装成线程任务对象。
把线程任务对象交给Thread处理。
调用Thread的start方法启动线程,执行任务
线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
代码实现:
package com.jie.multithreading;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @description:创建线程方式3,实现Callable,结合FutureTask完成
* @author: jie
* @time: 2022/3/20 14:02
*/
public class ThreadDemo03 {
public static void main(String[] args) {
//3、创建Callble任务对象
Callable call = new Mycallable(100);
//4、把Callable任务对象,交给FutureTask对象
//FutureTask对象的作用1 是Runnable的对象(实现了Runnable接口),可以交给Thread了
//FutureTask对象的作用2 可以在线程执行完毕之后通过调用其Get方法的到线程执行完成的结果
FutureTask f1 = new FutureTask<>(call);
//5、交给线程处理
Thread t1 = new Thread(f1);
//6、启动线程
t1.start();
Callable call1 = new Mycallable(200);
FutureTask f2= new FutureTask<>(call1);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果
String s = f1.get();
System.out.println("第一个结果:"+s);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果
String s1 = f2.get();
System.out.println("第二个结果:"+s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @description:1、定义一个任务类,实现callable接口, 应该声明线程任务执行完毕后的结果的数据类型
* @author: jie
* @time: 2022/3/20 14:04
*/
class Mycallable implements Callable {
private int n;
public Mycallable(int n) {
this.n = n;
}
/**
* @description:2、重写call(任务方法)
* @author: jie
* @time: 2022/3/20 14:06
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
执行结果:
FutureTask的API :
优缺点:
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。 可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
Thread t = new MyThread();
//线程取名
t.setName("1号");
Thread t = new MyThread();
t.getName()
Thread m = Thread.currentThread();
System.out.println(m.getName());
代码实现:
那这个休眠有何用?不知道大家有没有在网上看到一个段子,项目经理然我加上这段代码,如果用户愿意交钱,就注释掉......
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。 ②:需要定义一个线程类,线程类可以处理账户对象。 ③:创建2个线程对象,传入同一个账户对象。 ④:启动2个线程,去同一个账户对象中取钱10万。
代码实现:
账户类:
package com.jie.thread;
import java.math.BigDecimal;
/**
* @description:账户类
* @author: jie
* @time: 2022/3/20 16:29
*/
public class Account {
private String cardId;
private BigDecimal money;
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public BigDecimal getMoney() {
return money;
}
public void setMoney(BigDecimal money) {
this.money = money;
}
/**
* @description:取钱
* @author: jie
* @time: 2022/3/20 16:47
*/
public void drawMooney(BigDecimal money){
//1、先获取是谁来取钱,线程的名字就是人名
String name = Thread.currentThread().getName();
//2、判断账户是否够钱
if(this.money.compareTo(money)!=-1){
//2.取钱
System.out.println(name + "来取钱成功,吐出"+money);
//3.更新余额
BigDecimal subtract = this.money.subtract(money);
this.money = subtract;
System.out.println(name + "取钱后剩余"+this.money);
}else{
//余额不足
System.out.println(name+"来取钱,余额不足");
}
}
public Account(){
}
public Account(String cardId, BigDecimal money) {
this.cardId = cardId;
this.money = money;
}
}
实现类:
package com.jie.thread;
import java.math.BigDecimal;
/**
* @description:模拟取钱案例
* @author: jie
* @time: 2022/3/20 16:28
*/
public class ThreadDemo {
public static void main(String[] args) {
//1、定义线程类,创建一个账户对象
Account account = new Account("jie-1111", BigDecimal.valueOf(100000.0));
//2、创建两个线程对象,代表小明和小红同时进来了。
new DrawThread(account,"小明").start();
new DrawThread(account,"小红").start();
}
}
线程类:
package com.jie.thread;
import java.math.BigDecimal;
/**
* @description:线程类(取钱)
* @author: jie
* @time: 2022/3/20 16:39
*/
public class DrawThread extends Thread{
/**接收处理的账户对象*/
private Account account;
public DrawThread(Account account,String name){
super(name);
this.account = account;
}
@Override
public void run() {
//小明,小红 取钱
account.drawMooney(BigDecimal.valueOf(100000));
}
}
线程安全问题发生的原因是什么?
多个线程同时访问同一个共享资源且存在修改该资源。
线程同步就是为了解决线程安全问题。
1、取钱案例出现问题的原因?
多个线程同时执行,发现账户都是够钱的。
2、如何才能保证线程安全呢? 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的核心思想:
加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
代码实现:
小结:
1、同步代码块是如何实现线程安全的?
对出现问题的核心代码使用synchronized进行加锁
每次只能一个线程占锁进入访问
2、 同步代码块的同步锁对象有什么要求?
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
代码实现:
执行结果:
同步方法底层原理:
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
1、如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
2、如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
是同步代码块好还是同步方法好一点?
同步代码块锁的范围更小,同步方法锁的范围更大。
但是在实际开发中,同步方法或许会比同步代码块用得更多一点。因为写法方便。
小结:
1、同步方法是如何保证线程安全的?
对出现问题的核心方法使用synchronized修饰
每次只能一个线程占锁进入访问
2、同步方法的同步锁对象的原理?
对于实例方法默认使用this作为锁对象。
对于静态方法默认使用类名.class对象作为锁对象。
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
执行结果:
线程池是一个可以复用线程的技术。
不使用线程池的问题
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
谁代表线程池?
JDK 5.0起提供了代表线程池的接口:ExecutorService
线程类:
package com.jie.threadpool;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"输出了:"+i);
}
}
}
测试:
package com.jie.threadpool;
import java.lang.annotation.Target;
import java.util.concurrent.*;
/**
* @description:自定义一个线程池对象,测试其新特性
* @author: jie
* @time: 2022/3/20 20:31
*/
public class ThreadPoolDemo01 {
public static void main(String[] args) {
//创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//2、给任务线程池处理
MyRunnable runnable = new MyRunnable();
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
}
}
ThreadPoolExecutor构造器的参数说明
参数一:指定线程池的线程数量(核心线程): corePoolSize 注:不能小于0
参数二:指定线程池可支持的最大线程数: maximumPoolSize 注:最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime 注:不能小于0
参数四:指定存活时间的单位(秒、分、时、天): unit 注:时间单位
参数五:指定任务队列: workQueue 注:不能为null
参数六:指定用哪个线程工厂创建线程: threadFactory 注:不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler 注:不能为null
线程池常见面试题:
临时线程什么时候创建啊?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
线程类:
package com.jie.threadpool;
import java.util.concurrent.Callable;
/**
* @description:1、定义一个任务类,实现callable接口, 应该声明线程任务执行完毕后的结果的数据类型
* @author: jie
* @time: 2022/3/20 14:04
*/
public class Mycallable implements Callable {
private int n;
public Mycallable(int n) {
this.n = n;
}
/**
* @description:2、重写call(任务方法)
* @author: jie
* @time: 2022/3/20 14:06
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "执行1 - " + n + "的和,结果 = " + sum;
}
}
测试:
package com.jie.threadpool;
import java.util.concurrent.*;
/**
* @description:自定义一个线程池对象,测试其新特性
* @author: jie
* @time: 2022/3/20 20:31
*/
public class ThreadPoolDemo02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//2、给任务线程池处理
Future f1 = pool.submit(new Mycallable(100));
Future f2 = pool.submit(new Mycallable(200));
Future f3 = pool.submit(new Mycallable(300));
Future f4 = pool.submit(new Mycallable(400));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
测试结果:
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
注:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
代码实现:
package com.jie.threadpool;
import com.sun.jndi.ldap.pool.Pool;
import java.util.concurrent.*;
/**
* @description:使用Executors的工具方法直接得到一个线程池对象
* @author: jie
* @time: 2022/3/20 20:31
*/
public class ThreadPoolDemo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1、创建固定线程数据的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(new MyRunnable());
pool.execute(new MyRunnable());
pool.execute(new MyRunnable());
}
}
Executors使用可能存在的陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
1、ExecutorService的常用方法
3、线程池如何处理Runnable任务。
使用ExecutorService的方法:
void execute(Runnable target)
4、线程池如何处理Callable任务,并得到任务执行完后返回的结果。
使用ExecutorService的方法:
Future
submit(Callable command)
5、Executors工具类底层是基于什么方式实现的线程池对象?
线程池ExecutorService的实现类:ThreadPoolExecutor
6、Executors是否适合做大型互联网场景的线程池方案?
不合适。
建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。
概述
定时器是一种控制任务延时调用,或者周期调用的技术。 作用:闹钟、定时邮件发送。
实现方式
方式一:Timer 方式二: ScheduledExecutorService
代码实现:
package com.jie.timer;
import java.util.Timer;
import java.util.TimerTask;
/**
* @description:Timer定时器的使用
* @author: jie
* @time: 2022/3/20 22:21
*/
public class TimerDemo01 {
public static void main(String[] args) {
//1、创建Timer定时器 定时器本身就是一个单线程
Timer timer = new Timer();
//2、调用方法、处理定时任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行一次");
}
},3000,2000);
}
}
Timer定时器的特点和存在的问题
1、Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
ScheduledExecutorService是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。
package com.jie.timer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @description:ScheduledExecutorService定时器的使用
* @author: jie
* @time: 2022/3/20 22:21
*/
public class TimerDemo02 {
public static void main(String[] args) {
//1、创建ScheduledExecutorService线程池做定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//2、开启定时任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行输出:AAA");
}
},0,2, TimeUnit.SECONDS);
}
}
ScheduledExecutorService的优点
基于线程池,某个任务的执行情况不会影响其他定时任务的执行。
正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。
并发的理解
CPU同时处理线程的数量有限。
CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解
正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。
小结:
并发:CPU分时轮询的执行线程。
并行:同一个时刻同时在执行。
线程的状态
就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
Java总共定义了6种状态
Java总共定义了6种状态
1、 新建(New),
2、运行(Runnable),
3、阻塞(Blocked),
4、等待(Waiting),
5、计时等待(Time_Waiting),
6、终止(Terminated)。
6种状态都定义在Thread类的内部枚举类中。