文章目录
- 多线程
-
- 一、线程与进程
- 二、线程调度
- 三、同步与异步&并发与并行
- 四、多线程实现方式
-
- 1、继承Thread
- 2、实现Runnable
- 3、实现Runnable和继承Thread比较
- 4、匿名内部类实现方式
- 五、Thread类
- 六、设置和获取线程名称
- 七、线程休眠sleep
- 八、线程的中断
- 九、守护线程
- 十、线程安全问题
-
- 1、同步代码块
- 2、同步方法
- 3、显式锁Lock
-
- 显式锁和隐式锁的区别
- 4、线程死锁
-
- 1.多线程通信问题
- 十一、线程的六种状态
- 十二、带返回值的线程Callable
- 十三、线程池
-
- 1、缓存线程池
- 2、定长线程池
- 3、单线程线程池
- 4、周期性任务定长线程池
- 十四、Lambda表达式
- 总结
多线程
多线程:栈空间独立,堆内存共享。
一、线程与进程
- 进程
正在运行的应用程序:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
每个进程都有着自己独立的堆、栈且是互不共享的
- 线程
- 进程中的一个执行路径(一个应用程序从执行到结束的整个过程),共享一个内存空间,一个进程中可以包含多条线程;
- 线程之间可以自由切换,并发执行;
- 一个进程最少有一个线程,线程控制着进程。
二、线程调度
-
分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
-
抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为 抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
三、同步与异步&并发与并行
同步:排队执行 , 效率低但是安全
异步:同时执行 , 效率高但是数据不安全
并发:指两个任务在同一时间段内都请求运行
处理器只能接收一个任务,把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行
- 如果用一台电脑先给友人A发个消息,然后立刻再给友人B发消息,然后再跟友人A聊,再跟友人B聊。这就叫并发。
并行:两个任务同一时刻运行,需要多核CPU
- 比如跟两个朋友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。
四、多线程实现方式
1、继承Thread
步骤:
- 定义MyThread类并继承Thread类;
- 重写run方法,新的执行路径(通过thread对象的start()启动任务)
/**
* @author Elvira
* @date 2020/10/12 16:01
* @description 继承Thread
*/
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("嘿……"+i);
}
}
}
- 创建子类对象
- 调用start(),让线程执行
/**
* @author Elvira
* @date 2020/10/12 16:08
* @description 继承Thread的程序入口
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0; i < 10; i++) {
System.out.println("嘻嘻……"
+i);
}
}
}
- 运行结果:可以看到顺序并不统一,这是因为线程在抢占时间片,谁先抢到就是谁的
线程时序图:
注意:运行过程中子线程任务中调用的方法都在子线程中运行
2、实现Runnable
步骤:
- 实现Runnable接口;
- 实现抽象方法run(),编写线程的任务;
/**
* @author Elvira
* @date 2020/10/12 16:22
* @description 实现Runnable
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("哼哼!" + i);
}
}
}
- 创建一个任务对象
- 创建一个线程,并给它一个任务
- 调用start()方法执行线程
/**
* @author Elvira
* @date 2020/10/12 16:30
* @description 实现Runnable的程序入口
*/
public class RunnableTest {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("呵呵?" + i);
}
}
}
- 运行结果:
3、实现Runnable和继承Thread比较
继承Thread
- 优点:直接使用Thread类中的方法,代码简单
- 弊端:如果已有父类,不可用
实现Runnable接口(更常用)
-
其优点:
1. 通过创建任务,给线程分配任务实现多线程,更适合多个线程同时执行相同任务的情况
- 可以避免单继承带来的局限性
- 任务和线程分离,提高程序健壮性
- (最重要)后续提供的线程池任务,接收Runnable类型任务,不接收Thread类型线程
- 弊端:不能直接使用Thread中的方法,需先获取线程对象,再得到Thread的方法,代码复杂
4、匿名内部类实现方式
- 继承Thread
/**
* @author Elvira
* @date 2020/10/12 16:52
* @description 继承thread的匿名内部类实现方式
*/
public class MyThread0 {
public static void main(String[] args) {
new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("一二三四五" + i);
}
}
}.start();
for (int i = 0; i < 10; i++) {
System.out.println("啦啦啦啦啦" + i);
}
}
}
- 运行结果:
- 实现Runnable
new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("aaa" + i);
}
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("bbb" + i);
}
- 运行结果:
五、Thread类
接下来列举API中Thread类最常用的方法:
构造器 | 描述 |
---|---|
| 分配新的 对象。 |
| 分配新的 对象。 |
| 分配新的 对象。 |
| 分配新的 对象。 |
常见方法:
变量和类型 | 方法 | 描述 |
---|---|---|
| | 返回此Thread的标识符。 |
| | 返回此线程的名称。 |
| | 返回此线程的优先级。 |
| | 更改此线程的优先级。 |
| | 导致此线程开始执行; Java虚拟机调用此线程的 方法。 |
| | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 |
| | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。 |
| | 将此线程标记为 daemon线程或用户线程。 |
特殊字段:控制线程抢到时间片的几率
变量和类型 | 字段 | 描述 |
---|---|---|
| | 线程可以拥有的最大优先级。 |
| | 线程可以拥有的最低优先级。 |
| | 分配给线程的默认优先级。 |
六、设置和获取线程名称
首先需要了解:currentThread() 可以获取当前正在执行的线程对象
- 获取线程名称
/**
* @author Elvira
* @date 2020/10/12 17:41
* @description 获取线程名称
*/
public class NameTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
//给线程指定一个名称
new Thread(new MyRunnable(), "Elvira").start();
}
}
实现类:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
- 运行结果:
如果不指定线程名称,直接给线程一个任务:再加两句
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
- 运行结果:
- 设置线程名称
方式一:上述,直接在创建线程任务时指定名称
//给线程指定一个名称
new Thread(new MyRunnable(), "Elvira").start();
方式二:setName()设置
Thread t = new Thread(new MyRunnable());
t.setName("THREAD");
t.start();
- 运行结果:
七、线程休眠sleep
sleep是Thread类的静态方法,类名直接调用即可。单位ms。
/**
* @author Elvira
* @date 2020/10/12 20:13
* @description 线程休眠3秒
*/
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(3000);
}
}
}
-
运行结果:
每隔3秒蹦出来一个数字,最终打印了10个数字
线程阻塞:所有较耗时的操作都能称为阻塞。也叫耗时操作。
八、线程的中断
一个线程是一个独立的执行路径,它是否应该结束,由其自身决定。
因为线程执行过程会有很多资源需要使用或释放,如果干涉它的结束,很可能导致资源没能来得及释放,一直占用产生无法回收的内存垃圾。
早期有stop()方法可以结束线程,现在已经过时。现在出了新的方法,给线程打标记来控制它的结束。
标记是什么:interrupt
示例:
第一步:先设置main线程和Thread-0线程都执行循环数字1-10的任务
/**
* @author Elvira
* @date 2020/10/12 20:28
* @description 线程中断
*/
public class InterruptTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
实现类:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 运行结果:
第二步:给线程t1添加中断标记interrupt()
/**
* @author Elvira
* @date 2020/10/12 20:28
* @description
*/
public class InterruptTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t1.interrupt();
}
}
实现类:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("发现了中断标记,还没有停止。");
}
}
}
}
- 运行结果:
解释:发现了中断就会捕捉异常进入catch块,程序员决定该如何处理。不进行任何处理程序还会继续执行。此时在catch块中加入return,程序就会中断。
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("发现了中断标记,在此处停止。");
return;
}
}
}
}
- 运行结果:线程结束(”自杀“)
九、守护线程
线程分为守护线程和用户线程
- 用户线程:当一个进程不包含任何存活的用户线程时,进行结束。
- 守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
设置线程为守护线程:启动之前设置
完整代码:
/**
* @author Elvira
* @date 2020/10/13 9:32
* @description
*/
public class SetDaemonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.setDaemon(true);
t1.start();
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
实现类:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
}
}
- 运行结果:
十、线程安全问题
举一个生活中的小栗子,模拟买票窗口,创建三个线程,一起卖10张票。此时就会出现线程不安全的问题。
/**
* @author Elvira
* @date 2020/10/13 9:49
* @description
*/
public class ThreadSafeTest {
public static void main(String[] args) {
Runnable run = new Ticket();
//创建三个线程
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
买票任务:
/**
* 模拟买票窗口
*/
public class Ticket implements Runnable {
private int count = 10;//票数
@Override
public void run() {
while (count > 0) {
//开始卖票
System.out.println("正在卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票:" + count);
}
}
}
- 运行结果
结果:发现票数出现负数,而我们设定只有票数大于0(count>0)时才会进入循环。
原因:线程争抢,导致线程不安全。 多线程在进行同一卖票任务时,没人干涉,各个窗口疯狂买票,最终导致卖的票出现负数。
线程不安全的原因
- ★当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
如何保证线程安全性?
让它们排队执行,一个一个来就安全了。这个排队方式就是给线程加锁。
1、同步代码块
- 使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
- 多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
语法格式:
synchronized(锁对象) {}
任何对象都可以作为锁存在。可以一行代码加锁。
例:
/**
* 模拟卖票窗口
*/
public 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 {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
} else {
return;
}
}
}
}
}
- 运行结果:
2、同步方法
以方法为单位进行加锁。把synchronized关键字修饰在方法中。
例:此时写一个方法sale()执行卖票任务,不给方法加锁
/**
* @author Elvira
* @date 2020/10/13 14:22
* @description 同步方法
*/
public class Ticket implements Runnable {
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if (!flag) {
break;
}
}
}
public boolean sale() {
if (count > 0) {
System.out.println("正在卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
return true;
}
return false;
}
}
}
- 运行结果:
这时线程仍然不安全,我们给方法加上锁(synchronized关键字):
public synchronized boolean sale() {
//卖票逻辑
}
必须是同一个Runnable对象。
- 此时运行结果:安全了
3、显式锁Lock
Lock l = new ReentrantLock():自己创建一把锁
lock():加锁
unlock():解锁
例:
/**
* @author Elvira
* @date 2020/10/13 14:46
* @description 显式锁Lock
*/
public class Ticket implements Runnable {
private int count = 10;//票数
private Lock l = new ReentrantLock();//显式锁
@Override
public void run() {
while (true) {
l.lock();
if (count > 0) {
System.out.println("正在卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
} else {
break;
}
l.unlock();
}
}
}
}
显式锁和隐式锁的区别
隐式锁:同步代码块和同步方法的synchronized
显式锁:Lock
热心网友详解: Java并发之显式锁和隐式锁的区别。
区别 | synchronized | lock |
---|---|---|
原始构成 | Java关键字,由JVM维护,是JVM层面的锁 | JDK1.5之后的类,使用lock是在调用API,是API层面的锁 |
使用方式 | 隐式锁,不需要手动获取和释放锁,只需要写synchronized,不用进行其他操作 | 显式锁,需要手动获取和释放锁,如果没有释放锁,可能会出现死锁 |
等待中断 | 不会中断,除非抛出异常或正常运行完成 | 可以中断,1:调用设置超时方法tryLock(long timeout ,timeUnit unit);2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断 |
加锁公平 | 非公平锁 | 可以是公平锁也可以是非公平锁,默认是非公平锁。可以在其构造方法传入Boolean值,true公平锁,false非公平锁 |
绑定多个条件 | 没有。不能精确唤醒线程,要么随机唤醒一个线程,要么唤醒所有等待线程 | 用来实现分组唤醒需要唤醒的线程,可以精确唤醒线程 |
性能 | JDK1.5时性能较低,JDK1.6时性能优化,与lock相较无异 | JDK1.5时性能更高,JDK1.6时synchronized优化赶上lock |
加锁方式 | 线程获取独占锁(CPU悲观锁机制),只能依靠阻塞等待线程释放锁。在CPU转换线程阻塞时会引起线程上下文切换,当竞争锁的线程过多时,会引起CPU频繁上下文切换导致效率低下 | 使用乐观锁机制(CAS操作 Computer and Swap),假设不会发生冲突,一旦发生冲突失败就重试,直到成功为止。 |
- 公平锁:先来先到
- 非公平锁:大家一起抢,谁抢着是谁的
4、线程死锁
多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁。
当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
一个线程可以拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
例:举一个生活中的小栗子:警察和绑架犯
- Culprit:罪犯类
- Police:警察类
- MyThread:实现Runnable接口的实现类
- WaitTest:程序入口,start线程
罪犯类:
/**
* 罪犯
*/
public class Culprit {
public synchronized void say(Police p) {
System.out.println("罪犯:你放过我,我放了人质。");
p.fun();
}
public synchronized void fun() {
System.out.println("罪犯被放过了,罪犯也放了人质。");
}
}
警察类:
/**
* 警察
*/
public class Police {
public synchronized void say(Culprit c) {
System.out.println("警察:你放了人质,我放过你。");
c.fun();
}
public synchronized void fun() {
System.out.println("警察救了人质,但罪犯跑了。");
}
}
Runnable实现类:写一个能传Culprit和Police对象的构造方法。子线程给p对象的say方法传入对象c,让警察说话,罪犯回应
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() {
p.say(c);
}
}
主线程:启动子线程,给c对象的say方法传入p,让罪犯说话,警察回应。
public static void main(String[] args) {
Culprit c = new Culprit();
Police p = new Police();
new Thread(new MyThread(c, p)).start();
c.say(p);
}
- 运行结果:程序卡在这不动
注意:在开发过程中要尽量避免死锁的发生
如何避免死锁?
在根源解决:在任何可能导致锁产生的方法中,不要调用另一个也可能产生锁的方法。
上面的例子修改一下:在调用罪犯的say方法前,还没启动子线程,就不会锁上警察的say方法。等待罪犯说完,警察也回应完,再启动子线程。
c.say(p);
new Thread(new MyThread(c, p)).start();
- 运行结果:
(题外话:听说电脑越好未阻塞(如上图片中)效果出现越慢,电脑越差出现越快。然后本人电脑一次成功,该攒钱换新电脑了。。。哭唧唧)
1.多线程通信问题
Object类中的相关方法:
变量和类型 | 方法 | 描述 |
---|---|---|
| | 导致当前线程等待它被唤醒,通常是 通知或 中断 。 |
| | 导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
| | 导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
| | 唤醒正在此对象监视器上等待的单个线程。 |
| | 唤醒等待此对象监视器的所有线程。 |
什么时候需要通信
多个线程并发执行时, 在默认情况下CPU是随机切换线程的
如果我们希望他们有规律的执行, 就可以使用通信, 例如每个线程执行一次打印
-
等待 (拿着锁的:高风亮节,让步)
必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。 同时此线程因obj而处在无限期等待 的状态中。释放锁,进入等待队列。
-
通知
必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身没有任何影响。
例:生产者与消费者
- Cook类:厨师类,子线程,做饭
- Waiter类:服务生类,子线程,端饭
- Food类:包含菜的名称和属性,还有设置、获取菜的名称属性的方法
- Test类:主线程,创建食物对象和启动子线程
第一步:先简单罗列
Cook类:
/**
* 厨师
*/
public class Cook extends Thread {
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
f.setNameAndTaste("西红柿炒红薯","五香味");
} else {
f.setNameAndTaste("杂粮奶粉","酸甜味");
}
}
}
}
Waiter类:
/**
* 服务生
*/
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();
}
}
}
Food类:
/**
* 食物
*/
static class Food {
private String name;
private String taste;
public void setNameAndTaste(String name, String taste) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public void get() {
System.out.println("服务员端走了:"+ name +"\t味道:" + taste);
}
}
主线程:
/**
* @author Elvira
* @date 2020/10/13 18:46
* @description 生产者与消费者
*/
public class Test {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
}
- 运行结果:(有问题)
此时出现了问题,杂粮奶粉变成了五香味,西红柿炒红薯变成了酸甜味,结果错乱了。
如何解决这个问题?
第二步:为解决线程错乱,需要进行线程之间的通信
- 给Food类中添加一个标记,标记此时是在做饭还是在端饭。
- 在设置菜名和味道方法中添加条件,在执行完之后设置标记为false,
- 唤醒所有this线程中睡着的线程(端菜)
- 再让Cook线程睡着
if (flag == true) { this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false; }
- 在get方法中,flag默认值为false,服务员可以端菜,上菜后flag又变为true,Cook又能做菜
- 再把this中睡着的线程唤醒(厨师)
- 再让自己(Waiter)睡着
if (!flag) { System.out.println("服务员端走了:"+ name +"\t味道:" + taste); flag = true; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
解释:
- 厨师做完饭,叫醒服务生端菜,自己再睡着;
- 服务生端完菜,叫醒厨师,自己再睡着。
交替完成,保证线程一定不会出错。
十一、线程的六种状态
API中Thread.State给出的线程状态如下:
- 新建(new):线程对象被创建后就进入了新建状态。如:Thread thread = new Thread();
- 就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其他线程调用了该对象的start()方法,从而启动该线程。如:thread.start(); 处于就绪状态的线程随时可能被CPU调度执行。
- 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
-
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权限,暂时停止运行。直到线程进入就绪状态,才有机会进入运行状态。阻塞的三种情况:
1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
notify();随机叫醒同一把锁的线程;
notifyAll();叫醒所有使用同一把锁的线程
2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。
3)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。
-
死亡状态(Dead):线程执行完了或因异常退出了run()方法,该线程结束生命周期。
正常结束
关闭JVM
十二、带返回值的线程Callable
Runnable 与 Callable
接口定义
//Callable接口
public interface Callable<V> {
V call() throws Exception;
}
//Runnable接口
public interface Runnable {
public abstract void run();
}
Callable使用步骤
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
- 创建FutureTask对象 , 并传入第一步编写的Callable类对象
- 通过Thread,启动线程
Runnable 与 Callable的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
Runnable 与 Callable的不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执 行,如果不调用不会阻塞。
十三、线程池
实际用得不多,Java中都是多线程。
线程池 Executors
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
Java中的四种线程池 . ExecutorService
1、缓存线程池
长度没有限制
步骤:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程 并放入线程池, 然后使用
2、定长线程池
长度是指定的数值
步骤:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
3、单线程线程池
步骤:
- 判断线程池 的那个线程 是否空闲
- 空闲则使用
- 不空闲则等待池中的单个线程空闲后使用
4、周期性任务定长线程池
步骤:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务
-
定时执行
参数1. runnable类型的任务
参数2. 时长数字
参数3. 时长数字的单位
-
周期执行
参数1. runnable类型的任务
参数2. 时长数字(延迟执行的时长)
参数3. 周期时长(每次执行的间隔时间)
参数4. 时长数字的单位
十四、Lambda表达式
Lambda表达式是函数式编程思想,Java8版本引入的
- 面向对象:提倡创建对象解决问题
- 函数式编程思想:不关注过程,只注重结果
例1:实现Runnable打印哈哈哈哈哈
- 面向对象:把打印“哈哈哈哈哈”这件事封装在一个任务的对象里
/**
* @author Elvira
* @date 2020/10/14 0:04
* @description 面向对象
*/
public class LambdaTest {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable(){
@Override
public void run() {
System.out.println("哈哈哈哈哈");
}
});
t.start();
}
}
- 运行结果:
- 函数式思想:关注方法,让代码更简单
把这段代码传给Thread()。() -> System.out.println("哈哈哈哈哈")
- ()里是需要传的参数,没有就不写
- ->后面是方法中的内容
Thread t = new Thread(() -> System.out.println("哈哈哈哈哈"));
t.start();
-
运行结果:
和上面一大段代码一模一样
注意:如何使用Lambda表达式
⭐接口中只有一个抽象方法
例2:自定义接口打印sum
- 面向对象
/**
* @author Elvira
* @date 2020/10/14 0:15
* @description 算数和
*/
public class LambdaTest1 {
public static void main(String[] args) {
print(new MyMath() {
@Override
public int sum(int x, int y) {
return x + y;
}
}, 100, 200);
}
public static void print(MyMath m, int x, int y){
int num = m.sum(x, y);
System.out.println(num);
}
static interface MyMath {
int sum(int x, int y);
}
}
- 运行结果:
- 函数思想
print((int x, int y) -> {
return x + y;
},100,200);
-
运行结果:
与上面一致
总结
这篇文章的重点部分除了线程池,其他都很重要,尤其是如何解决死锁问题,更是重中之重。