目录
一、什么是多线程 ?
1. 线程与进程
2. 并发和并行
二、 多线程的实现方式
1. 多线程的第一种实现方式:Thread类
2. 多线程的第二种实现方式:Runnable接口
3. 多线程的第三种实现方式:Callable接口和Future接口
4. 多线程三种实现方式的对比
三、多线程常用的成员方法
四、线程的生命周期
五、线程安全问题
1. 同步代码块
2. 同步方法
3. lock锁
六、死锁
七、生产者和消费者 ( 等待唤醒机制 )
1. 消费者代码实现
2. 生产者代码实现
八、线程池
1. 线程池方法实现
2.自定义线程池
3.最大并行数
九、综合练习
1. 抢红包
2. 抽奖
进程是程序的基本执行实体。
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable接口和Future接口的方式进行实现
首先我们通过查找API帮助文档 了解 Thread类是什么?
多线程第一种实现方式:
- 自己手动定义一个类继承Thread类。
- 重写里面run方法。
- 创建子类对象,并启动线程。
public class ThreadDemo01 {
public static void main(String[] args) {
// 多线程第一种实现方式:
// 1.自己定义一个类继承Thread类
// 2.重写里面run方法
// 3.创建子类对象,并启动线程
myThread t1 = new myThread();
myThread t2 = new myThread();
// t1.run() 只是单纯调用一个方法
t1.setName("线程1");
t1.setName("线程2");
t1.start();
t2.start();
}
}
myThread.java
public class myThread extends Thread {
@Override
public void run() {
//书写线程执行代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "HelloWrold");
}
}
}
首先我们通过查找API帮助文档 了解 Runnable接口 是什么?
多线程第二种实现方式:
- 自己手动定义一个类去实现Runnable接口。
- 重写里面的run方法。
- 创建自己的类的对象。
- 创建一个Thread类的对象,并开启线程。
public class ThreadDemo2 {
public static void main(String[] args) {
// 多线程第二种实现方式:
// 1.自己定义一个类去实现Runnable接口
// 2.重写里面的run方法
// 3.创建自己的类的对象
// 4.创建一个Thread类的对象,并开启线程
// 创建MyRun的任务对象
MyRun mr = new MyRun();
// 创建线程对象
// 将任务mr传递给线程
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
//MyRun.java
public class MyRun implements Runnable {
@Override
public void run() {
// 书写线程执行代码
for (int i = 0; i < 100; i++) {
//获取当前线程对象
Thread thread = Thread .currentThread()
System.out.println(thread.getName() + "HelloWrold");
}
}
}
多线程第三种实现方式:
- 创建一个类MyCallable实现Callable接口。
- 重写里面的call方法。( 返回值表示多线程运行结果 )
- 创建MyCallable的对象。( 表示多线程要执行的任务 )
- 创建FutureTask的对象。( 作用管理多线程运行的结果 )
- 创建Thread类的对象,并启动线程。( 表示线程 )
特点: 可以获取到多线程运行的结果。
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 多线程第三种实现方式:
// 特点: 可以获取到多线程运行的结果
// 1.创建一个类Mycallable实现Callable接口
// 2.重写里面的call方法(返回值表示多线程运行结果)
// 3.创建MyCallable的对象(表示多线程要执行的任务)
// 4.创建FutureTask的对象(作用管理多线程运行的结果)
// 5.创建Thread类的对象,并启动(表示线程)
//创建MyCallable对象
MyCallable mc = new MyCallable();
//创建FuturaTask对象
FutureTask ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread();
t1.start();
//获取线程运行结果
Integer result = ft.get();
System.out.println(result);
}
//MyCallable.java
public class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
// 求1 ~ 100 和
int sum = 0;
for (int i = 0; i < 100; i++) {
sum = sum + i;
}
return sum;
}
}
优点 | 缺点 | |
继承Thread类 | 变成比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
实现Runnable | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
实现Callable接口 |
方法名称 | 说明 |
String getName ( ) | 返回此线程的名称 |
void setName ( String name ) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread ( ) | 获取当前线程的对象 |
static void sleep ( long time ) | 让线程休眠指定的时间,单位为毫秒 |
setPriority (int newPriority ) | 设置线程的优先级 |
final int getPriority ( ) | 获取线程的优先级 |
final void setDaemon ( boolean on ) | 设置为守护线程 |
public static void yield ( ) | 出让线程 / 礼让线程 |
public static void join ( ) | 插入线程 / 插队线程 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 1.getName 返回此线程的名称
myThread t1 = new myThread();
// 如果我们没有给线程命名,线程默认名字
// 格式:Thread-X(X序号,从0开始)
t1.start();
// 2.currentThread 获取当前线程的对象 (静态方法)
Thread t = Thread.currentThread();
// 哪条线程执行到这个方法,此时获取的就是哪条线程的对象
System.out.println(t.getName());
// 3.sleep 让线程休眠指定的时间
// 方法参数:表示睡眠时间,单位好眠
// 当时间到了之后,线程就会自动的醒来,继续执行下面的其他代码
System.out.println("1111");
Thread.sleep(5000);
System.out.println("2222");
}
}
线程的优先级:
- 抢占式调度:CPU执行每一条的线程的时机和执行时间都是不确定的。
- 非抢占式调度:所有的线程轮流进行,执行时间是差不多的。
public class ThreadDemo {
public static void main(String[] args) {
//创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t1 = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"坦克");
//优先级默认 : 5
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
System.out.println(Thread.currentThread().getPriority());
//细节:当其他的非守护线程执行完毕之后,守护线程将会陆续结束。
// 把第二个线程设置为守护线程
t2.setDaemon(true);
}
}
问: sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
- 答:不会。sleep方法结束后会进入就绪状态,抢到CPU执行权才会运行下面的代码。
线程的六大状态:
新建状态(New) | 创建线程对象 |
就绪状态(RUNNABLE ) | start方法 |
阻塞状态(BLOCKED ) | 无法获得锁对象 |
等待状态( WAITING ) | wait方法 |
计时等待(TIMED_WAITING ) | sleep方法 |
结束状态( TERMINATED ) | 全部代码运行完毕 |
通过小练习了解线程安全:
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//线程命名
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
//MyThread.java
public class MyThread extends Thread {
// 表示这个类的对象都共享一个ticket对象
static int ticket = 0;
@Override
public void run() {
// 书写线程执行代码
while (true) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
我们发现会出现三个窗口卖同样的票或者超出票数的问题:
那么我们该如何解决呢?
synchronized:
(1)可重入性
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;
synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。(2)不可中断性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断
- 特点1 : 锁默认打开,有一个线程进去了,锁自动关闭。
- 特点2 :里面的代码全部执行完毕,线程出来,锁自动打开。
public class MyThread extends Thread {
// 表示这个类的对象都共享一个ticket对象
static int ticket = 0;
// 锁对象一定唯一
static Object obj = new Object();
@Override
public void run() {
// 书写线程执行代码
while (true) {
//锁对象是任意的
synchronized(obj) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
}
- 特点1 : 同步方法是锁住方法里面的所有代码
- 特点2 : 锁对象不能自己指定。
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
// 1.循环
while (true) {
// 2.同步代码块(同步方法)
if (method()) {
break;
}
}
}
//this
public synchronized boolean method() {
// 3.判断共享数据是否到了末尾 如果到了末尾
if (ticket == 100) {
return true;
// 4.判断共享数据是否到了末尾 如果没到末尾
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
虽然我们可以理解同步代码块和同步方法的锁对象问题,
但是我们并没有直接地看到在哪里加上了锁以及在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock中提供了获得锁和释放锁的方法:
成员方法 | 说明 |
void lock ( ) | 获得锁 |
void unlock ( ) | 释放锁 |
Lock是接口不能直接实例化,这里采用它的实现类 ReentrantLock 实例化。
构造方法 | 说明 |
ReentrantLock ( ) | 创建一个 ReentrantLock 的实例 |
public class MyRunnable implements Runnable {
int ticket = 0;
// 多个对象共享同一个锁
static Lock lock = new ReentrantLock();
@Override
public void run() {
// 1.循环
while (true) {
// 2.同步代码块(同步方法)
// synchronized (MyThread.class) {
lock.lock();
try {
// 3.判断共享数据是否到了末尾 如果到了末尾
if (ticket == 100) {
break;
// 4.判断共享数据是否到了末尾 如果没到末尾
} else {
Thread.sleep(100);
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
//MyThread.java
public class MyThread extends Thread {
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
// 1.循环
while (true) {
if ("线程A".equals(getName())) {
synchronized (objA) {
System.out.println("线程A拿到了A锁,准备拿B锁");
synchronized (objB) {
System.out.println("线程A拿到了B锁,顺利执行完一轮");
}
}
} else if ("线程B".equals(getName())) {
if ("线程B".equals(getName())) {
synchronized (objB) {
System.out.println("线程B拿到了B锁,准备拿A锁");
synchronized (objA) {
System.out.println("线程B拿到了A锁,顺利执行完一轮");
}
}
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程A");
t2.setName("线程B");
t1.start();
t2.start();
}
}
运行结果:(卡死)
生产者消费者模式是一种非常经典的多线程协作的模式。
常见方法:
成员方法 | 说明 |
void wait ( ) | 当前线程等待,直到被其他线程唤醒 |
void notify ( ) | 所及唤醒单个线程 |
void notifyAll ( ) | 唤醒所有线程 |
举例:
//Desk.java
public class Desk {
// 作用: 控制生产者和消费者的执行
//判断桌子上是否有面条: 0:没有 ; 1:有
public static int foodFlag = 0;
//定义总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
//Foodie.java
public class Foodie extends Thread {
@Override
public void run() {
// 1.循环
while (true) {
// 同步代码块
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 先判断桌子上是否有面条
if (Desk.foodFlag == 0) {
// 没有:等待
try {
Desk.lock.wait(); // 让当前线程与锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 把吃的总数- 1
Desk.count--;
// 有: 开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗");
// 吃完之后:唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 创建线程对象
Cook c = new Cook();
Foodie f = new Foodie();
// 线程命名
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
//Cook.java
public class Cook extends Thread{
@Override
public void run() {
// 1.循环
while (true) {
// 同步代码块
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 判断桌子上是否有食物
if (Desk.foodFlag == 1) {
// 如果有:就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 没有: 就制作食物
System.out.println("厨师做了一碗面条");
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
以前写多线程的弊端:
弊端一:用到线程的时候就要创建 | 弊端二:用完之后线程消失 |
因此,我们我们引入线程池:
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。
- 创建一个池子,池子中是空的。
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子;下次再次提交任务时,不需要创建新的的线程,直接复用已有的线程即可。
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
public static ExecutorService newCachedThreadPool ( ) | 创建一个没有上限的线程池 |
public static ExecutorService newFixedThreadPool ( int nThreads ) | 创建有上限的线程池 |
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
ExecutorService pool2 = Executors.newFixedThreadPool(3);
// 2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
// pool2只能看到3个线程
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
// 3.销毁任务
pool1.shutdown();
}
}
//MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " - - " + i);
}
}
}
任务拒绝策略 | 说明 |
ThreadPoolExecutor.AbortPolicy | 默认策略:丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常这是不推荐的做法 |
ThreadPoolExecutor.DiscardoldestPolicy | 抛弃队列中等待最久的任务然后把当前任务加入队列中 |
ThreadPoolExecutor.callerRunsPolicy | 调用任务的run()方法绕过线程池直接执行 |
- 核心元素一:核心线程的数量(不能小于0)
- 核心元素二:线程池中最大线程的数量(最大数量>=核心线程数量)
- 核心元素三:空闲时间(值)(不能小于0)
- 核心元素四:空闲时间(单位)(用TimeUnit指定)
- 核心元素五:堵塞队列(不能为null)
- 核心元素六:创建线程的方式(不能为null)
- 核心元素七:要执行的任务过多时的解决方案(不能为null)
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor pool1 = new ThreadPoolExecutor(3, // 核心线程数量,不能小于0
6, // 最大线程数,不能小于0,最大数量 >= 核心线程数量
60, // 空间线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(3), // 任务队列
Executors.defaultThreadFactory(), // 创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
);
}
}
不断的提交任务,会有以下三个临界点:
- 当核心线程满时,再提交任务就会排队。
- 当核心线程满,队伍满时,会创建临时线程。
- 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略。
CPU密集型运算 (读取文件操作比较少) |
|
I/O密集型运算 (读取文件操作比较多) |
public class MyThreadPoolDemo {
public static void main(String[] args) {
//向Java虚拟机返回可用处理器的数目
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count); //12
}
}
所以线程池多大合适呢?
示例:(4线8核通过计算公式)
抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。其中,红包是共享数据。
5个人是5条线程。
打印结果如下:
XXX抢到了XXX元XXX抢到了XXX元
XXX抢到了XXX元
XXX没抢到
XXX没抢到
public class MyThread extends Thread {
// 总金额
static BigDecimal money = BigDecimal.valueOf(100.0);
// 个数
static int count = 6;
// 最小抽奖金额
static final BigDecimal MIN = BigDecimal.valueOf(0.01);
@Override
public void run() {
synchronized (MyThread.class) {
if (count == 0) {
System.out.println(getName() + "没有抢到红包");
}else {
//中奖金额
BigDecimal prize;
if (count == 1) {
prize = count;
}else {
//获取抽奖范围
double bounds = money.subtract(BigDecimal.valueOf(count -1).multiply(MIN).doubleValue());
Random r = new Random();
//抽奖金额
prize = BigDecimal.valueOf(r.nextDouble()bounds);
}
//设置抽中红包,小数点保留两位,四舍五入
prize = prize.setScale(2,RoundingMode.HALF_UP);
//在总金额中去掉对应的钱
money = money.subtract(prize);
//红包少了一个
count--;
//输出红包信息
System.out.println(getName() + "抽中了" + prize + "元");
}
}
}
}
//Test.java
public class Test {
public static void main(String[] args) {
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
MyThread t5 = new MyThread();
MyThread t6 = new MyThread();
// 线程命名
t1.setName("张三");
t2.setName("李四");
t3.setName("王五");
t4.setName("赵六");
t5.setName("钱七");
t6.setName("孙八");
// 线程启动
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽出一个奖项就打印一个(随机)
抽奖箱1又产生了一个10元大奖抽奖箱1又产生了一个100元大奖
抽奖箱1又产生了一个200元大奖
抽奖箱1又产生了一个800元大奖
抽奖箱2又产生了一个700元大奖
...
public class MyThread extends Thread {
ArrayList list;
public MyThread(ArrayList list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
break;
} else {
// 继续抽奖
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Collections.shuffle(list);
int prize = list.remove(0);
System.out.println(getName() + "又产生了一个" + prize + "元大奖");
}
}
}
}
}
//Test.java
public class Test {
public static void main(String[] args) {
//创建奖池
ArrayList list = new ArrayList<>();
Collections.addAll(list, 10,5,20,50,100,200,500,800,2,80,300,700);
// 创建线程对象
MyThread t1 = new MyThread(list);
MyThread t2 = new MyThread(list);
// 线程命名
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
// 线程启动
t1.start();
t2.start();
}
}