多线程
简介:
- 概述:进程有多条执行路径, 统称为:多线程.
- 进程: 指的是可执行程序,文件。例如: *.exe
- 大白话:进程 是 汽车
- 线程: 指的是进程的执行单元, 执行路径
- 大白话: 线程 是 车道
- 进程: 指的是可执行程序,文件。例如: *.exe
多线程 并行 和 **并发 **的区别是什么?
- 多线程并行:
指的是两个或者以上的线程同时执行. 前提:需要多核CPU. - 多线程并发:
指的是两个或者以上的线程同时请求执行,但是同一瞬间CPU只能执行一个,于是就安排它们(多个线程)交替执行,又因为时间间隔非常短,我们看起来好像是同时执行的,其实不是。
多线程并行和并发的区别 图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vj5RTdNe-1669233017783)(F:\BigData\java\图片\day13图片\多线程\多线程并行和并发的区别图解.png)]
多线程的两种实现方式:
- 记忆的话
- 一台电脑上可以有多个进程,这些进程之间的数据是相互 隔离 的
- 一个进程可以有多个线程,这些线程 共享进程的数据
- 开启线程调用的是 Thread#start() 方法,如果调用 Thread#run()方法,只是普通的方法调用,并没有开启线程
- 同一个线程对象重复开启,会报
IllegalThreadStateException
(非法的线程状态异常)
多线程 程序执行流 图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2B1aA6HV-1669233017785)(F:\BigData\java\图片\day13图片\多线程\单线程程序执行流图.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpRXus7L-1669233017785)(F:\BigData\java\图片\day13图片\多线程\多线程程序执行流图.png)]
方式一:继承Thread类
1. 自定义一个类(例如: MyThread, **线程类**),继承**Thread类**
2. 重写**Thread类**中的**run()方法**
3. 把**要执行的代码写到run()方法中**
4. 在**测试类中创建线程对象**
5. 开启线程
演示多线程的实现方式一: 继承Thread类
package com.itheima.demo01_thread;
//自定义的线程类, 用来输出一些内容.
//1. 定义一个类(MyThread), 继承Thread类.
public class MyThread extends Thread{
//2. 重写Thread#run()方法.
@Override
public void run() {
//3. 把要执行的代码放到run()方法中.
for (int i = 0; i < 100; i++) {
System.out.println("run...................." + i);
}
}
}
package com.itheima.demo01_thread;
//案例: 演示多线程的实现方式一: 继承Thread类.
public class Demo02 {
public static void main(String[] args) {
//需求2: 打印100次 run, 通过自定义的线程类实现.
//1. 创建线程对象.
MyThread mt = new MyThread();
//2. 开启线程.
//mt.run(); //这样写不会报错, 但是只是普通的方法调用而已, 并没有开启线程.
mt.start();
//mt.start(); //同一线程不能重复开启, 否则会报 IllegalThreadStateException(非法的线程状态异常).
//需求1: 打印100次 main
for (int i = 0; i < 100; i++) {
System.out.println("main..." + i);
}
//System.out 标准的输出流, 用来往控制台输出内容的.
//System.err 标准的错误流, 用来打印异常信息的.
}
}
方式二:实现Runnable接口
- 自定义一个类(例如: MyRunnable, 资源类), 实现Runnable接口
- 重写Runnable接口中的run()方法
- 把要执行的代码写到run()方法中
- 在测试类中: 创建资源类对象, 并将其作为参数传给Thread类的构造, 从而创建线程对象
- 开启线程
演示多线程的实现方式二: 实现Runnable接口
package com.itheima.demo02_runnable;
//1. 定义一个类(MyRunnable), 实现Runnable接口
public class MyRunnable implements Runnable {
//2. 重写Runnable#run()方法.
@Override
public void run() {
//3. 把要执行的代码放到run()方法中.
for (int i = 0; i < 100; i++) {
System.out.println("run...................." + i);
}
}
}
package com.itheima.demo02_runnable;
//案例: 演示多线程的实现方式二: 实现Runnable接口.
public class Demo01 {
public static void main(String[] args) {
//4. 创建Runnable接口的子类对象, 并将其作为参数传递给Thread类, 创建线程对象.
//4.1 创建Runnable接口的子类对象
MyRunnable mr = new MyRunnable();
//4.2 并将其作为参数传递给Thread类, 创建线程对象.
Thread th = new Thread(mr);
//Lambda表达式版本实现.
/*Thread th = new Thread(() -> {
//3. 把要执行的代码放到run()方法中.
for (int i = 0; i < 100; i++) {
System.out.println("run...................." + i);
}
});*/
//5. 开启线程.
th.start();
//需求1: 打印100次 main
for (int i = 0; i < 100; i++) {
System.out.println("main..." + i);
}
}
}
匿名内部类的方式实现多线程
package com.itheima.demo02_runnable;
//案例: 通过匿名内部类的方式实现多线程.
public class Demo02 {
public static void main(String[] args) {
//方式一: 继承Thread类.
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("传智播客.... 继承Thread类");
}
}
}.start();
//方式二: 实现Runnable接口, 匿名内部类. 采用面向对象的思想实现.
//格式: new Thread(Runnable接口的子类对象).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("黑马程序员.... 实现Runnable接口");
}
}
}).start();
//方式三: 实现Runnable接口, Lambda表达式, 采用函数式编程思想实现.
//因为Lambda表达式这种方式没有显示的重写run()方法, 所以导致 通过start()方法开启线程的时候,
//找不到对应的run()方法, 然后当做普通的方法调用了, 偶尔会看见抢资源的情况, 但是几率较小.
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("博学谷.... Lambda表达式");
}
}).start();
}
}
Thread类中的成员方法总结
构造方法
- public Thread();
- public Thread(String name); 指定线程的名字
- public Thread(Runnable target);
- public Thread(Runnable target, String name); 执行线程的名字
成员方法
- public void run(); 里边写的是该线程要执行的代码
- public void start(); 开启线程
- public String getName(); 获取当前正在执行的线程对象的名字.
- public void setName(String name); 设置线程名
- public static void sleep(long time) 让线程休眠指定时间, 单位是毫秒.
- public static Thread currentThread() 返回当前正在执行的线程对象的引用.
- public int getPriority(); 获取线程的优先级.
- public void setPriority(int i); 设置优先级.
- public void join(); 加入线程, 即: 相当于插队, 也叫: 等待这个线程死亡.
- public void setDaemon(boolean flag) 设置线程的类别, true: 守护线程, false: 非守护线程(默认)
多线程模拟卖票_继承Tread类的方式
出现的问题:
出现负数票和重复值
问题原因:
负数票:
当ticket的值为1的时候,此时不管哪个线程抢到资源,都会越过if判断,然后停留在休眠线程这里,而休眠线程的特点是:在那里睡,到点就在那里醒来,此时四个线程依次醒来,开始抢资源,就会出现如下的情况:
假设窗口1先抢到资源,此时会打印:窗口1正在售卖第1张票,之后执行ticket–,最终ticket = 0
假设窗口2先抢到资源,此时会打印:窗口2正在售卖第0张票,之后执行ticket–,最终ticket = -1
假设窗口3先抢到资源,此时会打印:窗口3正在售卖第-1张票,之后执行ticket–,最终ticket = -2
假设窗口4先抢到资源,此时会打印:窗口4正在售卖第-2张票,之后执行ticket–,最终ticket = -3
重复值:
它的出现和ticket–这行代码相关,它等价于:ticket = ticket - 1 ,这行代码做了3件事
1.读取ticket的值
2.修改ticket的值,即:-1
3.重新赋值
当某个窗口打印完票数之后,还没有来得及执行重新赋值的时候,被别的线程抢走了资源,就会抢走重复值
解决方案:
采用同步代码块解决,即:加锁的思想,让某一个线程在卖票的过程中,不会受到其他线程的影响。
package com.itheima.demo03_tickets;
//自定义的线程类MyThread, 用来实现模拟卖票
public class MyThread extends Thread {
//1. 定义变量, 记录票数.
private static int tickets = 100; //细节1: 这里要加static, 以为4个窗口(线程)共享100张票.
//定义构造方法, 用来给线程设置名字.
public MyThread() {
}
public MyThread(String name) {
super(name);
}
//2. 在run()方法中实现模拟卖票
@Override
public void run() {
//3.一直卖票, 没有票, 就不卖了.
while(true) {
// 没有票, 就不卖了.
if (tickets <= 0) { //线程1,线程2 线程3, 线程4
break;
}
//细节2: 加入休眠线程, 让程序出错的概率大一些.
try {
Thread.sleep(50); //单位是毫秒, 该方法的特点是: 在哪里睡, 到点后就在哪里醒来. 线程1,线程2 线程3, 线程4
} catch (InterruptedException e) {
e.printStackTrace();
}
//走到这里, 说明有票, 卖票即可
System.out.println(getName() + " 售卖出第 "+ tickets-- +" 张票"); //细节3: 这里要加入线程名, 即: 那个窗口卖的票.
}
}
}
package com.itheima.demo03_tickets;
//案例: 模拟卖票, 100张票, 由4个窗口售卖.
public class Demo {
public static void main(String[] args) {
//1. 创建4个窗口, 即: 4个线程.
MyThread mt1 = new MyThread("窗口1");
//mt1.setName("窗口1");
MyThread mt2 = new MyThread("窗口2");
MyThread mt3 = new MyThread("窗口3");
MyThread mt4 = new MyThread("窗口4");
//2. 开启线程, 卖票即可.
mt1.start();
mt2.start();
mt3.start();
mt4.start();
}
}
线程安全
线程同步
- 概述
- 多线程环境 并发 操作同一数据, 就有可能引发安全问题, 此时就需要通过同步思想来解决
同步代码块:
-
格式:
-
synchronized(锁对象) { //要加锁的代码 }
-
-
细节:
- 同步代码块的锁对象可以是任意类型的对象.
- 必须使用同一把锁, 否则可能出现锁不住的情况.:
模拟卖票_问题解决
package com.itheima.demo04_synchronized;
//自定义的线程类MyThread, 用来实现模拟卖票
public class MyThread extends Thread {
//1. 定义变量, 记录票数.
private static int tickets = 100; //细节1: 这里要加static, 以为4个窗口(线程)共享100张票.
//模拟锁对象
/*private static Object obj = new Object();
private static String s = new String();*/
//定义构造方法, 用来给线程设置名字.
public MyThread() {
}
public MyThread(String name) {
super(name);
}
//2. 在run()方法中实现模拟卖票
@Override
public void run() {
//3.一直卖票, 没有票, 就不卖了.
while(true) {
//************** 一次完整的卖票动作从这里开始..
synchronized (MyThread.class) { //实际开发中,我们一般用本类的字节码文件对象,当锁对象
// 没有票, 就不卖了.
if (tickets <= 0) { //线程1,线程2 线程3, 线程4
break;
}
//细节2: 加入休眠线程, 让程序出错的概率大一些.
try {
Thread.sleep(50); //单位是毫秒, 该方法的特点是: 在哪里睡, 到点后就在哪里醒来. 线程1,线程2 线程3, 线程4
} catch (InterruptedException e) {
e.printStackTrace();
}
//走到这里, 说明有票, 卖票即可
System.out.println(getName() + " 售卖出第 "+ tickets-- +" 张票"); //细节3: 这里要加入线程名, 即: 那个窗口卖的票.
}
//************** 一次完整的卖票动作到这里结束.
}
}
}
public class Demo {
public static void main(String[] args) {
//1. 创建4个窗口, 即: 4个线程.
MyThread mt1 = new MyThread("窗口1");
//mt1.setName("窗口1");
MyThread mt2 = new MyThread("窗口2");
MyThread mt3 = new MyThread("窗口3");
MyThread mt4 = new MyThread("窗口4");
//2. 开启线程, 卖票即可.
mt1.start();
mt2.start();
mt3.start();
mt4.start();
}
}
模拟卖票_实现Runnable接口版
package com.ithiema.api.demo;
//自定义的MyRunnable接口的子类对象, 充当: 资源对象.
public class MyRunnable implements Runnable {
private int ticket = 100; //细节1:这里要不要加static?可以不加
//细节2:这里能不能用Thread类的构造方法? 不能,因为该类和Thread类没有任何关系
//3.重写Runnable接口
@Override
public void run() {
//循环卖票
while (true) {
//**********一次完整的卖票从这里开始**********
//synchronized (MyRunnable.class) { //可以用当前的字节码文件对象
synchronized (this) { //细节4:锁对象可以用this吗? 可以
//越界处理
if (ticket <= 0)
break;
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
//正常卖票逻辑
//细节3:加入线程名, 即: 那个窗口卖的票,这里能不能直接调用Thread#getName()方法? 不能,因为该类和Thread类没有任何关系
System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticket-- + "张票");
}
//**********一次完整的卖票从这里结束**********
}
}
}
public class Demo02 {
public static void main(String[] args) {
//创建资源类对象 细节5:能不能直接创建线程对象? 不能,因为MyRunnable是资源类,不是线程类
MyRunnable mr = new MyRunnable();
//创建线程类对象
Thread th1 = new Thread(mr,"窗口1");
Thread th2 = new Thread(mr,"窗口2");
Thread th3 = new Thread(mr,"窗口3");
Thread th4 = new Thread(mr,"窗口4");
th1.start();
th2.start();
th3.start();
th4.start();
}
}
多线程的两种实现方式之间的区别是什么?
- 继承Thread类:
好处: 代码相对比较简单. //因为是继承Thread类, 所以可以直接用Thread类中的成员.
弊端: 扩展性相对较差. //因为已经继承了Thread类了, 所以就不能继承其他类了. - 实现Runnable接口:
好处: 扩展性相对较强. //因为是实现接口, 所以不影响类的体系.
弊端: 代码相对比较繁琐. //因为不是直接继承Thread类, 所以不能直接使用Thread类中的成员.
同步方法:
- 格式:
- 就是在定义方法的时候, 在方法的声明上(返回值类型之前)加上 synchronized 即可.
- 分类:
静态成员方法: 锁对象是该类的字节码文件对象.
非静态成员方法: 锁对象是 this
- 结论:
- 如果方法中部分的内容要加锁, 就用: 同步代码块.
- 如果方法中所有的内容都要加锁, 就用: 同步方法.
package com.ithiema.api.demo;
public class Demo03 {
public static void main(String[] args) {
}
//方法中部分代码块加锁,用同步代码块
public static void show1() {
System.out.println("start");
//部分内容加锁
//synchronized (锁对象){
synchronized (Demo01.class) {
System.out.println("Hello world!1");
System.out.println("Hello world!2");
System.out.println("Hello world!3");
}
System.out.println("end");
}
//静态方法,内容全部加锁,锁对象默认是:该类的字节码文件对象,即:Demo01.class
public static synchronized void show2() {
System.out.println("Hello world!1");
System.out.println("Hello world!2");
System.out.println("Hello world!3");
}
//非静态方法,内容全部加锁,锁对象默认是:this
public synchronized void show3() {
System.out.println("Hello world!1");
System.out.println("Hello world!2");
System.out.println("Hello world!3");
}
}
死锁
注意:该内容只在面试用,实际开发不用,所谓的死锁指的就是同步代码块的嵌套
细节:
- 死锁需要两个线程,两把锁
- 一个线程先抢A锁,后抢B锁
- 另一个线程先抢B锁,后抢A锁,就有可能会出现死锁的情况
- 为了让效果更明显,用while(true)改进
演示死锁的代码块
package com.ithiema.api.demo;
public class DeadLock {
//1.定义两把锁
private static final String LOCKA = "锁A";
private static final String LOCKB = "锁B";
public static void main(String[] args) {
//2.一个线程先抢A锁,后抢B锁
//继承Thread类的方式创建线程对象,格式:new Thread(){//重写run()方法}.start();
new Thread("李四") {
@Override
public void run() {
//为了效果更明显,用while(true)改进
while (true) {
synchronized (LOCKA) {
System.out.println(getName() + "获取到" + LOCKA + "等到" + LOCKB);
synchronized (LOCKB) {
System.out.println(getName() + "抢到" + LOCKB + "成功进到小黑屋");
}
}
}
}
}.start();
//3.一个线程先抢B锁,后抢A锁
//继承Thread类的方式创建线程对象,格式:new Thread(){//重写run()方法}.start();
new Thread("张三") {
@Override
public void run() {
//为了效果更明显,用while(true)改进
while (true) {
synchronized (LOCKB) {
System.out.println(getName() + "获取到" + LOCKB + "等到" + LOCKA);
synchronized (LOCKA) {
System.out.println(getName() + "抢到" + LOCKA + "成功进到小黑屋");
}
}
}
}
}.start();
//实现Runnable接口,格式:new Thread(Runnable接口的子类对象,线程名).start();
new Thread(new Runnable() {
@Override
public void run() {
//为了效果更明显,用while(true)改进
while (true) {
synchronized (LOCKB) {
System.out.println(Thread.currentThread().getName() + "获取到" + LOCKB + "等到" + LOCKA);
synchronized (LOCKA) {
System.out.println(Thread.currentThread().getName() + "抢到" + LOCKA + "成功进到小黑屋");
}
}
}
}
}).start();
}
}
多线程的生命周期详解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzSRoYJ4-1669233017786)(F:\BigData\java\图片\day13图片\02. 多线程的生命周期.png)]
请简述下多线程的生命周期是什么?
-
入门版:
- 新建, 就绪, 运行(可能会发生阻塞), 死亡.
-
进阶版:
- 新建, 就绪, 运行(可能会发生阻塞或者等待), 死亡.
阻塞:多指IO流阻塞, 大多数人为不可控.
等待:多指**sleep(), wait()**之类的, 人为可控.
- 新建, 就绪, 运行(可能会发生阻塞或者等待), 死亡.
线程进阶
线程优先级
- 结论:
- 目前我们遇到的 线程安全的类 有: StringBuffer, Vector, Hashtable
- 线程的调度模型有两种: 1. 分时调度模型. 2. 抢占式调度模型. Java采用的是第二种.
- Java中多线程程序的执行特点是:随机性和延迟性,原因是因为CPU在做着高效的切换。
- 线程的优先级默认为5,范围是:1-10,1最小,10最大。
优先级越高代表着它抢到CPU资源的几率会大一些, 并不一定会第一个执行
涉及到的Thread类中的成员:
成员方法:
- public int getPriorty(); 获取线程的优先级
- public void setPriority(int i); 设置优先级
成员常量:
- public static final int MAX_PRIORITY; 10
- public static final int MIN_PRIORITY; 1
- public static final int NORM_PRIORITY; 5,默认优先级
演示:多线程的优先级问题
package com.ithiema.api.demo;
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "run..." + i);
}
}
}
package com.ithiema.api.demo;
public class Demo01 {
public static void main(String[] args) {
//创建线程对象
MyThread mt1 = new MyThread("飞机");
MyThread mt2 = new MyThread("高铁");
MyThread mt3 = new MyThread("汽车");
//设置线程的优先级
System.out.println(mt1.getPriority());
System.out.println(mt2.getPriority());
System.out.println(mt3.getPriority());
//开启线程
mt1.start();
mt2.start();
mt3.start();
}
}
加入线程
成员方法:
- public void join(); 加入线程, 即: 相当于插队, 也叫: 等待这个线程死亡.
细节:如果要设置某个线程为加入线程,则必须在其他线程开启之前设置有效。
案例:演示加入线程
package com.ithiema.api.demo;
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "run..." + i);
}
}
}
package com.ithiema.api.demo;
public class Demo02 {
public static void main(String[] args) {
//创建三个线程
MyThread mt1 = new MyThread("康熙");
MyThread mt2 = new MyThread("四阿哥");
MyThread mt3 = new MyThread("八阿哥");
//开启线程
//设置爱第一个线程为:加入线程
mt1.start();
try {
mt1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
mt2.start();
mt3.start();
}
}
守护线程
细节:每一个线程默认都是非守护线程, 当非守护线程关闭(结束)的时候, 和它相关的守护线程都要自动关闭.
成员方法
- public void setDaemon(boolean flag); 设置线程的类别, true: 守护线程, false: 非守护线程(默认)
案例:演示守护线程
package com.ithiema.api.demo;
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "run..." + i);
}
}
}
package com.ithiema.api.demo;
public class Demo03 {
public static void main(String[] args) {
//创建两个线程对象
MyThread mt1 = new MyThread("关羽");
MyThread mt2 = new MyThread("张飞");
//设置它们为:守护线程
mt1.setDaemon(true);
mt2.setDaemon(true);
//设置当前进程(主进程)的名字为刘备
Thread.currentThread().setName("刘备");
//开启线程
mt1.start();
mt2.start();
//指定刘备的任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "..." + i);
}
}
}
线程池
简介:
-
概述:
- 实际开放中,当我们需要使用大量的生命周期短的线程对象时,每次频繁的创建和销毁线程对象非常消耗系统资源,针对于这种情况,我们可以创建一个池子,里面放一些线程对象,用的时候从池子里边拿,用完之后再放回去,从而避免了频繁创建和销毁大量生命周期短的线程对象,这个池子就叫: 线程池( ).
-
好处:节约资源,提高效率.
涉及到的成员方法:
- Executors:创建线程池的工具类
- public static ExecutorService newFixedThreadPool(int numThread); 获取线程池对象,指定线程个数
- ExecutorService:表示线程池
- public Future submit(Runnable target);
提交要执行的任务给线程池对象,由它自动分配线程对象来执行这些任务,并获取结果,任务类型为:Runnable资源类的对象. - public Future submit(Callable target);
提交要执行的任务给线程池对象,由它自动分配线程对象来执行这些任务,并获取结果,任务类型为:Callable资源类的对象. - public void shutdown(); 关闭线程池,实际开发中:不关闭
- public Future submit(Runnable target);
- Future:记录的是线程任务执行后的结果对象.
- public Object get(); 获取线程任务执行结束后,具体的返回值
步骤:
- 创建线程池,指定初始化的线程个数.
- 提交任务给线程池对象,由它分配线程来执行该任务,并返回执行对象.
- 从结果对象(Future)中获取线程任务执行结束后具体的返回值.
- 操作 线程任务的返回值.
- 关闭线程池.
细节:如果使用线程池提交任务, 提交的是Callable任务, 则返回值可以自定义, 提交的是Runnable任务, 默认返回: null
案例:线程池_提交Runnable任务
package com.ithiema.api.demo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Demo04 {
public static void main(String[] args) throws Exception {
//1.创建线程池,指定初始化的线程个数.
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//2.提交任务给线程池对象,由它分配线程来执行该任务,并返回执行对象.
//格式:threadPool.submit(Runnable接口的对象)
//方式一:匿名内部类
Future future = threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("我是匿名内部类的方式,提交Runnable任务");
}
});
//方式二:Lambda表达式
Future future = threadPool.submit(() -> System.out.println("我是Lambda表达式的方式,提交Runnable任务"));
//3.从结果对象(Future)中获取线程任务执行结束后具体的返回值.
Object obj = future.get();
//4.操作 线程任务的返回值.
System.out.println("线程任务执行结束后,返回值是:" + obj);
//5.关闭线程池.
threadPool.shutdown(); //实际开发中不关
}
}
案例:线程池_提交Callable任务
Runnable接口 和 Callable接口的区别是什么?
- 重写的方法不同.
Runnable接口是重写 **run()**方法, Callable接口是重写 **call()**方法. - 方法的返回值的类型不同.
Runnable#run(): 返回值是void类型.
Callable#call(): 返回值类型可以自定义. - 关于异常的处理方式不同.
Runnable#run(): 有异常只能try, 不能抛.
Callable#call(): 有异常可try, 可抛.
package com.ithiema.api.demo;
import java.util.concurrent.*;
public class Demo04 {
public static void main(String[] args) throws Exception {
//1.创建线程池,指定初始化的线程个数.
ExecutorService threadPool = Executors.newFixedThreadPool(10);
//2.提交任务给线程池对象,由它分配线程来执行该任务,并返回执行对象.
//格式:threadPoll.submit(Callable接口的子类任务)
//方式一:匿名内部类
Future future = threadPool.submit(new Callable<Integer>() { //这里泛型可以自定义
@Override
public Integer call() throws Exception {
System.out.println("匿名内部类的方式,提交Callable任务");
return 10;
}
});
//方拾二:Lambda表达式
Future future = threadPool.submit(() -> {
System.out.println("Lambda表达式的方式,提交Callable任务");
return 10;
});
//省略模式写法
Future future = threadPool.submit(() -> 10);
//3.从结果对象(Future)中获取线程任务执行结束后具体的返回值.
Integer num = (Integer) future.get();
//4.操作 线程任务的返回值.
System.out.println("返回值" + num);
//5.关闭线程池.
}
}
多线程记忆点-总结:
- 一台电脑上可以有多个进程, 这些进程之间的数据是相互 隔离 的.
- 一个进程可以有多条线程, 这些线程 共享该进程的 数据.
- 开启线程调用的是 Thread#start()方法, 如果调用Thread#run()方法, 只是普通的方法调用, 并没有开启线程.
- 同一个线程对象重复开启, 会报 IllegalThreadStateException(非法的线程状态异常)
- 线程的调度模型有两种: 1. 分时调度模型. 2. 抢占式调度模型. Java采用的是第二种.
- Java中多线程程序的执行特点是: 随机性和延迟性, 原因是因为CPU在做着高效的切换.
- 线程的优先级默认为5, 范围是: 1-10, 1最小, 10最大.
- 如果要设置某个线程为加入线程, 则必须在其他线程开启之前设置有效.
- 每一个线程默认都是非守护线程, 当非守护线程关闭(结束)的时候, 和它相关的守护线程都要自动关闭.
- 如果使用线程池提交任务, 提交的是Callable任务, 则返回值可以自定义, 提交的是Runnable任务, 默认返回: null
Lock锁
简介:
-
概述:
- 它是一个接口,也是JDK1.5的特性,叫:互斥锁,能让我清晰的看到在哪里加锁,在哪里释放锁的操作.
它的常用子类是ReentrantLock,可以实现同时管理多个线程,让线程有规律的执行,即:多线程的等待唤醒机制.
- 它是一个接口,也是JDK1.5的特性,叫:互斥锁,能让我清晰的看到在哪里加锁,在哪里释放锁的操作.
-
名词解释:多线程的等待唤醒机制
- 概述:
- 目前我们写的多线程程序打印的结果都是"成片"的数据, 这是因为多线程的执行具有随机性和延迟性,
这样做不太方便我们管理线程, 如果说我们想实现 线程1一次, 线程2一次, 线程3一次, 线程1一次, 线程2一次, 线程3一次… - 这种情况, 就必须使用: 多线程的等待唤醒机制了, 该某个线程执行, 我们就把它唤醒, 不该某个线程执行, 如果它抢到资源了, 我们就让它等待.
这个就是:多线程的等待唤醒机制, 它描述的是 3个或者3个以上的线程有规律的执行.
- 目前我们写的多线程程序打印的结果都是"成片"的数据, 这是因为多线程的执行具有随机性和延迟性,
- 概述:
实现方式:
方式1:采用 wait() + notifyAll() 组合实现.
Object类中的方法:
public void wait(); 让当前线程处于等待状态 张三, 李四, 王五.
public void notify(); 随机唤醒某一个线程, 注意: 唤醒是随机的.
public void notifyAll(); 唤醒所有等待的线程.
- 方式2:采用 Lock锁机制实现, 它能实现精准唤醒.
成员方法:
- public void lock(); 加锁
- public void unlock(); 解锁
Lock锁优化
package com.ithiema.api.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable implements Runnable {
private int ticket = 1000;
//创建Lock锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //加锁
if (ticket <= 0)
break;
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + ticket--);
lock.unlock(); //解锁
}
}
}
生产者和消费者
设计模式简介
-
概述:
-
所谓的设计模式不独属于任何的单一语言,它是前辈们的经验和总结,是一系列解决问题的思路和方案。
-
分类:
-
创建型:
- 特点:顾名思义,需要创建对象
- 例如:
- 单列设计模式,原型设计模式,工厂方法设计模式,抽象工厂设计模式,建造者设计模式
-
结构型:
- 特点:就是用来表述事物之间的关系的
- 例如:
- 装饰设计模式,适配器设计模式…
-
例:
装饰类 被装饰的类 BufferedReader br = new BufferedReader(new FileReader("day13/data/1.txt"))
-
行为型:
- 特点:描述事物能够做什么
- 例如:
- 模板方法设计模式,观察者,访问者,中介者(也叫消费者)…
-
-
结论:设计模式一共有23种,分为3大类,即:创建型(5种),结构型(7种),行为型(11种)。
生产者和消费者案例
步骤:
生产者消费者案例中包含的类:
奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
①创建奶箱对象,这是共享数据区域
②创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
③创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
④创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
⑤启动线程
奶箱类(Box)
package com.ithiema.api.demo;
//奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
public class Box {
//1.定义变量,记录第几瓶奶
private int milk;
//2.定义变量,记录奶箱放奶状态
private boolean state = false; //true:有奶,false:无奶
//3.定义方法,表示放奶瓶
public synchronized void put(int milk) {
//3.1 判断奶箱状态,有奶就等待
if(state){
//等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//3.2 走这里,说明肯定无奶,就放奶
System.out.println("送奶工正在放入第" + milk + "瓶奶");
this.milk = milk;
//3.3 修改奶箱状态
state = true;
//3.4 唤醒消费者来消费(取奶)
this.notify(); //随机唤醒一个等待的线程
}
//4.定义方法,表示取奶瓶
public synchronized void get() {
//4.1 判断奶箱状态,无奶就等待
if(!state){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//4.2 走这里,说明肯定有奶,就取奶
System.out.println("消费者正在获取第" + milk + "瓶奶");
//4.3 修改奶箱状态
state = false;
//4.4 唤醒生产者,放奶
this.notify();
}
}
生产者类(Producer,资源类)
package com.ithiema.api.demo;
//生产者类(Producer,资源类):实现Runnable接口,重写run()方法,调用存储牛奶的操作
public class Producer implements Runnable {
//1.定义变量,记录:奶箱(共享数据区)
private Box b;
//2.定义构造方法,传入具体的奶箱对象
public Producer(Box b) {
this.b = b;
}
//3.重写run()方法
@Override
public void run() {
for (int i = 1; i <= 31; i++) {
b.put(i);
}
}
}
消费者类(Customer)
package com.ithiema.api.demo;
//消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
public class Customer implements Runnable{
//1.定义变量,记录:奶箱(共享数据区)
private Box b;
//2.定义构造方法,传入具体的奶箱对象
public Customer(Box b) {
this.b = b;
}
//3.重写run()方法
@Override
public void run() {
while (true)
b.get();
}
}
测试类
package com.ithiema.api.demo;
public class Demo01 {
public static void main(String[] args) {
//1.创建奶箱类,表示:共享数据区
Box b = new Box();
//2.创建生产者类和消费者类对象,它们表示:资源类对象
Producer p = new Producer(b);
Customer c = new Customer(b);
//3.创建两个线程,分别封装上述的两个,资源类对象
Thread th1 = new Thread(p);
Thread th2 = new Thread(c);
//4.开启线程
th1.start();
th2.start();
}
}
定义变量,记录:奶箱(共享数据区)
private Box b;
//2.定义构造方法,传入具体的奶箱对象
public Producer(Box b) {
this.b = b;
}
//3.重写run()方法
@Override
public void run() {
for (int i = 1; i <= 31; i++) {
b.put(i);
}
}
}
**消费者类(Customer)**
~~~~java
package com.ithiema.api.demo;
//消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
public class Customer implements Runnable{
//1.定义变量,记录:奶箱(共享数据区)
private Box b;
//2.定义构造方法,传入具体的奶箱对象
public Customer(Box b) {
this.b = b;
}
//3.重写run()方法
@Override
public void run() {
while (true)
b.get();
}
}
~~~~
**测试类**
~~~java
package com.ithiema.api.demo;
public class Demo01 {
public static void main(String[] args) {
//1.创建奶箱类,表示:共享数据区
Box b = new Box();
//2.创建生产者类和消费者类对象,它们表示:资源类对象
Producer p = new Producer(b);
Customer c = new Customer(b);
//3.创建两个线程,分别封装上述的两个,资源类对象
Thread th1 = new Thread(p);
Thread th2 = new Thread(c);
//4.开启线程
th1.start();
th2.start();
}
}
~~~