死锁:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。
死锁在开发中,也会遇到,当线程进入到死锁状态时,程序中线程就会一直处于等待状态。
死锁的发生:
举例1:
我请柳岩吃饭(麻辣烫)
我点了一份。上了一双筷子
我:一支
柳岩:一支
我和柳岩就相当于两个线程,而麻辣烫相当于共享资源,两个人都没办法吃,都在等待看着麻辣烫,这种现象就是死锁。
举例2:有两个线程(t1线程、t2线程),有两个对象锁(lock_a、lock_b)
t1线程在执行时,先拿到lock_a对象锁(此时lock_a对象锁绑定在t1线程上)。而正在此时CPU切换到t2线程上,t2线程拿到lock_b对象锁(此时lock_b对象锁绑定在t2线程上),这时CPU又切换到t1线程上,这时t1线程需要拿lock_b对象锁,此时t1线程获取不到lock_b对象锁(t1线程处于等待)。
当CPU切换到t2线程上,这时t2线程需要拿lock_a对象锁,此时t2线程获取不到lock_a对象锁(B线程处于等待)。
上述案例2简化如下说法:
有2个线程,需要执行相同的任务,但是需要分别获取的A锁和B锁才能去执行,第一个线程获取锁的顺序是先A后B。第二个线程获取锁的顺序是先B后A。
当第一个线程获取A锁,CPU切换到第二个线程,此时第二个线程获取B锁。而此时第一个线程缺少B锁,第二个线程缺少A锁。两个线程都在等待,发生死锁现象。
有多把锁
有多个线程
有同步代码块嵌套
分析和步骤:
1)创建一个任务类DeadLockTask 实现Runnable接口,复写run函数;
2)创建两个Object类的对象lock_a,lock_b作为锁对象;
3)定义一个变量flag,让不同的线程切换到不同的地方去执行,按照不同的方式来获取锁;
4)在run函数中使用if-else结构来控制两个线程去执行不同的内容,并使用while循环一直让其执行;
5)在if中嵌套书写两个同步代码块lock_a和lock_b分别作为两个代码块的锁,将if中相同的内容复制一份写到else中;
6)创建测试类DeadThreadLockDemo,在这个类的主函数中创建任务类的对象;
7)创建两个线程对象t1和t2;
8)让主线程休息1毫秒;
9)使用t1对象调用start函数开启线程,让下一个线程进入到else中;
10)开启t2线程;
代码如下所示:
/*
* 演示线程死锁的问题
*/
//定义一个线程任务类
class DeadLockTask implements Runnable
{
//定义两个锁对象
private Object lock_a=new Object();
private Object lock_b=new Object();
//定义一个变量作为标记,控制取锁的方式
boolean flag=true;
public void run() {
//当线程进来之后,一个线程进入到if中,另一个进入到else中
if(flag)
{
while(true)
{
synchronized(lock_a)
{
System.out.println(Thread.currentThread().getName()+"if.....lock_a");
synchronized(lock_b)
{
System.out.println(Thread.currentThread().getName()+"if.....lock_b");
}
}
}
}else
{
while(true)
{
synchronized(lock_b)
{
System.out.println(Thread.currentThread().getName()+"else.....lock_b");
synchronized(lock_a)
{
System.out.println(Thread.currentThread().getName()+"else.....lock_a");
}
}
}
}
}
}
public class DeadThreadLockDemo {
public static void main(String[] args) {
// 创建任务类对象
DeadLockTask dlt = new DeadLockTask();
//创建线程对象
Thread t1 = new Thread(dlt);
Thread t2 = new Thread(dlt);
//开启第一个线程
t1.start();
//修改标记让下一个线程进入到else中
dlt.flag=false;
t2.start();
}
}
上述代码的结果如下图所示:
通过以上结果发现,线程一直在执行else中的代码,根本就没有执行if语句中的代码。
说明:
出现上述结果是因为在主线程开启第一个线程之后,很有可能CPU还在主线程上运行,那么开启的线程是不会被CPU立刻去执行,而CPU继续处理主线程中的代码, 就会直接去执行d.flag = false; ,这时就已经把标记修改成false,不管线程是否进入到run 方法中,flag都已经变成false,那么就无法在进入if中,因此我们为了保证第一个线程一定能够进入到if中,于是在这里让主线程在开启第一个线程之后,主线程进行休眠1毫秒。
死锁的结果如下图所示:
注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。
在开发中,死锁现象可以避免,但不能直接解决。当程序中有多个线程时,并且多个线程需要通过嵌套对象锁(在一个同步代码块中包含另一个同步代码块)的方式才可以操作代码,此时就容易出现死锁现象。
可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)
线程由生到死的完整过程:技术素养和面试的要求。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典叫法) |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。
我们的卖票案例中,所有的线程都是在做相同的任务:卖票。而我们真实情况中,不同的线程有可能需要去做不同的任务。在程序中,最常见的一种模型,就是生产者和消费者模型。生产者线程和消费者线程之间需要进行通信,我们可以使用等待唤醒机制来实现生产者线程和消费者线程之间的通信。
Object类的方法
wait() :让当前线程进入等待状态
notify() :唤醒一个正在等待的线程,唤醒是随机的
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
注意事项: 必须要使用锁对象来调用的。
两个方法的小疑问
等待和唤醒的方法为什么要定义在Object类中?
因为需要用锁对象调用这两个方法,任意对象都可以作为锁对象。
也就是说任意类型的对象都可以调用的两个方法,就需要定义在Object类中
两个方法必须写在同步里面吗?
两个方法必须要在同步里面调用,因为在同步里面才有锁对象。
如果一个线程执行了wait()方法,那么当前线程进入等待状态,并且会释放锁对象,下次即使被唤醒必须获取到锁对象才可以执行。
代码演示:
public class MyRun1 implements Runnable {
@Override
public void run() {
synchronized ("abc"){
try {
//等待
"abc".wait();
} catch (InterruptedException e) {
}
}
System.out.println(1);
System.out.println(2);
}
}
public class MyRun2 implements Runnable {
@Override
public void run() {
synchronized ("abc"){
//notify()也必须要用锁对象调用
"abc".notify();
System.out.println("A");
System.out.println("B");
}
}
}
public class Test01 {
public static void main(String[] args) throws Exception{
//只能用锁对象调用 别的对象调用就会报错
// "abc".wait();
//开启线程
MyRun1 mr = new MyRun1();
new Thread(mr).start();
//睡一秒钟
Thread.sleep(1000);
MyRun2 mr2 = new MyRun2();
new Thread(mr2).start();
}
}
包子案例
说明:
1.定义一个包子类,类中成员变量:
pi //皮儿
xian //馅儿
flag://用来表示有没有包子,用true来代表有 用false来代表没有
2.定义一个生产包子的任务类即生产者线程类:
生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
3.定义一个消费包子的任务类即消费者线程类:
消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
/*
包子类需要定义3个成员变量:
pi
xian
flag:表示是否有包子
*/
//包子类
public class BaoZi {
//皮儿
String pi;
//馅儿
String xian;
//布尔值
boolean flag=false; //用来表示有没有包子,用true来代表有 用false来代表没有
}
//生产包子:生产者线程执行的任务
/*
生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
*/
public class ZhiZuo implements Runnable {
//成员变量
BaoZi baoZi;
//构造方法
public ZhiZuo(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
//制作包子
while (true){
synchronized ("锁"){//t1
if(baoZi.flag == true){
//如果有包子就不需要制作
//就让制作的线程进入等待状态
try {
"锁".wait();
} catch (InterruptedException e) {
}
}
//表示没有包子
//制作包子
baoZi.pi = "白面";
baoZi.xian = "韭菜大葱";
//修改包子状态
baoZi.flag = true;
System.out.println("生产出了一个包子!");
//生产好了包子叫醒吃货(消费者)来吃
"锁".notify();
}
}
}
}
//吃包子:消费者线程执行的任务
/*
消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
*/
public class ChiHuo implements Runnable {
//成员变量
BaoZi baoZi;
//构造方法
public ChiHuo(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
//吃包子
while(true){
synchronized ("锁"){
if(baoZi.flag == false){
//没包子
//让吃包子的线程进入等待
try {
"锁".wait();
} catch (InterruptedException e) {
}
}
//表示有包子
//开吃
System.out.println("吃货吃了一个" + baoZi.pi+"皮儿," + baoZi.xian + "馅儿的大包子");
baoZi.pi = null;
baoZi.xian = null;
//修改包子状态
baoZi.flag = false;
//吃完包子叫醒对方(生产者)来做
"锁".notify();
}
}
}
}
//测试类
public class Test01 {
public static void main(String[] args) {
//创建包子
BaoZi baoZi = new BaoZi();
//创建对象
ZhiZuo zz = new ZhiZuo(baoZi);
Thread t1 = new Thread(zz);//生产者线程
t1.start();
//创建对象:消费者线程
ChiHuo ch = new ChiHuo(baoZi);
Thread t2 = new Thread(ch);
t2.start();
}
}
功能介绍
定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件 事情。定时器类是java.util.Timer类
方法介绍
构造方法:
public Timer():构造一个定时器
常用方法:
void schedule(TimerTask task, long delay)//在指定的延迟之后安排指定的任务执行。
参数:
task - 所要安排的任务。属于TimerTask类型,java.util.TimerTask是一个实现Runnable接口的抽象类,代表一个可以被Timer执行的任务。我们需要扩展这个类来创建我们自己的TimerTask,它可以使用java Timer类进行调度。
delay - 执行任务前的延迟时间,单位是毫秒。就是过了多少毫秒之后再执行task任务
void schedule(TimerTask task, long delay, long period)
//在指定 的延迟之后开始,重新执行固定延迟执行的指定任务。
参数:
task - 所要安排的任务。
delay - 执行任务前的延迟时间,单位是毫秒。
period - 执行各后续任务之间的时间间隔,单位是毫秒。 就是每隔多长时间执行一次task任务
void schedule(TimerTask task, Date time) //在指定的时间安排指定的任务执行。
参数:
task - 所要安排的任务。
time - 执行任务的时间。 时间一到就会开始执行任务
void schedule(TimerTask task, Date firstTime, long period)
//从指定的时间开始,对指定的任务执行重复 的 固定延迟执行 。
参数:
task - 所要安排的任务。
time - 执行任务的时间。 时间一到就会开始执行任务
period - 执行各后续任务之间的时间间隔,单位是毫秒。 就是每隔多长时间执行一次task任务
代码演示
public class Test01 {
public static void main(String[] args) throws ParseException {
//构造方法:
//public Timer():构造一个定时器
Timer t = new Timer();
//常用方法:
TimerTask tt = new TimerTask() {
@Override
public void run() {
//这里面要写的是执行的代码
System.out.println("起床了");
}
};
//void schedule(TimerTask task, long delay)
//在3秒钟之后会自动执行任务
// t.schedule(tt,3000);
//void schedule(TimerTask task, long delay, long period)
//在3秒钟之后会自动执行任务,每过1秒执行一次
// t.schedule(tt,3000,1000);
String s = "2020-4-9 12:06:40";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//解析:能够包字符串类型变成Date类型
Date date = sdf.parse(s);
//void schedule(TimerTask task, Date time)
//表示时间到2020-4-9 12:06:40的时候就开始执行任务tt
// t.schedule(tt,date);
//
// //void schedule(TimerTask task, Date firstTime, long period)
// //从指定的时间开始,对指定的任务执行重复 的 固定延迟执行 。
//表示时间到2020-4-9 12:06:40的时候就开始执行任务tt,以后每隔2秒执行一次任务tt
t.schedule(tt,date,2000);
}
}
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
做什么,而不是怎么做
例如之前的匿名内部类方式实现多线程案例中。
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。
传统写法,代码如下:
public class Demo03Thread {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
}
}
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析:
对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;run
的方法体,不得不需要Runnable
接口的实现类;RunnableImpl
实现类的麻烦,不得不使用匿名内部类;run
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;Lambda表达式写法,代码如下:
借助Java 8的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
public class Demo04LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
Lambda省去面向对象的条条框框,格式由3个部分组成:
Lambda表达式的标准格式为:
(参数类型 参数名称,参数类型 参数名称,..) -> { 代码语句 }
格式说明:
->
是新引入的语法格式,代表指向动作。匿名内部类与lambda对比:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
仔细分析该代码中,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个方法):
同样的语义体现在Lambda
语法中,要更加简单:
() -> System.out.println("多线程任务执行!")
run
方法的参数(无),代表不需要任何条件;下面举例演示java.util.Comparator
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
public class Person {
private String name;
private int age;
// 省略构造器、toString方法与Getter Setter
}
传统写法
如果使用传统的代码对Person[]
数组进行排序,写法如下:
public class Demo05Comparator {
public static void main(String[] args) {
// 本来年龄乱序的对象数组
Person[] array = { new Person("古力娜扎", 19),new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) };
// 匿名内部类
Comparator<Person> comp = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
};
Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例
for (Person person : array) {
System.out.println(person);
}
}
}
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator
接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析
下面我们来搞清楚上述代码真正要做什么事情。
Arrays.sort
方法需要排序规则,即Comparator
接口的实例,抽象方法compare
是关键;compare
的方法体,不得不需要Comparator
接口的实现类;ComparatorImpl
实现类的麻烦,不得不使用匿名内部类;compare
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;Lambda写法
public class Demo06ComparatorLambda {
public static void main(String[] args) {
Person[] array = {
new Person("古力娜扎", 19),
new Person("迪丽热巴", 18),
new Person("马尔扎哈", 20) };
Arrays.sort(array, (Person a, Person b) -> {
return a.getAge() - b.getAge();
});
for (Person person : array) {
System.out.println(person);
}
}
}
省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
备注:如果省略大括号、return关键字及语句分号的原则要省略都省略,要么都不能省略。
可推导即可省略
Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
匿名内部类:
可以用于类也可以用于接口,对类和接口中的方法的个数没有要求。
Lambda表达式:
只能用于接口,接口中抽象方法只能有一个。
Lambda表达式的要求更严格,并不是所有的匿名内部类都能改成Lambda表达式。