什么是线程?
线程(Thread)是一个程序内部的一条执行流程。
这个是一条执行流程,虽然有循环,但是最后只有一条流程往前推进,所以视为一条。
程序中如果只有一条执行流程,那这个程序就是单线程的程序。
程序是指令序列,这些指令可以让CPU完成指定的任务。*.java程序经过编译后形成 *.class文件,在Windows中启动一个JVM虚拟机具体的任务相当于创建了一个进程,在虚拟机中加载class文件并运行,在class文件中通过执行创建新线程的代码来执行。
多线程是什么?
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
例如:消息通信、淘宝、京东系统都离不开多线程技术。
1、多线程的创建
如何在程序中创建出多条线程?
Java是通过java.lang.Thread 类的对象来代表线程的。 用以下3个方式来创建多线程:
1.1 方式一:继承Thread类
实现步骤
①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
②创建MyThread类的对象
③调用线程对象的start()方法启动线程(启动后还是执行run方法的)
package com.itheima.d1_create_thread;/*** 目标:掌握线程的创建方式一:继承Thread类*/
public class ThreadTest1 {// main方法是由一条默认的主线程负责执行。public static void main(String[] args) {// 3、创建MyThread线程类的对象代表一个线程Thread t = new MyThread();// 4、启动线程(自动执行run方法的)t.start(); // main线程 t线程for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出:" + i);}}
}
package com.itheima.d1_create_thread;/*** 1、让子类继承Thread线程类。*/
public class MyThread extends Thread{// 2、必须重写Thread类的run方法@Overridepublic void run() {// 描述线程的执行任务。for (int i = 1; i <= 5; i++) {System.out.println("子线程MyThread输出:" + i);}}
}
每次调用的结果不一样,随机生成:
继承Thread类优缺点:
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。(单继承)
多线程的注意事项
1、启动线程必须是调用start方法,不是调用run方法。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
2、不要把主线程任务放在启动子线程之前。
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
1.2 方式二:实现Runnable接口
实现步骤
①定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
②创建MyRunnable任务对象
③把MyRunnable任务对象交给Thread处理。
Thread类提供的构造器 | 说明 |
---|---|
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
④调用线程对象的start()方法启动线程
代码如下:
package com.itheima.d1_create_thread;/*** 1、定义一个任务类,实现Runnable接口*/
public class MyRunnable implements Runnable{// 2、重写runnable的run方法@Overridepublic void run() {// 线程要执行的任务。for (int i = 1; i <= 5; i++) {System.out.println("子线程输出 ===》" + i);}}
}
package com.itheima.d1_create_thread;/*** 目标:掌握多线程的创建方式二:实现Runnable接口。*/
public class ThreadTest2 {public static void main(String[] args) {// 3、创建任务对象。Runnable target = new MyRunnable();// 4、把任务对象交给一个线程对象处理。// public Thread(Runnable target)new Thread(target).start();for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出 ===》" + i);}}
}
其中,任务对象没有start方法,因为任务对象不是线程对象,start方法是线程对象才有的。所以需要把任务对象交给线程对象去处理。
实现Runnable接口优缺点:
- 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
- 缺点:需要多一个Runnable对象。
线程创建方式二:匿名内部类的写法
①可以创建Runnable的匿名内部类对象。
②再交给Thread线程对象。
③再调用线程对象的start()启动线程。
代码如下:
package com.itheima.d1_create_thread;/*** 目标:掌握多线程创建方式二的匿名内部类写法。*/
public class ThreadTest2_2 {public static void main(String[] args) {// 1、直接创建Runnable接口的匿名内部类形式(任务对象)Runnable target = new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子线程1输出:" + i);}}};new Thread(target).start();// 简化形式1:new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子线程2输出:" + i);}}}).start();// 简化形式2:new Thread(() -> {for (int i = 1; i <= 5; i++) {System.out.println("子线程3输出:" + i);}}).start();for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出:" + i);}}
}
1.3 方式三:实现Callable接口
在前面学的两种线程创建方式都存在一个问题:假如线程执行完毕后有一些数据需要返回,它们重写的run方法均不能直接返回结果。
那么如何解决这个问题呢?
- JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
- 这种方式最大的优点:可以返回线程执行完毕后的结果。
步骤:
①创建任务对象
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
- 把Callable类型的对象封装成FutureTask(线程任务对象)。
②把线程任务对象交给Thread对象。
③调用Thread对象的start方法启动线程。
④线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
FutureTask的API
FutureTask提供的构造器 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
FutureTask提供的方法 | 说明 |
---|---|
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
package com.itheima.d1_create_thread;import java.util.concurrent.Callable;/*** 1、让这个类实现Callable接口*/
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重写call方法@Overridepublic String call() throws Exception {// 描述线程的任务,返回线程执行返回后的结果。// 需求:求1-n的和返回。int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return "线程求出了1-" + n + "的和是:" + sum;}
}
package com.itheima.d1_create_thread;import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;/*** 目标:掌握线程的创建方式三:实现Callable接口。*/
public class ThreadTest3 {public static void main(String[] args) throws Exception {// 3、创建一个Callable的对象Callable<String> call = new MyCallable(100);// 4、把Callable的对象封装成一个FutureTask对象(任务对象)// 未来任务对象的作用?// 1、是一个任务对象,实现了Runnable对象.// 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。FutureTask<String> f1 = new FutureTask<>(call);// 5、把任务对象交给一个Thread对象new Thread(f1).start();Callable<String> call2 = new MyCallable(200);FutureTask<String> f2 = new FutureTask<>(call2);new Thread(f2).start();// 6、获取线程执行完毕后返回的结果。// 注意:如果执行到这儿,假如上面的线程还没有执行完毕// 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。String rs = f1.get();System.out.println(rs);String rs2 = f2.get();System.out.println(rs2);}
}
线程创建方式三的优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
1.4 对比三种线程的创建方式,以及不同点
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
2、Thread的常用方法
Thread提供了很多与线程操作相关的方法
Thread提供的常用方法 | 说明 |
---|---|
public void run() | 线程的任务方法 |
public void start() | 启动线程 |
public String getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
public void setName(String name) | 为线程设置名称 |
public static Thread currentThread() | 获取当前执行的线程对象 |
public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
public final void join()… | 让当前调用这个方法的线程先执行完! |
Thread提供的常见构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target, String name) | 封装Runnable对象成为线程对象,并指定线程名称 |
getName()
,setName()
,currentThread()
代码如下:
public class MyThread extends Thread{public MyThread(String name){super(name); //1.执行父类Thread(String name)构造器,为当前线程设置名字}@Overridepublic void run() {//2.currentThread() 哪个线程执行它,它就会得到哪个线程对象。Thread t = Thread.currentThread();for (int i = 1; i <= 3; i++) {//3.getName() 获取线程名称System.out.println(t.getName() + "输出:" + i);}}
}
在测试类中,创建线程对象,并启动线程
public class ThreadTest1 {public static void main(String[] args) {Thread t1 = new MyThread();t1.setName(String name) //设置线程名称;t1.start();System.out.println(t1.getName()); //Thread-0Thread t2 = new MyThread("2号线程");// t2.setName("2号线程");t2.start();System.out.println(t2.getName()); // Thread-1// 主线程对象的名字// 哪个线程执行它,它就会得到哪个线程对象。Thread m = Thread.currentThread();m.setName("最牛的线程");System.out.println(m.getName()); // mainfor (int i = 1; i <= 5; i++) {System.out.println(m.getName() + "线程输出:" + i);}}
}
执行上面代码,效果如下,每条线程都有自己的名字了:
sleep()
,join()
应用实例如下:
package com.itheima.d2_thread_api;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 目标:掌握sleep方法,join方法的作用。*/
public class ThreadTest2 {public static void main(String[] args) throws Exception {System.out.println(Runtime.getRuntime().availableProcessors());for (int i = 1; i <= 5; i++) {System.out.println(i);// 休眠5sif(i == 3){// 会让当前执行的线程暂停5秒,再继续执行// 项目经理让我加上这行代码,如果用户交钱了,我就注释掉!Thread.sleep(5000);}}// join方法作用:让当前调用这个方法的线程先执行完。Thread t1 = new MyThread("1号线程");t1.start();t1.join();Thread t2 = new MyThread("2号线程");t2.start();t2.join();Thread t3 = new MyThread("3号线程");t3.start();t3.join();}
}
执行效果如下:
前几行(1-5)是每隔5秒输出一个
后面几行(1号线程输出-3号线程输出)是等每个线程输出完才会往下继续执行,不像之前是随机执行。
Thread其他方法的说明:
Thread类还提供了诸如:yield, interrupt, 守护线程,线程优先级等线程的控制方法,在开发中很少用。
3、线程安全
3.1 什么是线程安全问题
线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。
比如:取钱的线程安全问题(场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?)
①小明线程步骤一先判断余额是否够,够
②小红线程步骤一先判断余额是否够,够
③因为小明线程判断够,所以银行吐出10万
④银行账户更新为0元
⑤因为小红线程判断够,所以银行吐出10万
⑥银行账户更新为-10万元
结果:2人都取钱10万,银行亏了10万。
以上取钱案例中的问题,就是线程安全问题的一种体现。
线程安全问题出现的原因?
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
3.2 用程序模拟线程安全问题
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
①:需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。
③:创建2个线程,传入同一个账户对象给2个线程处理。
④:启动2个线程,同时去同一个账户对象中取钱10万。
先定义一个共享的账户类
package com.itheima.d3_thread_safe;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();// 1、判断余额是否足够if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + 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;}}
再定义一个取钱的线程类
package com.itheima.d3_thread_safe;public class DrawThread extends Thread{private Account acc;public DrawThread(Account acc, String name){super(name);this.acc = acc;}@Overridepublic void run() {// 取钱(小明,小红)acc.drawMoney(100000);}
}
最后,再写一个测试类,在测试类中创建两个线程对象
package com.itheima.d3_thread_safe;/*** 目标:模拟线程安全问题。*/
public class ThreadTest {public static void main(String[] args) {// 1、创建一个账户对象,代表两个人的共享账户。Account acc = new Account("ICBC-110", 100000);// 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。new DrawThread(acc, "小明").start(); // 小明new DrawThread(acc, "小红").start(); // 小红}
}
最后结果:两个人都取了10万元,余额为-10万元
4、线程同步
4.1 认识线程同步
所谓线程同步就是解决线程安全问题的方案。
线程同步的思想就是让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步的常见方案
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
谁先进来,锁对象就分配给谁,如果是同时,锁对象有相应的机制,分配给其中的一个人。此时是小红线程先拿到锁对象
然后拿走10万元,余额变成0元
小红任务离开。自动解锁。小明竞争到锁
小明加锁,进入账户,发现余额不足
对于加锁的实现,有很多方案,比如以下三种:
4.2 方式一:同步代码块
同步代码块就是把访问共享资源的核心代码给上锁,以此保证线程安全
这个同步锁就是一个java对象来代表一把锁。
它的原理是每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
注意:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
把核心代码块也就是Accout.java中对于钱的操作加锁,代码实现如下:
// 小明 小红线程同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够synchronized ("黑马") { //此时加锁了if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}}
现在问题解决了,加了锁,小红小明只有一人可以对账户里的钱进行操作,但是如果说还有两个人有另一个共享的账号呢?
“黑马”是用双引号括起来的,整个计算机中只有一份,所以锁住小红或者小明的同时也锁住了另两个人,那么怎么办呢?
同步锁选择使用this
关键字,this正好代表共享资源!
// 小明 小红线程同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够// this正好代表共享资源!synchronized (this) {if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}}
那么如果是静态方法呢?
官方建议,静态方法建议使用字节码(类名.class)对象作为锁对象。
public static void test(){synchronized (Account.class){}}
总结一下:
-
锁对象随便选择一个唯一的对象好不好呢?
不好,因为会影响其他无关线程的执行。
-
锁对象的使用规范
建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
4.3 方式二:同步方法
同步方法就是把访问共享资源的核心方法给上锁,以此保证线程安全
原理也是每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
代码如下:
// 小明 小红线程同时过来的// 同步方法public synchronized void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}
同步方法底层原理:
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
那么有个问题,是同步代码块好还是同步方法好一点?
分两个方面比较:
范围上:同步代码块锁的范围更小,同步方法锁的范围更大。范围小的性能更好(因为大家可以先把不是带锁的代码同步执行完,速度更快)
可读性:同步方法更好。
4.4 方法三:Lock锁
以上两个办法都可以保证线程安全,他们共同的特点都是提供一个所谓的对象来代表锁,然后程序就会自动的帮助加锁解锁,以便保证线程安全。还有最后一个办法,Lock锁。
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。也就是可以自己创建一个锁对象,然后new一个锁对象,进行手工的加锁和解锁。
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
构造器 | 说明 |
---|---|
public ReentrantLock() | 获得Lock锁的实现类对象 |
Lock常用方法:
方法名称 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
代码如下:
package com.itheima.d6_synchronized_lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Account {private String cardId; // 卡号private double money; // 余额。// 创建了一个锁对象private final Lock lk = new ReentrantLock();public Account() {}public Account(String cardId, double money) {this.cardId = cardId;this.money = money;}// 小明 小红线程同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();try {lk.lock(); // 这个位置加锁// 1、判断余额是否足够if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}} catch (Exception e) {e.printStackTrace();} finally {lk.unlock(); // 这个位置解锁}}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;}
}
注意:
- 在创建锁对象的时候,建议在前面加一个final修饰,作用是lk这个锁之后是不能被替换的
- 在之后的编程中,如果加锁部分有bug,跳出了程序,那就意味着此时这个线程只加锁,但没有解锁,那会导致其他线程也进不来。所以加锁部分最好加个trycatchfianlly,然后把解锁操作放在finally中
5、线程通信[了解]
首先,什么是线程通信呢?
- 当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源挣抢。
线程通信的常见模式:是生产者与消费者模型
- 生产者线程负责生成数据
- 消费者线程负责消费生产者生成的数据
- 注意:生产者生产完数据后应该让自己等待,通知其他消费者消费;消费者消费完数据之后应该让自己等待,同时通知生产者生成。
比如下面案例中,有3个厨师(生产者线程),两个顾客(消费者线程)。
接下来,我们先分析一下完成这个案例的思路
1.先确定在这个案例中,什么是共享数据?
答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。2.再确定有哪几条线程?哪个是生产者,哪个是消费者?
答:厨师是生产者线程,3条生产者线程; 顾客是消费者线程,2条消费者线程3.什么时候将哪一个线程设置成什么状态生产者线程(厨师)放包子:1)先判断是否有包子2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待3)有包子时,不做包子了,直接唤醒别人、然后让自己等待消费者线程(顾客)吃包子:1)先判断是否有包子2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待
按照上面分析的思路写代码。先写桌子类,代码如下
public class Desk {private List<String> list = new ArrayList<>();// 放1个包子的方法// 厨师1 厨师2 厨师3public synchronized void put() {try {String name = Thread.currentThread().getName();// 判断是否有包子。if(list.size() == 0){list.add(name + "做的肉包子");System.out.println(name + "做了一个肉包子~~");Thread.sleep(2000);// 唤醒别人, 等待自己//这里要注意顺序,一定是先唤醒别人,再等待自己this.notifyAll();this.wait();}else {// 有包子了,不做了。// 唤醒别人, 等待自己this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}// 吃货1 吃货2public synchronized void get() {try {String name = Thread.currentThread().getName();if(list.size() == 1){// 有包子,吃了System.out.println(name + "吃了:" + list.get(0));list.clear();Thread.sleep(1000);this.notifyAll();this.wait();}else {// 没有包子this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}
}
再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程
public class ThreadTest {public static void main(String[] args) {// 需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上// 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。Desk desk = new Desk();// 创建3个生产者线程(3个厨师)new Thread(() -> {while (true) {desk.put();}}, "厨师1").start();new Thread(() -> {while (true) {desk.put();}}, "厨师2").start();new Thread(() -> {while (true) {desk.put();}}, "厨师3").start();// 创建2个消费者线程(2个吃货)new Thread(() -> {while (true) {desk.get();}}, "吃货1").start();new Thread(() -> {while (true) {desk.get();}}, "吃货2").start();}
}
执行上面代码,运行结果如下:发现多个线程相互协调执行,避免无效的资源争抢。
厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货2吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货1吃了:厨师1做的肉包子
厨师2做了一个肉包子~~
吃货2吃了:厨师2做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
6、线程池
线程池就是一个可以复用线程的技术。
要理解什么是线程复用技术,我们先得看一下不使用线程池会有什么问题,理解了这些问题之后,我们在解释线程复用同学们就好理解了。
假设:用户每次发起一个请求给后台,后台就创建一个新的线程来处理,下次新的任务过来肯定也会创建新的线程,如果用户量非常大,创建的线程也讲越来越多。然而,创建线程是开销很大的,并且请求过多时,会严重影响系统性能。
而使用线程池,就可以解决上面的问题。如下图所示,线程池内部会有一个容器,存储几个核心线程,假设有3个核心线程(人),这3个核心线程可以处理3个任务(圆点)。
但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。
工作线程(WorkThread)或者核心线程,这些是固定数量可以重复利用的。下面的区域是任务队列(WorkQueue),任务队列就是放线程需要处理的任务的,里面的任务实际是一个个对象,他们必须实现Runnable或者Callable任务接口才能成为任务对象扔到任务队列,然后被线程处理。
线程池可以控制线程的数量,然后重复利用线程来处理任务。同时它也可以控制任务的数量,可以把任务暂时缓存起来。因为线程池既可以控制线程的数量,又可以控制任务的数量,所以它不会因为线程过多,或者任务过多而导致把系统资源耗尽而引起系统耗尽的风险,提高系统的工作性能。
所以,线程池就是一个线程复用技术,它可以提高线程的利用率。
6.1 如何创建线程池?
在JDK5版本中提供了代表线程池的接口ExecutorService
如何得到线程池对象呢,有以下两个方式:
方式一:ExecutorService实现类ThreadPoolExecutor
使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
用这7个参数的构造器来创建线程池的对象。代码如下
ExecutorService pool = new ThreadPoolExecutor(3, //核心线程数有3个5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=28, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。TimeUnit.SECONDS,//时间单位(秒)new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待Executors.defaultThreadFactory(), //用于创建线程的工厂对象new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);
关于线程池,我们需要注意下面的两个问题:
1、临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
2、什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
方式二:Executors线程池的工具类
使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
跳转
6.2 线程池处理Runnable任务
ExecutorService的常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行 Runnable 任务 |
Future submit(Callable task) | 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
List shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
先准备一个线程任务类
public class MyRunnable implements Runnable{@Overridepublic void run() {// 任务是干啥的?System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");//为了模拟线程一直在执行,这里睡久一点try {Thread.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}}
}
下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。
ExecutorService pool = new ThreadPoolExecutor(3, //核心线程数有3个5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=28, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。TimeUnit.SECONDS,//时间单位(秒)new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待Executors.defaultThreadFactory(), //用于创建线程的工厂对象new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);Runnable target = new MyRunnable();
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
//下面4个任务在任务队列里排队
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);//下面2个任务,会被临时线程的创建时机了
pool.execute(target);
pool.execute(target);
// 到了新任务的拒绝时机了!
pool.execute(target);
执行上面的代码,结果输出如下:
接着看最后一个参数:新任务的拒绝策略
策略 | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy: | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
6.3 线程池处理Callable任务
ExecutorService的常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行 Runnable 任务 |
Future submit(Callable task) | ✅执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
List shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
先准备一个Callable线程任务
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重写call方法@Overridepublic String call() throws Exception {// 描述线程的任务,返回线程执行返回后的结果。// 需求:求1-n的和返回。int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;}
}
再准备一个测试类,在测试类中创建线程池,并执行callable任务。
public class ThreadPoolTest2 {public static void main(String[] args) throws Exception {// 1、通过ThreadPoolExecutor创建一个线程池对象。ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());// 2、使用线程处理Callable任务。Future<String> f1 = pool.submit(new MyCallable(100));Future<String> f2 = pool.submit(new MyCallable(200));Future<String> f3 = pool.submit(new MyCallable(300));Future<String> f4 = pool.submit(new MyCallable(400));// 3、执行完Callable任务后,需要获取返回结果。System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}
}
执行后,结果如下图所示
(没听懂)
6.4 Executors工具类实现线程池
Java为开发者提供了一个创建线程池的工具类,叫做Executors,它提供了方法可以创建各种不能特点的线程池。
方法名称 | 说明 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
创建固定线程数量的线程池代码如下。这几个方法用得不多,所以这里不做过多演示,了解一下就行了。
public class ThreadPoolTest3 {public static void main(String[] args) throws Exception {// 1、通过Executors创建一个线程池对象。ExecutorService pool = Executors.newFixedThreadPool(17);// 老师:核心线程数量到底配置多少呢???// 计算密集型的任务:核心线程数量 = CPU的核数 + 1// IO密集型的任务:核心线程数量 = CPU核数 * 2// 2、使用线程处理Callable任务。Future<String> f1 = pool.submit(new MyCallable(100));Future<String> f2 = pool.submit(new MyCallable(200));Future<String> f3 = pool.submit(new MyCallable(300));Future<String> 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创建线程池这么好用,为什么不推荐同学们使用呢?原因在这里:看下图,这是《阿里巴巴Java开发手册》提供的强制规范要求。
7、其它细节知识:并发、并行
进程就是:
- 正在运行的程序(软件)就是一个独立的进程。
- 线程是属于进程的,一个进程中可以同时运行很多个线程。
- 进程中的多个线程其实是并发和并行执行的。
大块的就是进程,下拉里是线程
并发:
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行:
在同一个时刻上,同时有多个线程在被CPU调度执行。
多线程是并发和并行同时进行的。
8、其它细节知识:线程的生命周期
线程的生命周期:
- 也就是线程从生到死的过程中,经历的各种状态及状态转换。
- 理解线程这些状态有利于提升并发编程的理解能力。
Java线程的状态:Java总共定义了6种状态,6种状态都定义在Thread类嵌套的内部枚举类中(Thread.State)。
线程的6种状态互相转换
线程的6种状态总结
线程状态 | 说明 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。 |
Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。 |
Teminated(终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
9、悲观锁与乐观锁
要求又要线程安全,又要同时执行,应该怎么处理?
悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后再解锁。线程安全,性能较差
乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全的时候才开始控制。线程安全,性能较好。