线程的一些方法使用
- Thread.currentThread.getName(); 得到线程的名字(执行此代码的线程对象,即当前线程对象)
- sleep(long time); 线程休眠
- isDaemon(); 测试此线程是否为守护程序线程。
- interrupt(); 中断此线程
为什么结束线程不用stop()?那用什么来结束线程?
答:一个线程就是一个独立的执行路径,他是否应该结束应该由自身决定,
因为在启动线程后,我们为这个线程安排任务,这个线程从开始到结束一定是
为了完成某个任务,在完成任务的过程中,他有可能会使用到n个资源,也必须
释放掉这n个资源,这时问题就来了。若想结束掉这个线程,假如从外部直接掐死
这个线程,极有可能导致资源还没来得及释放而一直占用。其他的线程就没法得到
这个资源的使用权。所以线程的结束应该由自己决定更为合理,具体使用的方法是
线程的中断方法 interrupt()。
下面就用代码来描述线程的中断场景。
/**
主线程从0打印到4,子线程从0打印到9,加上sleep()方法实现间隔打印的方式
当主线程打印到4的时候,标记子线程中断
interrupt()告诉子线程你自己该死亡了,这时子线程的处理方式就进入到下面的
try catch块,程序员来决定是否死亡,若是让其死亡应释放完应有的资源后直接return
*/
public class TestInterrupted {
public static void main(String[] args) throws InterruptedException {
//创建线程并给任务
Thread my = new Thread(new MyRunnable());
my.setName("子线程");
my.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(1000);
}
//主线程执行完毕之后,子线程直接中断
my.interrupt(); //加了中断的标记
}
static 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();
System.out.println("发现了系统中断,我就是不中断");*/
//处理中断---就是中断
System.out.println("发现了系统中断,中断系统");
return;
}
}
}
}
}
由此我们应该使用线程中断的方式来结束线程的生命而不是使用stop(),建议大家尽量少使用stop()方法。
线程死锁
比如说A线程持有A资源,B线程持有B资源,A线程想获取B资源而B线程想获取A资源,由于A线程在等待线程B线程释放B资源,线程B在等线程A释放A资源,此时造成了两线程在互相等待的形式,成为线程死锁。
下面用代码示例描述线程死锁的场景。
public class Demo {
/**
首先要明确一点,在同一个类中,若是加了synchronized 关键字,那么锁住的是同一个对象this
主线程是罪犯对警察说---罪犯:你放了我,我放了人质
子线程是警察对罪犯说---警察:你放了人质,我放了你
当运行程序的时候,两线程可能会同时运行,就是警察运行了say方法的同时罪犯也运行了say方法
而警察中的say方法要调用罪犯中的fun方法,此时罪犯在执行say方法,它里面要调用警察的fun方法
此时两者的资源都是被锁住的,就是说警察要调用罪犯的fun方法,他必须要等罪犯执行完并释放资源后才能进行调用;
同理罪犯要调用警察的fun方法,他必须要等警察执行完并释放资源后才能带调用。
两者目前都是在互相等待的状态,产生了线程死锁。
*/
public static void main(String[] args) {
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c, p).start();
c.say(p);
}
static class MyThread extends Thread {
private Culprit culprit; //外部传递过来
private Police police;
public MyThread(Culprit culprit, Police police) {
this.culprit = culprit;
this.police = police;
}
@Override
public void run() {
police.say(culprit);
}
}
//罪犯类
static class Culprit {
public synchronized void say(Police p) {
System.out.println("罪犯:你放了我,我放了人质");
p.fun(); //警察回应的方法
}
public synchronized void fun() {
System.out.println("罪犯跑走了,罪犯也放了人质");
}
}
//警擦类c
static class Police {
public synchronized void say(Culprit c) {
System.out.println("警察:你放了人质,我放了你");
c.fun(); //人质回应的方法
}
public synchronized void fun() {
System.out.println("人质解救了,最后罪犯跑了");
}
}
}
运行结果(程序产生死锁,执行不下去了):
那么怎么能避免线程死锁的产生?
在任何有可能导致锁产生的方法里不要再调用另一个方法让另外一个锁产生。
多线程的通信
多线程间通讯就是多个线程在操作同一资源,但是操作的动作不同.
(1)为什么要通信
多线程并发执行的时候, 如果需要指定线程等待或者唤醒指定线程, 那么就需要通信.比如生产者消费者的问题,
生产一个消费一个,生产的时候需要负责消费的进程等待,生产一个后完成后需要唤醒负责消费的线程,
同时让自己处于等待,消费的时候负责消费的线程被唤醒,消费完生产的产品后又将等待的生产线程唤醒,
然后使自己线程处于等待。这样来回通信,以达到生产一个消费一个的目的。
(2)怎么通信
在同步代码块中, 使用锁对象的wait()方法可以让当前线程等待, 直到有其他线程唤醒为止.
使用锁对象的notify()方法可以唤醒一个等待的线程,或者notifyAll唤醒所有等待的线程.
多线程间通信用sleep很难实现,睡眠时间很难把握。
(3)多线程通信的一些方法介绍
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程(一般是最先开始等待的线程),而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
接下来我们就说下线程通信中最经典的生产者与消费者的问题。首先我们先来描述一下这种机制,当生产者在生产的过程中,消费者处于等待的状态;当生产者生成完物品时,唤醒消费者,生产者自己处于等待的状态,让消费者进行消费。也就是说一个在生产,在生产的过程中不准许消费;一个在消费,在消费的过程中不能生产。下面我们用厨师(生产者)和服务员(消费者)的例子用代码来描述这种机制。
我们先来看看这种机制所带来的两个问题:
package com.huang.No4.session5.生产者和消费者;
/**
* @author HSD
* @create 2020-10-03 22:02
我们理想的状态是厨师生产出一道菜,服务员就端走一道菜,依次交替执行的
*/
public class Demo1 {
public static void main(String[] args) {
//食物对象可理解为盘子
Food food = new Food();
//厨师生产食物
new Cook(food).start();
//服务员端走盘子
new Waiter(food).start();
}
static 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("煎饼果子", "甜辣味");
}
}
}
}
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(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//模拟生产者和消费者的关系,就创建一个食物对象(可以把这个对象看成盘子)
//厨师生产的时候,盘子归厨师使用(服务员等待)
//服务员上菜的时候,盘子归服务券使用(厨师等待)
static class Food {
private String name;
private String tatse;
public void setNameAndTaste(String name, String tatse) {
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.tatse = tatse;
}
public void get() {
System.out.println("服务员端走的菜是:" + name + "," + tatse);
}
}
}
运行结果如下,发现有不合理的地方,一是煎饼果子应该是甜辣味的,老干妈应该是香辣味的,出现数据匹配错误的问题;二是服务员一连端走了2份老干妈,2份煎饼果子,出现连续端同一道菜的错误。
对于第一种错误的产生,是因为在设置菜的名称和味道时,由于sleep()生产者还没来得及设置菜的味道,消费者的线程就执行端走盘子(get)的操作。解决就是在setNameAndTaste()和get()方法前加锁。
static class Food {
private String name;
private String tatse;
public synchronized void setNameAndTaste(String name, String tatse) {
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.tatse = tatse;
}
public synchronized void get() {
System.out.println("服务员端走的菜是:" + name + "," + tatse);
}
}
虽然说线程排队执行方法了(线程安全了),但是第二种错误还是没有解决,这是因为由于上的的锁不是公平锁,很容易引起一种严重的错误,就是当服务员释放所的时候,由于不公平锁,服务员二话不说直接回首掏继续端盘子(也可以理解为服务员的线程在释放锁后很容易再次抢到执行权,而厨师的线程抢不过它)。这时,我们就用线程睡眠wait()和唤醒线程notify()的方法解决此问题。
public class Demo {
public static void main(String[] args) {
//食物对象可理解为盘子
Food food = new Food();
//厨师生产食物
new Cook(food).start();
//服务员端走盘子
new Waiter(food).start();
}
static 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("煎饼果子", "甜辣味");
}
}
}
}
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(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//模拟生产者和消费者的关系,就创建一个食物对象(可以把这个对象看成盘子)
//厨师生产的时候,盘子归厨师使用(服务员等待)
//服务员上菜的时候,盘子归服务券使用(厨师等待)
static class Food {
private String name;
private String tatse;
//打标记,防止回首掏
private boolean flag = true;
public synchronized void setNameAndTaste(String name, String tatse) {
if (flag) {
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.tatse = tatse;
//此时以生产出菜,必须让服务员端走菜才能再让厨师生产菜
flag = false;
//先唤醒服务员线程
notifyAll();
try {
//再让厨师线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get() {
if (!flag) {
System.out.println("服务员端走的菜是:" + name + "," + tatse);
//服务员已经端走菜了,可以让厨师生产菜了
flag = true;
//先唤醒厨师线程
notifyAll();
try {
//再让服务员线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
这样,通过打标记的方式不让服务员回首掏,通过线程等待和唤起的方式依次循环完成以下的效果。
- 当厨师生产时,服务员等待;
- 当厨师生产完物品时,服务员被唤起消费时,厨师等待;
- 服务员消费完后,唤醒厨师生产,服务员等待;
执行效果如下: