文章目录
一、 多线程概述
多线程简单描述:一个程序有多个执行路径
1.1 线程与进程
进程
有独立内存空间的应用程序
【指一个内存中运行的应用程序,每个进程都有一个独立的内存空间(每个内存都有自己的堆、栈,互不共享)】
线程
是进程中的一个执行路径,共享一个内存空间
线程之间可以自由切换,并发执行, 一个进程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
1.2 线程调度
线程调度,即线程到底是怎样执行的
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
使程序更合理地交替进行(交替时间极小,使得看上去感觉许多线程在同时运行)
抢占式调度(Java使用)
通过代码可以调整时间片来控制
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)
CPU使用抢占式调度模式在多个线程间进行高速切换,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高
1.3 同步与异步
同步(线程安全) | 异步(线程不安全) |
---|---|
排队执行 | 同时执行 |
效率低 | 效率高 |
数据安全 | 数据不安全 |
1.4 并发与并行
并发
两个或多个事件在同一个时间段内发生
并行
两个或多个事件在同一时刻发生(同时发生)
二、 创建线程
2.1 继承Thread类
Thread是JAVA提供的用于实现线程的类
有一个继承的方法run,run方法中的代码就是一条新的执行路径,路径的触发方式不是调用run方法,而是通过Thread对象的start来启动任务
Thread类
构造方法
常用构造方法:
构造器 | 描述 |
---|---|
Thread() | 分配新的 Thread对象,默认初始化 |
Thread(Runnable target) | 分配新的 Thread对象,传一个任务 |
Thread(Runnable target, String name) | 分配新的 Thread对象,给一个名称 |
字段
变量和类型 | 字段 | 描述 |
---|---|---|
static int | MAX_PRIORITY | 最高优先级 |
static int | MIN_PRIORITY | 最低优先级 |
static int | NORM_PRIORITY | 默认优先级 |
常用方法
变量和类型 | 方法 | 描述 |
---|---|---|
long | getId() | 返回此Thread的标识符 |
String | getName() | 返回此线程的名称 |
int | getPriority() | 返回此线程的优先级 |
void | setPriority(int newPriority) | 更改此线程的优先级 |
void | start() | 导致此线程开始执行; Java虚拟机调用此线程的run方法 |
static void | sleep(long millis) | 指定的毫秒数,使当前正在执行的线程休眠(暂时停止执行)【应用:每隔一秒输入输出等等,比较常用】 |
static void | sleep(long millis, int nanos) | 指定的毫秒数加上纳秒数,使当前正在执行的线程休眠(暂时停止执行) |
void | setDaemon(boolean on) | 将此线程标记为daemon线程或用户线程 |
daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了
用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡
面试题:
如何将一个线程停止?
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时
我们可以通过变量做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定
Thread在程序中的使用
常规用法
新建一个类继承Thread
代码示例:
- 编写一个线程
class MyThread extends Thread{ //Thread,JAVA提供的用于实现线程的类
/**
* 线程线程要执行的任务方法
* 每个线程都有自己的栈空间,共用一份堆内存
*/
@Override
public void run() {
//这里的代码,就是一条新的执行路径
//这个执行路径的触发方式,不是调用run方法,而是通过Thread对象的start来启动任务
for(int i = 0;i < 5;i++){
System.out.println( i + "这是另一个线程");
}
}
- 再在主函数中编写代码,来实现多线程
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); //时间分配是抢占式分配
for(int i = 0;i < 5;i++){
System.out.println( i + "这是main中的线程");
}
}
运行结果:
由于Java的时间分配是抢占式时间分配,谁抢占到谁就先执行,因此得到如下输出结果
0这是另一个线程
0这是main中的线程
1这是另一个线程
1这是main中的线程
2这是另一个线程
2这是main中的线程
3这是另一个线程
4这是另一个线程
3这是main中的线程
4这是main中的线程
画图解释此程序中的多线程执行:
使用匿名内部类
代码示例:
public static void main(String[] args) {
//new Thread(){}.start();
new Thread(){ //匿名内部类,仅几行代码即可实现一个线程
@Override
public void run() {
for (int i = 0;i < 5;i++){
System.out.println(i + "hahaha");
}
}
}.start();
for (int i = 0;i < 5;i++){
System.out.println(i + "heiheihei");
}
}//end main
运行结果:
0hahaha
0heiheihei
1heiheihei
1hahaha
2heiheihei
3heiheihei
2hahaha
4heiheihei
3hahaha
4hahaha
2.2 实现Runnable接口(用得更多)
用于给线程执行的任务,但是还是要借助Thread
【创建一个任务对象,里面包含了任务→再创建一个线程,为其分配这个任务→start执行】
与前面继承Thread类相比,优势在于:
-
通过 创建任务→给线程分配的方式 来实现多线程,更适合多个线程同时执行相同任务的情况
-
可以避免单继承所带来的的局限性(java单继承,但是可以多实现)
-
任务与线程本身分离,提高了程序的健壮性
-
!!线程池技术,接收Runnable类型的任务,不接收Thread类型的线程
代码示例:
- 新建一个MyRunnable类实现Runnable接口
/**
* 第二种实现多线程技术
* 实现Runnable接口
* 用于给线程执行的任务,但是还是要借助Thread
*/
class MyRunnable implements Runnable{
@Override
public void run() {
//线程任务
for(int i = 0;i < 5;i ++){
System.out.println( i + "这是另一个线程");
}
}
}
- 再在主函数中编写代码,来实现多线程
public static void main(String[] args) {
/**
* 第二种实现多线程技术
* 实现Runnable接口(用的更多)
*/
MyRunnable r = new MyRunnable();//创建一个任务对象,里面包含了任务
Thread t = new Thread(r);//创建一个线程,为其分配一个任务
t.start();//执行这个线程
for(int i = 0;i < 5;i++){
System.out.println( i + "这是main中的线程");
}
}
运行结果:
0这是另一个线程
1这是另一个线程
2这是另一个线程
3这是另一个线程
0这是main中的线程
4这是另一个线程
1这是main中的线程
2这是main中的线程
3这是main中的线程
4这是main中的线程
2.3 实现Callable接口(用的很少)
面试中容易被问到: 这是java中第三种线程的实现方式
Thread和Runnable实现的线程和主线程并发执行(主线程执行,另外的线程也执行)
Callable创建的线程可以与主线程并发执行,也可以实现主线程等待该线程执行完毕并返回结果
FutureTask接口
变量和类型 | 方法 | 描述 |
---|---|---|
V | get() | 获取线程执行的结果 |
V | get(long timeout, TimeUnit unit) | 传入超时的时间,获取线程执行的结果,如果超时则不获取结果 |
Callable使用步骤
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable< T > {
@Override
public < T > call() throws Exception {
return T;
}
}- 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask< Integer > future = new FutureTask<>(callable);- 通过Thread启动线程
new Thread(future).start();
代码示例:
使用Callable创建线程:
- 编写类实现Callable接口 , 实现call方法
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//Thread.sleep(3000);
for (int i = 0;i < 10;i ++){
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " " + i);
}
return 100;
}
}
- 在main中创建FutureTask对象 , 并传入第一步编写的Callable类对象
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
- 在main中启动线程
new Thread(task).start();
线程用法一
与Thread和Runnable一样,Callable创建的线程可以与主线程并发执行
在main中创建主线程:
for (int i = 0;i < 10;i ++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
输出结果:
两个线程同时交替执行
Thread-0 0
main 0
main 1
Thread-0 1
Thread-0 2
main 2
main 3
Thread-0 3
Thread-0 4
main 4
main 5
Thread-0 5
Thread-0 6
main 6
main 7
Thread-0 7
Thread-0 8
main 8
Thread-0 9
main 9
线程用法二
该线程先执行,主线程等待该线程执行完毕并返回结果
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,调用此方法会阻塞主进程继续往下执行
Callable获取返回值:在main中主线程的前面添加语句,用 FutureTask的.get()方法获取子线程的返回结果
Integer x = task.get();
System.out.println(Thread.currentThread().getName() + "返回值为:" + x);
输出结果:
由输出可以看出,该线程先执行,主线程等待该线程执行完毕后返回结果再执行
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
main返回值为:100
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
Callable的其它使用方法
-
判断线程任务是否执行完毕:.isDone()
-
取消线程:.cancel(参数)
参数是boolean类型,true表示取消
返回值类型也是boolean,返回true表示取消线程成功(通常是任务没完成然后取消了),返回false表示取消线程失败(通常情况下任务已经执行完毕)
Callable<Integer> c = new MyCallable(); //编写类实现Callable接口 , 实现call方法,创建对象
FutureTask<Integer> task = new FutureTask<>(c);//创建FutureTask对象 , 并传入第一步编写的Callable类对象
task.isDone();//判断线程任务是否执行完毕
task.cancel();//取消线程
2.4 Runnable 与 Callable
接口定义
//Runnable接口
public interface Runnable {
public abstract void run();
}
//Callable接口
public interface Callable<V> {
V call() throws Exception;
}
异同点
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
区别点 | Runnable | Callable |
---|---|---|
有无返回值 | 没有返回值 | 可以返回执行结果 |
任务能否抛出异常 | Runnable的run()不能抛出异常 | Callable接口的call()允许抛出异常 |
三、 线程有关操作
常用方法
变量和类型 | 方法 | 描述 |
---|---|---|
long | getId() | 返回此Thread的标识符 |
String | getName() | 返回此线程的名称 |
int | getPriority() | 返回此线程的优先级 |
void | setPriority(int newPriority) | 更改此线程的优先级 |
void | start() | 导致此线程开始执行; Java虚拟机调用此线程的run方法 |
static void | sleep(long millis) | 指定毫秒数,使当前正在执行的线程休眠(暂时停止执行)【应用:每隔一秒输入输出等等,比较常用】 |
static void | sleep(long millis, int nanos) | 指定毫秒数+纳秒数,使当前正在执行的线程休眠(暂时停止执行) |
void | setDaemon(boolean on) | 将此线程标记为daemon线程或用户线程 |
daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了
用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡
3.1 设置和获取线程名称
变量和类型 | 方法 | 描述 |
---|---|---|
String | setName() | 设置线程的名称 |
String | getName() | 获取线程的名称 |
代码示例:
- 新建一个MyRunnable类实现Runnable接口
static class MyRunnable implements Runnable{
@Override
public void run() {
//currentThread():获取当前正在执行的对象
//getName获取线程名称
System.out.println(Thread.currentThread().getName());
}
}
- 再在主函数中编写代码,来实现多线程以及获取线程名称
//currentThread():获取当前正在执行的对象
System.out.println(Thread.currentThread().getName());//main线程
Thread t = new Thread(new MyRunnable());
t.setName("第0个线程");//使用setName设置线程名称
t.start();
new Thread(new MyRunnable(),"第1个线程").start();
new Thread(new MyRunnable(),"第2个线程").start();
new Thread(new MyRunnable(),"第3个线程").start();
new Thread(new MyRunnable()).start();//没有给线程setName,则系统会自动命名
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
运行结果:
main
第0个线程
第1个线程
第3个线程
Thread-1
第2个线程
Thread-3
Thread-2
3.2 线程休眠
变量和类型 | 方法 | 描述 |
---|---|---|
static void | sleep(long millis) | 指定毫秒数,使当前正在执行的线程休眠(暂时停止执行) |
static void | sleep(long millis, int nanos) | 指定毫秒数+纳秒数,使当前正在执行的线程休眠(暂时停止执行) |
sleep为Thread的静态方法,因此可以用Thread直接调用:Thread.sleep()
1秒 = 1000毫秒
1毫秒 = 1000微妙 = 1000000纳秒
代码示例:
for (int i = 0;i < 5;i++){
System.out.println(i);
Thread.sleep(1000); //每次循环暂停1000毫秒后再继续执行
}
输出结果:
每隔1秒(1000毫秒)输出一个数
0
1
2
3
4
3.3 线程中断
一个线程是一个独立的执行路径,是否应该结束,应该由其自身决定
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时
我们可以通过给对象做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定
代码示例:
- 首先新建类实现Runnable接口,并继承run方法 新建线程
static class MyRunnable implements Runnable{
@Override
public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
for (int i =0;i < 10;i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 编写main
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable()); //新建线程
t1.start();
for (int i =0;i < 5;i ++){//main线程
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//打标记之后程序进去catch
e.printStackTrace();
}
}
//main线程打印5次,t1线程打印10次,因此main线程先中断,此时对t1线程打标记
//给线程t1添加中断标记,但是只是告诉线程它可以死亡,但是未必死亡
t1.interrupt();
}
- 打标记处理:
对线程对象打标记,触发异常,使程序进入catch,后续的处理依旧由程序员决定
① 修改MyRunnable中的try-catch语句
发现中断标记后进入catch,但是程序可以选择不死亡,继续执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("发现了中断标记,但是不死亡");
}
输出结果:
每个线程都隔1秒打印一个数,由于线程不死亡,因此发现标记之后继续执行
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,但是不死亡
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
② 修改MyRunnable中的try-catch语句
发现中断标记后进入catch,程序死亡,中断程序、释放资源
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
return;//表示线程结束,资源释放
}
输出结果:
每个线程都隔1秒打印一个数,由于发现中断标记后线程自杀死亡,因此发现标记之后结束程序
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,线程自杀
3.4 设置守护线程
变量和类型 | 方法 | 描述 |
---|---|---|
void | setDaemon(boolean on) | 将此线程标记为daemon线程或用户线程 |
线程分为守护线程和用户线程
用户线程:当一个进程不包含任何存活的用户线程时,进行结束(我们直接创建的线程都是用户线程)
守护线程:用于守护用户线程,当最后一个用户线程结束时,守护线程自动死亡
代码示例:
- 新建类实现Runnable接口,并继承run方法 新建线程
static class MyRunnable implements Runnable{
@Override
public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
for (int i =0;i < 10;i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 编写main
在线程启动前,用setDaemon()来标记守护线程
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());//t1为子线程
t1.setDaemon(true);//设置t1为守护线程,在t1启动前设置
t1.start();//启动守护线程
//main主线程,当主线程结束时,守护线程也会结束
for (int i =0;i < 5;i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
main:3
Thread-0:3
main:4
Thread-0:4
Thread-0:5
3.5 停止线程
以下文多线程通信问题中的生产者与消费者问题中的程序为例,我们让线程运行,但是我们只是打开了线程,并没有关闭线程,到最后程序运行完只能手动停止线程
我们也可以选择调用方法去停止线程
Thread.currentThread().interrupt();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());
System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());
四、 线程阻塞
通俗地讲,线程阻塞不仅仅指线程休眠,程序停在那读文件、读完之后继续执行,那一段时间也算是阻塞
因此可总结为:
所有消耗时间的操作都属于线程阻塞,如文件读取、接收用户收入、线程休眠等
五、 线程安全
线程不安全问题
一个任务交给三个线程去执行,容易出现线程不安全问题
代码示例:
- 首先新建类Ticket 实现接口Runnable,新建run卖票任务
/**
* 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {//每次被触发就进卖买票操作
while(count > 0){
//卖票
System.out.println("正在准备卖票");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("出票成功!余票:" + count);
}
}//end
}
- 在main中创建并启动三个线程
public static void main(String[] args) {
//线程不安全
Runnable runnable = new Ticket();
//启动三个线程
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
输出结果:
正在准备卖票,请稍等...
正在准备卖票,请稍等...
正在准备卖票,请稍等...
出票成功!余票:8
正在准备卖票,请稍等...
出票成功!余票:9
正在准备卖票,请稍等...
出票成功!余票:7
正在准备卖票,请稍等...
出票成功!余票:5
正在准备卖票,请稍等...
出票成功!余票:6
正在准备卖票,请稍等...
出票成功!余票:4
正在准备卖票,请稍等...
出票成功!余票:3
正在准备卖票,请稍等...
出票成功!余票:2
正在准备卖票,请稍等...
出票成功!余票:1
正在准备卖票,请稍等...
出票成功!余票:0
出票成功!余票:-2
出票成功!余票:-1
通过输出结果观察可知,余票出现了负数,但是代码逻辑上余票count=0时便不再执行了
出现问题原因:假设三段线程为ABC,ABC可能同时进行到while,假设A先进入,此时count = 1,当A进入休眠未进行到count–时,B检测到count = 1,进入while,当B进入休眠未进行到count–时,C检测到count = 1,进入while,此时A运行count–,count = 0,B接着运行count- -,count =-1,C接着运行count- -,count = -2,同时由于线程阻塞以及线程调度,输出的顺序可能不同
这就是多线程完成统一任务时出现的线程不安全问题
实现线程安全(显式锁与隐式锁)
- 隐式锁:隐式锁使用synchronized修饰符。在使用sync关键字的时候,当sync代码块执行完成之后程序能够自动获取锁和释放锁
- 显式锁:显式锁使用Lock关键字。在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
添加synchronized关键字的同步代码块和同步方法属于隐式锁
Lock关键字属于显式锁
1 同步代码块
线程同步,使线程排队执行
实现思路:每个线程在执行时看同一把锁,谁抢到了锁,谁就执行
线程同步实现:synchronized
格式:
synchronized(锁对象){
// 同步代码块
}
`
锁对象: java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
代码示例:
对原有的线程不安全的卖票示例进行修改
在while循环中加锁
同步代码块为:当余票大于0时,进行卖票操作
因此当一个线程正在执行同步代码块时,另外的线程不会执行该代码块,在后面排队等待执行
/**
* 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
*
* 解决线程不安全问题:排队执行
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();//创建对象
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
synchronized (o){//加锁
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
}
}//end while
}//end run
}
由于只创建了一个任务,因此Object对象只创建了一个,即创建了一把锁
而后面启动的三个线程由于只有一个任务,因此三个线程在执行的时候看同一把锁,排队执行
Runnable runnable = new Ticket();//只有一个任务,因此下面的object对象只创建了一个
//启动三个线程,o是同一个,只有一个任务,因此在执行的时候只看一个o
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
//如果上述写法写成如下,则依旧为不安全线程
//此时创建了三个任务(new Ticket()),分别创建三个object对象(即锁),此时相当于3个人卖票,每个人卖10张
//错误写法!!!!注意
//new Thread(new Ticket()).start();
//new Thread(new Ticket()).start();
//new Thread(new Ticket()).start();
加了锁之后的输出结果:
正在准备卖票,请稍等...
Thread-0出票成功!余票:9
正在准备卖票,请稍等...
Thread-0出票成功!余票:8
正在准备卖票,请稍等...
Thread-0出票成功!余票:7
正在准备卖票,请稍等...
Thread-0出票成功!余票:6
正在准备卖票,请稍等...
Thread-0出票成功!余票:5
正在准备卖票,请稍等...
Thread-0出票成功!余票:4
正在准备卖票,请稍等...
Thread-0出票成功!余票:3
正在准备卖票,请稍等...
Thread-0出票成功!余票:2
正在准备卖票,请稍等...
Thread-0出票成功!余票:1
正在准备卖票,请稍等...
Thread-0出票成功!余票:0
如果将创建锁的对象写在任务的代码块中,如下所示
此时,每个线程启动时都会创建o对象,因此每个线程都有自己锁o,每个线程在执行时都看自己的锁,这时不能排队,要格外注意!!!!
错误写法:
public void run() {//每次被触发就进卖买票操作
Object o = new Object();//!!!!!!!三个线程启动时都会创建o对象,即每个线程都有自己的锁o,每个人都看自己的不同的锁,此时不能排队
while(true){
synchronized (o){//加锁
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
}
}//end while
}//end run
2 同步方法
与同步代码块相似,不同的是,同步方法以方法为单位进行加锁,给方法添加synchronized修饰符
同步方法的锁为this
同步方法有可能被静态修饰,如果被静态修饰,则同步方法的锁为类.class
代码示例:
/**
* 创建一个任务,但是交给三个线程去执行,则会出现线程不安全问题
* 解决线程不安全问题:排队执行
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
boolean flag = sale();//sale()为加了锁的方法
if(!flag){
break;
}
}//end while
}//end run
//添加synchronized修饰符,给方法加锁
public synchronized boolean sale(){
//this,同步的方法的锁
//Ticket.class,如果方法为静态方法,则同步方法的锁为类.class
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
return true;
}
return false;
}
}
如果同步代码块锁了一段代码,同步方法锁了另一端代码,锁的对象都是this,那么这当一段代码正在执行时,另一段加锁的代码不能执行
如下面的代码所示,在循环前加了一把锁,则当一个线程执行这段代码块时,同步方法sale不能执行
public void run() {
synchronized (this){//再加一把锁
}
while(true){
boolean flag = sale();
if(!flag){
break;
}
}//end while
}//end run
如果有多个同步的方法,且多个方法都是this这把锁,则其中一个方法执行、其他方法无法执行
3 显式锁Lock
在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
显式锁比隐式锁更好,更能体现锁的概念,体现了面向对象的机制
显式锁Lock的子类:ReentrantLock
代码示例:
- 创建隐式锁l
Lock l = new ReentrantLock();
- 在进行代码块前锁住
l.lock();
- 在代码块结束后开锁
l.unlock();//代码执行完毕,开锁
完整代码如下:
public static void main(String[] args) {
//线程不安全
//解决方案3:显式锁Lock
//java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
Runnable runnable = new Ticket();//只有一个任务
//启动三个线程,但使用的都是runnable对象,因此用的都是同一把锁l
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//创建显式锁l
private Lock l = new ReentrantLock();
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
l.lock();//进入if之前,锁住
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
l.unlock();//代码执行完毕,开锁
}//end while
}//end run
}
六、 公平锁与非公平锁
大厂面试可能会遇到
- 公平锁:排队,先来先到,在Lock构造方法传入Boolean值True,则为公平锁
- 非公平锁:抢,隐式锁Sync属于非公平锁,Lock默认为非公平锁
实现公平锁:
显式锁Lock的构造方法中,参数为True则表示公平锁
Lock l = new ReentrantLock(true);
七、 线程死锁
死锁:多个线程线程互相持有对方所需要的资源,多个线程因竞争资源而造成的一种僵局(互相等待)
死锁举例
拿生活中的场景并结合代码,举一个简单的栗子:
挟持着人质的罪犯与警察两人僵持不下(警察抓着罪犯,而罪犯手上有人质)
罪犯对警察说:“你放了我,我放人质!”然而警察听到后内心想:“我救人质,但是罪犯跑了”
警察对罪犯说:“你放了人质,我放过你!”
然而罪犯听到后内心想:“警察放过我,但是人质跑了”
根据这个场景,来进行代码的实现
- 罪犯Culprit
/**
* 罪犯
*/
static class Culprit{
//罪犯对警察说
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放人质!");
p.fun();
}
//听了警察的话,内心回应
public synchronized void fun(){
System.out.println("罪犯内心:警察放过我,但是人质跑了");
}
}
- 警察Police
/**
* 警察
*/
static class Police{
//警察对罪犯说
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放过你!");
c.fun();
}
//听了罪犯的话,警察回应
public synchronized void fun(){
System.out.println("警察内心:我救人质,但是罪犯跑了");
}
- 新建线程MyThread,警察对罪犯说
static class MyThread extends Thread{
private Culprit c;
private Police p;
//构造方法
public MyThread(Culprit c,Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
/**
* 警察say方法执行完之后,调用罪犯的fun方法,等待罪犯回应
*/
p.say(c);//警察说话,让罪犯回应
}
}
- 新建主线程,罪犯对警察说
public static void main(String[] args) throws InterruptedException {
Culprit c = new Culprit();//新建一个罪犯对象
Police p = new Police();//新建一个警察对象
new MyThread(c,p).start();//新建线程:警察说话,让罪犯回应
/**
* 罪犯的say方法调用执行完后,调用警察的fun方法,等待警察回应
*/
c.say(p);//主线程:罪犯说话,让警察回应
}
此时,有两个线程,而这两个线程中,警察和罪犯都说完了自己的话(执行say),等待对方回应(执行fun),然而等待对方回应前先必须等待对方把话说完(执行say),但是不知道对方有没有先说完(有没有执行完say),因此卡住了,造成了死锁
死锁的结果输出:
罪犯和警察说完之后都在等待对方回应,从而造成了死锁,程序卡在那无法继续进行,只能手动结束程序
罪犯:你放了我,我放人质!
警察:你放了人质,我放过你!
但是,当其中一个线程执行够快,则不会出现死锁情况(概率很低),输出结果如下:
罪犯:你放了我,我放人质!
警察内心:我救人质,但是罪犯跑了
警察:你放了人质,我放过你!
罪犯内心:警察释放我,但是人质跑了
死锁避免
- 线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 根源上解决: 在任何有可能产生锁的方法中,不调用另一个有可能产生锁的方法
八、 多线程通信问题
概述
多线程通信问题,也就是生产者与消费者问题
生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全
引入
该过程可以类比为一个栗子:
厨师为生产者,服务员为消费者,假设只有一个盘子盛放食品。
厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…
在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保了数据的安全
根据厨师和服务员这个栗子,我们可以通过代码来一步步实现
- 定义厨师线程
/**
* 厨师,是一个线程
*/
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
//运行的线程,生成100道菜
@Override
public void run() {
for (int i = 0 ; i < 100; i ++){
if(i % 2 == 0){
f.setNameAneTaste("小米粥","没味道,不好吃");
}else{
f.setNameAneTaste("老北京鸡肉卷","甜辣味");
}
}
}
}
- 定义服务员线程
/**
* 服务员,是一个线程
*/
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i =0 ; i < 100;i ++){
//等待
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}//end run
}//end waiter
- 新建食物类
/**
* 食物,对象
*/
static class Food{
private String name;
private String taste;
public void setNameAneTaste(String name,String taste){
this.name = name;
//加了这段之后,有可能这个地方的时间片更有可能被抢走,从而执行不了this.taste = taste
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}//end set
public void get(){
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
}
}//end food
- main方法中去调用两个线程
public static void main(String[] args) {
Food f = new Food();
Cook c = new Cook(f);
Waiter w = new Waiter(f);
c.start();//厨师线程
w.start();//服务生线程
}
运行结果:
只截取了一部分,我们可以看到,“小米粥”并没有每次都对应“没味道,不好吃”,“老北京鸡肉卷”也没有每次都对应“甜辣味”,而是一种错乱的对应关系
...
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...
name和taste对应错乱的原因:
当厨师调用set方法时,刚设置完name,程序进行了休眠,此时服务员可能已经将食品端走了,而此时的taste是上一次运行时保留的taste。
两个线程一起运行时,由于使用抢占式调度模式,没有协调,因此出现了该现象
以上运行结果解释如图:
加入线程安全
针对上面的线程不安全问题,对厨师set和服务员get这两个线程都使用synchronized关键字,实现线程安全,即:当一个线程正在执行时,另外的线程不会执行,在后面排队等待当前的程序执行完后再执行
代码如下所示,分别给两个方法添加synchronized修饰符,以方法为单位进行加锁,实现线程安全
/**
* 食物,对象
*/
static class Food{
private String name;
private String taste;
public synchronized void setNameAneTaste(String name,String taste){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}//end set
public synchronized void get(){
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
}
}//end food
输出结果:
由输出可见,又出现了新的问题:
虽然加入了线程安全,set和get方法不再像前面一样同时执行并且菜名和味道一一对应,但是set和get方法并没有交替执行(通俗地讲,不是厨师一做完服务员就端走),而是无序地执行(厨师有可能做完之后继续做,做好几道,服务员端好几次…无规律地做和端)
...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...
实现生产者与消费者问题
由上面可知,加入线程安全依旧无法实现该问题。因此,要解决该问题,回到前面的引入部分,严格按照生产者与消费者问题中所说地去编写程序
生产者与消费者问题:
生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全
↓
厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…
↓
在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保数据的安全
需要用到的java.lang.Object 中的方法:
变量和类型 | 方法 | 描述 |
---|---|---|
void | notify() | 唤醒当前this下的单个线程 |
void | notifyAll() | 唤醒当前this下的所有线程 |
void | wait() | 当前线程休眠 |
void | wait(long timeoutMillis) | 当前线程休眠一段时间 |
void | wait(long timeoutMillis, int nanos) | 当前线程休眠一段时间 |
- 首先在Food类中加一个标记flag:
True表示厨师生产,服务员休眠
False表示服务员端菜,厨师休眠
private boolean flag = true;
- 对set方法进行修改
当且仅当flag为True(True表示厨师生产,服务员休眠)时,才能进行做菜操作
做菜结束时,将flag置为False(False表示服务员端菜,厨师休眠),这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
然后唤醒在当前this下休眠的所有进程,而厨师线程进行休眠
public synchronized void setNameAneTaste(String name,String taste){
if(flag){//当标记为true时,表示厨师可以生产,该方法才执行
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;//生产完之后,标记置为false,这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
this.notifyAll();//唤醒在当前this下休眠的所有进程
try {
this.wait();//此时厨师线程进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}//end set
- 对get方法进行修改
当且仅当flag为False(False表示服务员端菜,厨师休眠)时,才能进行端菜操作
端菜结束时,将flag置为True(True表示厨师生产,服务员休眠),这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师生产一份的情况
然后唤醒在当前this下休眠的所有进程,而服务员线程进行休眠
public synchronized void get(){
if(!flag){//厨师休眠的时候,服务员开始端菜
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
flag = true;//端完之后,标记置为true,这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师只生产一份的情况
this.notifyAll();//唤醒在当前this下休眠的所有进程
try {
this.wait();//此时服务员线程进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}// end if
}//end get
作了以上调整之后的程序输出:
我们可以看到,没有出现数据错乱,并且菜的顺序是交替依次进行的
...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...
这就是生产者与消费者问题的一个典型栗子
九、 线程的六种状态
java.lang.Object
java.lang.Enum<Thread.State>
java.lang.Thread.State
Enum Thread.State
线程可以处于以下状态之一:
状态 | 描述 |
---|---|
NEW | 尚未启动的线程的线程状态 |
RUNNABLE | 可运行线程的线程状态 |
BLOCKED | 线程的线程状态被阻塞等待监视器锁定 |
WAITING | 等待线程的线程状态 |
TIMED_WAITING | 具有指定等待时间的等待线程的线程状态 |
TERMINATED | 终止线程的线程状态 |
NEW
- 线程刚创建,但是没有启动
RUNNABLE
- 在Java虚拟机中执行的线程处于此状态
BLOCKED
-
当一个线程和其他线程排队时
-
被阻塞等待监视器锁定的线程处于此状态
WAITING
-
当一个线程休眠的时候(可以被唤醒)
-
无限期等待另一个线程执行操作的线程处于此状态
TIMED_WAITING
-
当一个线程在指定时间内休眠的时候(可以被唤醒)
-
指定时间内等待另一个线程执行操作的线程处于此状态
TERMINATED
- 已退出的线程处于此状态
线程在给定时间点只能处于一种状态, 这些状态是虚拟机状态,不反映任何操作系统线程状态
不需要理解如何实现,只需要理解状态种类,理解线程在可运行状态和非运行状态之间的切换情况
十、 线程池Executors
引入
池:容器的意思
使用一个线程通常要经过创建线程、创建任务、执行任务、关闭线程,在这个过程中,创建任务和执行任务的时间很少
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,浪费的时间多,因此o频繁创建线程o会大大降低系统的效率(频繁创建线程和销毁线程需要时间)
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源
作用
-
降低资源消耗
-
提高响应速度
-
提高线程的可管理性
分类
不论是哪一类,获取线程池的对象都是ExecutorService
1 缓存线程池
长度没有限制
- 创建缓存线程池:
.newCachedThreadPool()
//创建缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
- 向线程池中加入新的任务,指挥线程池执行新的任务(run):
//向线程池中加入新的任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
//向线程池中加入新的任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
//向线程池中加入新的任务,执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
输出结果:
由输出可知,三个线程名称为1,2,3
pool-1-thread-3任务执行
pool-1-thread-1任务执行
pool-1-thread-2任务执行
- 添加休眠时间,使程序休眠一段时间
Thread.sleep(1000);//停一秒之后,再去执行线程,此时缓存线程池中已有内容,执行缓存池中的内容
- 指挥线程池执行任务
此时缓存池中已有内容,再去执行任务时,执行缓存池中空闲的任务
//向线程池中加入任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {//execute中传入任务对象即可
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
输出结果:
由输出结果可知,线程实现了重复使用,休眠后执行的任务是缓存池中已有的空闲任务3
pool-1-thread-1任务执行
pool-1-thread-3任务执行
pool-1-thread-2任务执行
pool-1-thread-3任务执行
2 定长线程池
相对于缓存线程池,长度有限制,线程池中的当前线程数目不会超过给定的长度
当该值为0的时候,意味着没有任何线程,线程池会终止
代码示例:
- 创建定长线程池,这里指定线程池大小为2
.newFixedThreadPool(参数),参数为线程池的长度
//创建定长线程池,指定了线程池的大小为2
ExecutorService service = Executors.newFixedThreadPool(2);
- 向线程池中加入新的任务,指挥线程池执行任务
如下面代码所示,添加3个任务
//向线程池中加入任务,指挥线程池执行新的任务(run)
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
输出结果:
由于线程池长度为2,因此最多两个任务,线程池中的当前线程数目不会超过2
pool-1-thread-2任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
3 单线程线程池
与定长线程池中传入参数为1的作用相同,即线程池中只有一个线程
- 创建单线程线程池
.newSingleThreadExecutor()
ExecutorService service = Executors.newSingleThreadExecutor();
- 向线程池中加入新的任务,指挥线程池执行任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
});
输出结果:
由输出可知,线程池中只有一个线程
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
4 周期性任务定长线程池
为定长线程池
把一个任务定时在某个时期执行,或者是周期性执行
任务在某个时期执行
- 创建单线程线程池
.newScheduledThreadPool(参数),参数为线程池的长度
//创建 周期性任务定长线程池
//任务创建出来的结果不一样
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
- 向线程池中加入新的任务,指挥线程池执行任务
.schedule(参数1,参数2,参数3)
参数1:定时执行的任务
参数2:表示时长的数字x(每隔x运行一次任务)
参数3:时长数字的时间单位,由TimeUnit的常量制定
/**
* 定时执行一次
* 参数1:定时执行的任务
* 参数2:表示时长的数字x(每隔x运行一次任务)
* 参数3:时长数字的时间单位,由TimeUnit的常量制定
*/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
},5, TimeUnit.SECONDS);//任务在5秒钟后执行
输出结果:
5秒钟后输出
pool-1-thread-1任务执行
周期性执行
- 创建单线程线程池
.newScheduledThreadPool(参数),参数为线程池的长度
//创建 周期性任务定长线程池
//任务创建出来的结果不一样
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
- 向线程池中加入新的任务,指挥线程池执行任务
.schedule(参数1,参数2,参数3,参数4)
参数1:定时执行的任务
参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
参数3:表示时长的数字x(每隔x运行一次任务)
参数4:时长数字的时间单位,由TimeUnit的常量制定
/**
* 周期性执行
* 参数1:任务
* 参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
* 参数3:表示时长的数字x(每隔x运行一次任务)
* 参数4:时长数字的时间单位,由TimeUnit的常量制定
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "任务执行");
}
},5,1,TimeUnit.SECONDS);//5秒后执行,每隔1秒执行一次
输出结果:
5秒钟后开始输出,之后每隔1秒输出一次,直到停止程序
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
无论是哪种线程池,使用完毕后必须手动关闭线程池,否则会一直在内存中存在
十一、 Lambda表达式
线程举例
首先来写一段冗余的Runnable代码
- 新建MyRunnable类实现Runnable接口
static class MyRunnable implements Runnable{
@Override
public void run() {
// 任务
System.out.println("任务执行");
}
}
- 在main中执行任务
//冗余的Runnable代码
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
观察上面的代码,我们可以看到,为了完成某个任务需要写多行代码,代码麻烦,存在冗余
↓
r只用了一次,因此可以用匿名对象(匿名内部类)来实现
↓
Thread t = new Thread(new Runnable() {//使用匿名内部类来实现
@Override
public void run() {
System.out.println("任务执行");
}
});
t.start();
相比上面的两大段代码,已经简洁了很多
最后来看Lambda表达式的使用
↓
Thread t = new Thread(() -> {
System.out.println("任务执行");
});
t.start();
明显简洁了很多,具体分析如下图所示
常见例子
接口必须只有一个方法,才能使用Lambda表达式
- 定义一个只有一个方法的接口
只包含一个方法,那么在实现接口的时候,一定是为了实现这个方法
static interface MyMath{
int sum(int x,int y);
}
- 编写print方法
public static void print(MyMath m,int x,int y){
int num = m.sum(x,y);
System.out.println(num);
}
- 主函数中去调用这个方法
使用Lambda表达式中,把匿名内部类的类的部分都删掉,只保留方法参数部分和方法体
public static void main(String[] args) {
//常规写法
print(new MyMath() {//实现接口
@Override
public int sum(int x, int y) {
return x+y;
}
},100,200);
//使用Lambda表达式写法
print((int x, int y) -> {//把匿名内部类的类的部分都删掉,只保留方法参数部分和方法体
return x+y;
},100,200);
}