目录
一、什么是多线程 ?
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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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,"坦克");//优先级默认 : 5System.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;@Overridepublic void run() {// 书写线程执行代码while (true) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}
}
我们发现会出现三个窗口卖同样的票或者超出票数的问题:
那么我们该如何解决呢?
- 特点1 : 锁默认打开,有一个线程进去了,锁自动关闭。
- 特点2 :里面的代码全部执行完毕,线程出来,锁自动打开。
public class MyThread extends Thread {// 表示这个类的对象都共享一个ticket对象static int ticket = 0;// 锁对象一定唯一static Object obj = new Object();@Overridepublic void run() {// 书写线程执行代码while (true) {//锁对象是任意的synchronized(obj) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}}
}
- 特点1 : 同步方法是锁住方法里面的所有代码
- 特点2 : 锁对象不能自己指定。
public class MyRunnable implements Runnable {int ticket = 0;@Overridepublic void run() {// 1.循环while (true) {// 2.同步代码块(同步方法)if (method()) {break;}}}//thispublic synchronized boolean method() {// 3.判断共享数据是否到了末尾 如果到了末尾if (ticket == 100) {return true;// 4.判断共享数据是否到了末尾 如果没到末尾} else {try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.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();@Overridepublic 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();@Overridepublic 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 {@Overridepublic 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 {// 把吃的总数- 1Desk.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{@Overridepublic 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 {@Overridepublic 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, // 核心线程数量,不能小于06, // 最大线程数,不能小于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);@Overridepublic 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;}@Overridepublic 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();}
}