个人推荐:
📢📢📢 前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下
"通俗易懂,风趣幽默"
,感觉非常有意思,忍不住分享一下给大家。点击跳转到教程。
前言:
📢📢📢本篇博文主要对B站韩顺平老师的<<一天学会线程 Thread Synchronized 互斥锁 进程 并行 并发 死锁等:>>和廖雪峰博客多线程进行学习。
【韩顺平讲Java】一天学会线程 Thread Synchronized 互斥锁 进程 并行 并发 死锁等:https://www.bilibili.com/video/BV1zB4y1A7rb?spm_id_from=333.999.0.0
廖雪峰多线程连接:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472
一.线程相关概念
1.什么是程序(program)
是为完成特定任务、用某种语言编写的一组指令的集合。简单的说就是我们写的代码(数据结构+算法)。
备注:软件不等于程序,软件可以简单理解为由相关开发文档和程序组成
2.什么是进程
① 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
② 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。
③进程是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源。
3.什么是线程
为了提⾼系统的执⾏效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理,所以有了线程,线程取代了进程调度的基本功能(线程由进程创建,是进程的一个实体)。
4.单线程和多线程
单线程:
单线程:同一个时刻,只允许执行一个线程
多线程:
多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件
5.并发和并行
并发:
并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说单核cpu实现的多任务就是并发。
并行:
并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。
二.线程的三种实现方式
创建线程的方式:
- 通过重写Runnable 接口的run方法;
- 通过继承 Thread 类,重写run方法;
- 通过 线程池的方式分配线程。【主流】
1.通过重写Runnable 接口的run方法
public class RunnableTest implements Runnable
{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println("执行方式一:执行第" + i + "次");
}
}
}
class Test{
public static void main(String[] args) {
// 方式一
Thread thread = new Thread(new RunnableTest());
thread.start();
// 方式二
Thread thread1 = new Thread(()->{
for (int i = 1; i <= 10; i++) {
System.out.println("执行方式二:执行第" + i + "次");
}
});
thread1.start();
}
}
备注: ()->{}
这种写法是Java8的新特性Lambda表达式
运行效果:
2.通过继承 Thread 类,重写run方法
class Thread_NEW extends Thread {
@Override
public void run() {
// 重写run方法
for (int i = 1; i <= 10; i++) {
System.out.println("执行方式三:执行第" + i + "次");
}
}
}
class Test2{
public static void main(String[] args) {
Thread_NEW thread_new = new Thread_NEW();
thread_new.start();
}
}
运行效果:
分析方式一和方式二:
线程通过start()方法启动,实际上该方法调用的是start0(),该方法被native关键字修饰,表示该方法是一个外部方法。
start()方法调用start0()方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态。具体什么时候执行,取决于CPU,由CPU统一调度。
线程的执行方法体就是Runnable接口的run方法(下面的代码在Thread类中)
我们需要执行的内容,只需要重写Runnable接口的run方法,在run方法中编写我们要执行的内容即可
方式一和方式二区别:
① 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口。
② 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable。
3.通过线程池的方式分配线程
上面二种方式都是创建一个线程,然后去执行任务,任务执行完毕后关闭线程,如果下次使用的时候需要再次进行创建,这种方式资源消耗大,任务响应速度慢,可管理性差。我们可以通过池化技术来解决以上问题,就是通过线程池的方式去分配线程。
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。---廖雪峰官网使用线程池(https://www.liaoxuefeng.com/wiki/1252599548343744/1306581130018849)
Java标准库提供了ExecutorService接口表示线程池,该接口的几个常用实现类(以下实现类封装在Executors类中)如下:
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
(1) FixedThreadPool
FixedThreadPool线程池表示创建固定大小的线程池
- 实例代码
package com.zm.createthread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 通过线程池的方式分配线程
*/
public class ExecutorServiceTest {
public static void main(String[] args) {
// 方式1
ExecutorService executorService = Executors.newFixedThreadPool(5);// 线程池中有5个线程
for (int i = 1; i <= 5; i++) {// 一共有5个任务
executorService.submit(()->{
try {
System.out.println(Thread.currentThread().getName()+":执行任务");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 等任务执行完后,线程池关闭
executorService.shutdown();
}
}
运行效果:
当线程池的大小为3,而任务数为5时,剩余二个任务需要等待前面三个任务执行完后再进行执行:
注意submit()方法用于将任务提交到线程池中,其中接收的参数为Runnable和Callable接口。
shutdown()方法表示等任务执行完后,线程池再进行关闭,而shutdownNow()方法不需要等待全部任务执行完就关闭。
(2) CachedThreadPool
CachedThreadPool线程池表示根据任务动态调整的线程池的大小
- 实例代码
package com.zm.createthread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 通过线程池的方式分配线程
*/
public class ExecutorServiceTest {
public static void main(String[] args) {
// 方式1
//ExecutorService executorService = Executors.newFixedThreadPool(3);// 线程池中有5个线程
// 方式2
ExecutorService executorService = Executors.newCachedThreadPool();//根据任务数动态跳转任务数
for (int i = 1; i <= 5; i++) {// 一共有5个任务
executorService.submit(()->{
try {
System.out.println(Thread.currentThread().getName()+":执行任务");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown(); // 表示等任务执行完后,线程池在进行关闭
//executorService.shutdownNow();// 表示立即关闭,不需要等待全部任务执行完
}
}
- 运行效果
调整任务数量为10时,会线程池的大小为10
- 创建指定动态范围的线程池
根据newCachedThreadPool()的源码,可知线程池通过ThreadPoolExecutor进行创建。
只需要修改第一个参数和第二个参数就可以实现在指定动态范围创建线程池
package com.zm.createthread;
import java.util.concurrent.*;
/**
* 通过线程池的方式分配线程
*/
public class ExecutorServiceTest {
public static void main(String[] args) {
// 方式1
//ExecutorService executorService = Executors.newFixedThreadPool(3);// 线程池中有5个线程
// 方式2
// ExecutorService executorService = Executors.newCachedThreadPool();//根据任务数动态跳转任务数
int min = 0;
int max = 10;
ExecutorService executorService = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
for (int i = 1; i <= 5; i++) {// 一共有5个任务
executorService.submit(()->{
try {
System.out.println(Thread.currentThread().getName()+":执行任务");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown(); // 表示等任务执行完后,线程池在进行关闭
//executorService.shutdownNow();// 表示立即关闭,不需要等待全部任务执行完
}
}
(3) ScheduledThreadPool
SingleThreadExecutor线程池表示仅单线程执行的线程池,意思是同一个任务需要反复执行(定时器),就需要使用该线程池来实现。通过ScheduledThreadPool来实现单线程执行的线程池,其实ScheduledThreadPool就是在ExecutorService接口中多了几个方法,来实现定时执行任务。通过ScheduledThreadPool执行定时任务,是完全可以通过ScheduledThreadPool取代旧的Timer。
- n秒/毫秒/分后执行一次性任务(只执行一次):
参数corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态
class ScheduledThreadPoolTest{
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
System.out.println("当前时间:"+dateFormat.format(new Date()));
// 1秒后执行一次性任务:
ses.schedule(()->{
System.out.println("当前时间:"+dateFormat.format(new Date()));
}, 1, TimeUnit.SECONDS);
ses.shutdown();// 当任务执行完毕后进行关闭
}
}
- n秒/毫秒/分后开始执行定时任务,每n秒/毫秒/分执行:
class ScheduledThreadPoolTest{
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
System.out.println("当前时间:"+dateFormat.format(new Date()));
// 2秒后开始执行定时任务,每3秒执行一次:
ses.scheduleAtFixedRate(()->{
System.out.println("当前时间:"+dateFormat.format(new Date()));
}, 2, 3, TimeUnit.SECONDS);
}
}
- 任务以固定的n秒/毫秒/分为间隔执行:
class ScheduledThreadPoolTest{
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
System.out.println("当前时间:"+dateFormat.format(new Date()));
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(()->{
System.out.println("当前时间:"+dateFormat.format(new Date()));
}, 2, 3, TimeUnit.SECONDS);
}
}
- FixedRate和FixedDelay的区别
FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:
而FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:
- 在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?
class ScheduledThreadPoolTest{
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
//2s后,每间隔一秒执行一次任务
ses.scheduleAtFixedRate(()->{
try {
System.out.println("开始执行");
Thread.sleep(2000);// 延时2s
System.out.println("当前时间:"+dateFormat.format(new Date()));
System.out.println("执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 2, 1, TimeUnit.SECONDS);
}
}
本来每1s执行一次任务,但是由于任务加了延时函数,所以需要2s才能够完成,根据下面的运行情况,可知后续任务并不会并行执行,而是会等上一次执行完毕后,再立即执行下一次。
- 如果任务抛出了异常,后续任务是否继续执行?
class ScheduledThreadPoolTest{
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
ses.scheduleAtFixedRate(()->{
System.out.println("开始执行");
System.out.println("执行任务:1/0");
int num1 = 1;
int num2 = 0;
int i = num1 / num2;
System.out.println("开始结束");
}, 2, 1, TimeUnit.SECONDS);
Thread.sleep(5000);
ses.shutdown();
}
}
如果执行任务抛出了异常,后续任务不会继续执行。
三.Callable接口
1.Callable接口
在上文中submit()方法的参数有二种类型,Runnable和Callable接口,二个接口都是用来执行提交的任务,和Runnable接口不同的是Callable接口可以获取执行任务后的结果。
- 实例代码
package com.zm.createthread;
import java.util.concurrent.*;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);// 线程池中有5个线程
// 一个Future类型的实例代表一个未来能获取结果的对象
Future<String> future = executorService.submit(new Task());// 通过Callable接口作为参数传入
// 判断是否执行完毕
while (true) {
if (future.isDone()) {
// 在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果
System.out.println("执行情况:"+future.get());
executorService.shutdownNow();// 立即关闭线程池
break;
}
}
}
}
class Task implements Callable<String> {
@Override
public String call() {
System.out.println("执行任务");
return "成功";
}
}
- 运行效果
- 如何获取执行任务返回的结果
executorService.submit(new Task())会返回一个 Future对象,该对象代表一个未来能获取结果的对象,通过Future对象的get()方法就可以获取到执行任务完成后返回的值(值的类型由Callable的返回类型决定,
Callable<String>
表示返回一个String类的数据),需要注意的是在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。所以会搭配Future对象的isDone()方法一起使用,isDone()表示是否执行完毕。
2.Future
Future接口定义了主要的5个接口方法,有RunnableFuture和SchedualFuture继承这个接口,以及CompleteFuture和ForkJoinTask实现这个接口。
来源于:
https://blog.csdn.net/u014209205/article/details/80598209
Future接口主要包括5个方法
方法 | 描述 |
---|---|
get() | 可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕 |
get(long timeout,TimeUnit unit) | 等待timeout的时间就会返回结果 |
cancel(boolean mayInterruptIfRunning) | 可以用来停止一个任务,如果任务可以停止(通过mayInterruptIfRunning来进行判断),则可以返回true,如果任务已经完成或者已经停止,或者这个任务无法停止,则会返回false. |
isDone() | 判断当前方法是否完成 |
isCancel() | 判断当前方法是否取消 |
- CompletableFuture
从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
class CompletableFutureTest{
public static void main(String[] args) throws InterruptedException {
// 创建异步执行任务:
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(CompletableFutureTest::fetchPrice);
// 如果执行成功:
cf.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 如果执行异常:
cf.exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
static Double fetchPrice() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
if (Math.random() < 0.3) {
throw new RuntimeException("fetch price failed!");
}
return 5 + Math.random() * 20;
}
}
二种不同的执行效果:
- CompletableFuture相较于Future还可以实现任务串行和并行。
例如,定义两个CompletableFuture,第一个CompletableFuture根据证券名称查询证券代码,第二个CompletableFuture根据证券代码查询证券价格,这两个CompletableFuture实现串行操作如下(
串行化
):
实例代码:
class CompletableFutureTest2{
public static void main(String[] args) throws Exception {
// 第一个任务:
CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油");
});
// cfQuery成功后继续执行下一个任务:
CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
return fetchPrice(code);
});
// cfFetch成功后打印结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
static String queryCode(String name) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
运行效果:
例如,我们考虑这样的场景:同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作(
并行化
):
class CompletableFutureTest3{
public static void main(String[] args) throws Exception {
// 两个CompletableFuture执行异步查询:
CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://finance.sina.com.cn/code/");
});
CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
// 两个CompletableFuture执行异步查询:
CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
});
CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://money.163.com/price/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);
// 最终结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
static String queryCode(String name, String url) {
System.out.println("query code from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code, String url) {
System.out.println("query price from " + url + "...");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
运行效果:
- 汇总
thenAccept()处理正常结果;
exceptional()处理异常结果;
thenApplyAsync()用于串行化另一个CompletableFuture,当前一个CompletableFuture执行成功后才执行下一个CompletableFuture;
anyOf()和allOf()用于并行化多个CompletableFuture;anyOf()任意个CompletableFuture只要一个成功”,allOf()可以实现“所有CompletableFuture都必须成功。
四.线程的终止
① 当线程完成任务后,会自动退出。
② 还可以通过使用变量来控制run方法退出的方式来停止线程,即通知方式
需求:主线程休眠1s之后停止子线程运行,实现代码如下:
class RunnableController implements Runnable
{
// 定义一个线程状态标识
private boolean start;
private int i;
public RunnableController() {
this.start = true;
}
@Override
public void run() {
while (start) {
++i;
System.out.println("执行方式一:执行第" + i + "次");
}
}
// 通过set进行属性设置
public void setStart(boolean start) {
this.start = start;
}
}
class Test4{
public static void main(String[] args) throws InterruptedException {
RunnableController runnableController = new RunnableController();
Thread thread = new Thread(runnableController);
thread.start();
// 1s钟后停止线程
Thread.sleep(1000);
runnableController.setStart(false);
}
}
运行效果:
五.线程常用方法
1.第一组常用方法
- setName(),getName(),getPriority(),setPriority()
public class ThreadMethod {
public static void main(String[] args) {
Thread thread = new Thread(()-> System.out.println("当前线程名称:"+Thread.currentThread().getName()+"该线程的优先级:"+Thread.currentThread().getPriority()));
thread.setName("线程1");
thread.setPriority(1);// 1~10
Thread thread2 = new Thread(()-> System.out.println("当前线程名称:"+Thread.currentThread().getName()+"该线程的优先级:"+Thread.currentThread().getPriority()));
thread2.setName("线程2");
thread2.setPriority(10);// 1~10
Thread thread3 = new Thread(()-> System.out.println("当前线程名称:"+Thread.currentThread().getName()+"该线程的优先级:"+Thread.currentThread().getPriority()));
thread2.setName("线程3");
thread.start();
thread2.start();
thread3.start();
}
}
运行效果:
优先级必须设置在1~10闭区间,不然会引发
IllegalArgumentException
异常,数值越大优先级越高,
默认优先级为5,最小优先级为1,最大优先级为10
优先级越高并不是一定先执行,只是获得更多的执行机会
@Test
public void test() {
Thread.currentThread().setPriority(6);
for (int j = 0; j < 50; j++) {
if (j == 10) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
System.out.println("优先级为---------" + Thread.currentThread().getPriority());
}
});
thread1.setPriority(1);
thread1.start();
} else if (j == 20) {
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
System.out.println("优先级为---------" + Thread.currentThread().getPriority());
}
});
thread2.setPriority(10);
thread2.start();
}
}
}
运行效果:
- run()
注意:执行run()方法并不是开启线程,只是通过对象调用run()方法!
@Test
public void test2() {
Thread thread = new Thread(() -> System.out.println("-------调用run方法-----" + Thread.currentThread().getName()));
thread.run();
}
运行效果:
- sleep
调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞),其他线程可以通过interrupt方法打断正在休眠的线程,这时sleep方法会抛出InterruptException异常,睡眠结束后的线程未必会立刻执行,最后推荐使用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
TimeUnit可以指定休眠时间单位(sleep只有毫秒为单位)
实例代码:
运行效果:
- interrupt
中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程(sleep)和等待(wait)的线程
实例代码:
运行效果:
我们可以通过isInterrupted()判断当前线程是否被中断,返回true表示是,返回false表示否,此方法不会影响中断标志的状态,当调用interrupted()方法可以获取中断情况但是会改变中断的标志状态。:
实例代码:
运行效果:
实例代码:
运行效果:
2.第二组常用方法
- yield线程礼让
调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程,具体的实现依赖于操作系统的任务调度器,所以礼让的时间不确定,也不一定定礼让成功。
实例代码:
运行效果( 由于礼让不一定成功无法控制,所以不怎么使用,也看不出什么效果
):
- join线程的插队
线程插队插队成功后,先执行完插入的形成的所有任务,再执行后面的任务,调用join可以表示等待某一个线程结束。
实例代码:
运行效果:
可以通过给join传入一个时间来设置最大等待时间:
运行效果:
六.用户线程和守护线程
用户线程:
用户线程:也叫工作线程,当线程的任务执行完或通知方式结束。
守护线程:
守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,常见的守护线程,垃圾回收机制。
通过 setDaemon(true);
设置该线程为守护线程,实例代码如下:
@Test
public void test5() throws InterruptedException {
Thread thread = new Thread(()->{
try {
while (true){
System.out.println("小偷正在偷钱..");
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 将thread设置为守护线程
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 50; i++) {
System.out.println("警察正在来的路上.....");
Thread.sleep(50);
}
System.out.println("警察赶到,小偷被抓,小偷停止偷钱");
}
运行效果:
七.线程生命周期
JDK 中用 Thread.State 枚举表示了线程的几种状态
具体代码如下:
线程状态转换图
下面的内容来源于:https://www.pdai.tech/md/java/thread/java-thread-x-thread-basic.html
- 新建(New)
创建后尚未启动。 - 可运行(Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。 - 阻塞(Blocking)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。 - 无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | - |
- 限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | - |
LockSupport.parkUntil() 方法 | - |
- 死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
八.线程同步
什么是线程同步:
① 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
② 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作.。
③ 被Synchronized关键字修饰的同步方法的效率会变低。
同步使用Synchronized关键字进行修饰,共二种写法同步代码块和同步方法,如下:
同步原理:
类似于生活中,在寝室上厕所,一次进入一个人(线程),等线程出来之后,再放其他线程进入(这里包括从厕所出来的线程),进入就关门(上锁,互斥锁),出来开门。
需求:网上售票,一段时间同时有多人进行购票,要保证同一张票不能被多人购买,造成票数出现负数情况。
出现票数负数的代码:
package com.zm.synchronization;
public class SynChronZedTest {
public static void main(String[] args) {
RunnableNew runnableNew = new RunnableNew();
Thread thread = new Thread(runnableNew);
thread.setName("售票窗口1");
thread.start();
Thread thread2 = new Thread(runnableNew);
thread2.setName("售票窗口2");
thread2.start();
Thread thread3 = new Thread(runnableNew);
thread3.setName("售票窗口3");
thread3.start();
Thread thread4 = new Thread(runnableNew);
thread4.setName("售票窗口4");
thread4.start();
}
}
class RunnableNew implements Runnable {
private Integer tick = 50;
@Override
public void run() {
while (true) {
if (tick <= 0) {
System.out.println("售票结束");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 进行出售
System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));
}
}
}
改进使用同步代码块:
package com.zm.synchronization;
public class SynChronZedTest {
public static void main(String[] args) {
RunnableNew runnableNew = new RunnableNew();
Thread thread = new Thread(runnableNew);
thread.setName("售票窗口1");
thread.start();
Thread thread2 = new Thread(runnableNew);
thread2.setName("售票窗口2");
thread2.start();
Thread thread3 = new Thread(runnableNew);
thread3.setName("售票窗口3");
thread3.start();
Thread thread4 = new Thread(runnableNew);
thread4.setName("售票窗口4");
thread4.start();
}
}
class RunnableNew implements Runnable {
private Integer tick = 50;
@Override
public void run() {
while (true) {
synchronized (RunnableNew.class) {
if (tick <= 0) {
System.out.println("售票结束");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 进行出售
System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));
}
}
}
}
运行效果:
改进使用同步方法:
package com.zm.synchronization;
public class SynChronZedTest {
public static void main(String[] args) {
RunnableNew runnableNew = new RunnableNew();
Thread thread = new Thread(runnableNew);
thread.setName("售票窗口1");
thread.start();
Thread thread2 = new Thread(runnableNew);
thread2.setName("售票窗口2");
thread2.start();
Thread thread3 = new Thread(runnableNew);
thread3.setName("售票窗口3");
thread3.start();
Thread thread4 = new Thread(runnableNew);
thread4.setName("售票窗口4");
thread4.start();
}
}
class RunnableNew implements Runnable {
private boolean tage = true;
private Integer tick = 50;
@Override
public void run() {
while (tage) {
sell();
}
}
private synchronized void sell() {
if (tick <= 0) {
System.out.println("售票结束");
this.tage = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 进行出售
System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));
}
}
运行效果:
其实上面二种方式本质是一样的:
- 同步方法如果没有使用static修饰,默认锁对象为this
- 同步方法如果使用static修饰,默认锁对象为.class
使用this进行上锁和未用static修饰的同步方法进行上锁本质上是一致的都是对当前对象进行上锁(上面锁的就是runnableNew对象,四个线程都是使用该对象进行上锁可以保证线程同步)
为了验证上面的说法,这里我们给四个线程分别传递不同的RunnableNew对象,该对象中tick通过static进行修饰保证数据共享
详细代码如下:
package com.zm.synchronization;
public class SynChronZedTest {
public static void main(String[] args) {
Thread thread = new Thread(new RunnableNew());
thread.setName("售票窗口1");
thread.start();
Thread thread2 = new Thread(new RunnableNew());
thread2.setName("售票窗口2");
thread2.start();
Thread thread3 = new Thread(new RunnableNew());
thread3.setName("售票窗口3");
thread3.start();
Thread thread4 = new Thread(new RunnableNew());
thread4.setName("售票窗口4");
thread4.start();
}
}
class RunnableNew implements Runnable {
private static Integer tick = 50;
@Override
public void run() {
while (true) {
synchronized (this) {
if (tick <= 0) {
System.out.println("售票结束");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 进行出售
System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));
}
}
}
}
运行效果( 再次运行会发现票数出现负数
):
上面的操作类似于下图:
使用this表示对当前对象进行上锁,但是传入的几个对象都不相同所以当tick=3时,四个线程同时进入,且tick>0满足条件,d都执行tick-1,所以最后的结果为tick=-1。
改进方法,同步代码块中将this换成RunnableNew.class或同步方法加上static关键字(提高锁对象的作用域)
运行效果( 问题解决
):
上面的操作类似与下图:
通过.class进行上锁是对类进行上锁,四个对象都属于同一个类,所以当有一个对象执行时就会上锁,另外三个就无法进入,需要等待进去的线程出来后,再次找机会进入。
实际上像上面这种操作,效率是非常低的,运行多次发现大多数情况下只有一个线程在跑:
九.线程死锁
1.重入锁
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁,Java的线程锁就是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
例子:
当执行add(int n)方法,且n<0时会执行dec(-n)方法,由于在执行add(int n)方法时我们获取到了this这个对象的锁,当我们调用dec(-n)方法时我们任然可以再次获取this这个对象的锁,这就是重入锁,并且此时记录数为2。
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
2.释放锁
当,当前线程中的记录数为0时就会释放锁。
下面操作会释放锁:
下面操作不会释放锁:
3.死锁
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生.
应用案例:
实例代码如下:
package com.zm.synchronization;
import org.junit.Test;
public class DeadLockDemo extends Thread {
private static Object playPhone = new Object();
private static Object doHomerWork = new Object();
private boolean tage;
public DeadLockDemo(boolean tage) {
this.tage = tage;
}
@Override
public void run() {
if (tage) {
// 先玩手机再做作业
synchronized (playPhone) {
System.out.println(Thread.currentThread().getName() + ":我在玩手机不要来管喔!");
synchronized (doHomerWork) {
System.out.println(Thread.currentThread().getName() + ":妈妈,我手机玩好啦,开始做作业啦");
}
}
} else {
synchronized (doHomerWork) {
System.out.println(Thread.currentThread().getName() +":儿子,快去做作业,做完再玩!");
synchronized (playPhone) {
System.out.println(Thread.currentThread().getName() +":儿子真乖,去玩会手机吧");
}
}
}
}
public static void main(String[] args) {
DeadLockDemo mom = new DeadLockDemo(false);
mom.setName("妈妈");
DeadLockDemo son = new DeadLockDemo(true);
son.setName("儿子");
mom.start();
son.start();
}
}
运行效果( 造成了死锁
):
备注:解决死锁的方法就是保证锁的顺序要一致。
4.wait和notify
多线程可以通过synchronized解决多线程竞争的问题,但是无法解决线程协调的问题,这时候可以使用wait和notify用于多线程协调运行:
- 在synchronized内部可以调用wait()使线程进入等待状态;
- 必须在已获得的锁对象上调用wait()方法;
- 在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程,其中notify()是随机唤醒一个,有可能某个线程一直无法唤醒,所以使用notifyAll()更安全一些;
- 必须在已获得的锁对象上调用notify()或notifyAll()方法;
- 已唤醒的线程还需要重新获得锁后才能继续执行。
例子:
下面的代码,大致就是先创建5个线程去调用getTask(),该方法的作用就是去删除队列里的数据 (该线程内部采用while(true)的死循环,当触发中断异常的时候才会结束线程),然后创建1个线程去向队列添加数据,共添加10条数据,添加完后结束该线程。
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
// 创建一个TaskQueue的队列
TaskQueue q = new TaskQueue();
// 创建一个线程集合
ArrayList<Thread> ts = new ArrayList<>();
// 循环创建线程
for (int i=0; i<5; i++) {
// 创建执行getTask()方法的线程
Thread t = new Thread(() -> {
// 执行task:
while (true) {
try {
String s = q.getTask();// 创建的线程去执行队列里的getTask()方法
System.out.println("线程:"+Thread.currentThread().getName()+"execute task: " + s);
} catch (InterruptedException e) {
System.out.println("退出getTask()线程!");
return; // 当外部嗲用中断方法时,退出执行
}
}
});
t.start();// 启动线程
ts.add(t);// 将该线程加入到集合中
}
// 创建执行addTask()的线程
Thread add = new Thread(() -> {
for (int i=0; i<10; i++) {
// 放入task:
String s = "t-" + Math.random();
System.out.println("add task: " + s);
q.addTask(s);// 将数据加入到队列中去
try { Thread.sleep(100); } catch(InterruptedException e) {}
}
});
add.start();// 启动线程
add.join();// 当前线程插队
Thread.sleep(100);// 主线程休眠100ms
for (Thread t : ts) {// 调用线程的中断方法,当前正在执行sleep/wait的线程会触发中断异常
t.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() throws InterruptedException {
if (!queue.isEmpty()) {
return queue.remove();
}
return "当前队列没有任何参数,删除失败!";
}
}
运行效果:
程序会一直循环的去执行 getTask()方法,虽然在主线程中调用了 t.interrupt();方法,但是并不会引发中断异常,因为需要线程正在执行sleep/wait时才会触发,所以这5个执行 getTask()的线程会一致执行下去。
当队列没有数据时,5个线程会一致执行,并且是无意义的执行,这样非常消耗CPU资源,这时我们可以通过wait方法去暂停该线程,直到队列里面有数据后才去执行(通过notifyAll()唤醒所有线程),如下:
运行效果:
程序执行后不在是死循环了,但是出现了相应的异常信息!
上面出现的异常信息是因为当执行addTask()方法时会让所有线程都唤醒,假设5个线程唤醒,队列里的数据只有3个时,当第四个线程从wait返回的时候,去执行remove()方法时就会抛出异常,因为队列里没有数据,if判断并没有判断到,因为用if判断只能判断一次,而且这一次在我们执行wait之前就已经执行,所以当我们返回的时候没有再一次判断当前队列中的数据是否为空,而直接去执行remove()方法,解决办法非常简单将if换成while即可,如下:
再次运行:
十.锁
1.ReentrantLock
从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁。
传统的synchronized代码(前面售票的例子):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SynChronZedTest {
public static void main(String[] args) {
RunnableNew runnableNew = new RunnableNew();
Thread thread = new Thread(runnableNew);
thread.setName("售票窗口1");
thread.start();
Thread thread2 = new Thread(runnableNew);
thread2.setName("售票窗口2");
thread2.start();
Thread thread3 = new Thread(runnableNew);
thread3.setName("售票窗口3");
thread3.start();
Thread thread4 = new Thread(runnableNew);
thread4.setName("售票窗口4");
thread4.start();
}
}
class RunnableNew implements Runnable {
private static Integer tick = 50;
@Override
public void run() {
while (true) {
synchronized (this) {
if (tick <= 0) {
System.out.println("售票结束");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 进行出售
System.out.println(Thread.currentThread().getName() + "售出,还剩票数" + (--tick));
}
}
}
}
如果用ReentrantLock替代,可以把代码改造为:
因为synchronized是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。顾名思义,ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。和synchronized不同的是,ReentrantLock可以尝试获取锁:
上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
2.Condition
使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。但是,synchronized可以配合wait和notify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?答案是使用Condition对象来实现wait和notify的功能。
我们仍然以TaskQueue为例,把前面用synchronized实现的功能通过ReentrantLock和Condition来实现:
前面的实例:
修改后的:
可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:
- await()会释放当前锁,进入等待状态;
- signal()会唤醒某个等待线程;
- signalAll()会唤醒所有等待线程;
- 唤醒线程从await()返回后需要重新获得锁。
此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来:
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
3.ReadWriteLock
在进行读操作的时候线程的同步并不重要,虽然保证写操作的同步性但是在一些使用读操作的场景下这样操作效率太低,我可以通过ReadWriteLock锁实现读取的时候可以有多个读操作,写的时候只有一个操作。
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.IntStream;
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();// 读锁
private final Lock wlock = rwlock.writeLock();// 写锁
private int[] counts = new int[2];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread writerThread = new Thread(()->{
counter.inc(0);
System.out.println("写入数据:(1,0)");
counter.inc(1);
System.out.println("写入数据:(1,1)");
});
writerThread.start();
for (int i = 0; i < 1000; i++) {
Thread readThread = new Thread(()->{
int[] ints = counter.get();
System.out.println("("+ints[0]+","+ints[1]+")");
});
readThread.start();
}
}
}
运行效果:
把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。
4.StampedLock
如果我们深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。 StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
我们来看例子:
import java.util.concurrent.locks.StampedLock;
public class Point {
private final StampedLock stampedLock = new StampedLock();// 乐观锁
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
System.out.println("写入数据:("+x+","+y+")");
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public void distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
System.out.println("【验证失败】读取数据("+currentX+","+currentY+")");
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}else {
System.out.println("【验证成功】读取数据("+currentX+","+currentY+")");
}
}
public static void main(String[] args) {
Point point = new Point();
Thread writerThread= new Thread(()->{
point.move(100,200);
point.move(300,300);
point.move(500,200);
});
writerThread.start();
for (int i = 0; i < 1000; i++) {
Thread readThread = new Thread(()->{
point.distanceFromOrigin();
});
readThread.start();
}
}
}
运行效果:
和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。StampedLock还提供了更复杂的将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。