Java基础—多线程(二)

时间:2023-02-16 08:16:51

多线程(二)

一、线程间通信

    1.定义
      线程间通信就是多个线程操作同一资源,但是操作的动作不同。

    2.等待唤醒机制
      等待唤醒机制,是由 wait()notify()notifyAll()等方法组成。对于有些资源的操作,需要一个线程完成一步,进入等待状态,将CPU执行权交由另一个线程,让它完成下一步的操作,如此交替进行。这个过程中,一个线程需要在完成一步操作后,先通知( notify())另一个线程运行,再等待( wait()),进入冻结状态,以此类推。等待中的线程,都储存在系统线程池中,等待这被 notify()唤醒。

以下代码,通过等待唤醒机制,实现了生产一个披萨,消费一个披萨:

package com.heisejiuhuche;

public class ProductionConsumptionModel {
public static void main(String[] args) {
Pizza pizza = new Pizza();

new Thread(new Producer(pizza)).start();
new Thread(new Consumer(pizza)).start();
}
}

class Pizza {
private String pizza;
private int count = 1;
//包子存在与否的旗标,false代表没有pizza,true代表有
private boolean flag = false;

public synchronized void producePizza(String pizza) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.pizza = pizza + "---" + count++;
System.out.println(Thread.currentThread().getName() + "-生产-----"
+ this.pizza);
flag = true;
notify();
}

public synchronized void consumePizza() {
//如果没有pizza,则执行生产包子的代码
if (!flag) {
try {
//如果有pizza,则线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "-消费--------------" + this.pizza);
//如果没有pizza,生产完之后,将flag设为true
flag = false;
//线程进入冻结状态之前,通知另一线程开始启动,消费pizza
notify();
}
}

class Producer implements Runnable {
private Pizza pizza;

Producer(Pizza pizza) {
this.pizza = pizza;
}

public void run() {
while (true) {
pizza.producePizza("pizza");
}
}
}

class Consumer implements Runnable {
private Pizza pizza;

Consumer(Pizza pizza) {
this.pizza = pizza;
}

public void run() {
while (true) {
pizza.consumePizza();
}
}
}

程序运行部分结果如下:

Thread-1-消费--------------pizza---20609
Thread-0-生产-----pizza---20610
Thread-1-消费--------------pizza---20610
Thread-0-生产-----pizza---20611
Thread-1-消费--------------pizza---20611

    3.Object类中的wait等方法
      wait()等多线程同步等待唤醒机制中的方法,被定义在 Object类中是因为:
      首先,在等待唤醒机制中,无论是等待操作,还是唤醒操作,都必须标识出等待的这个线程和被唤醒的这个线程锁持有的锁;表现为代码是: 锁.wait(); 锁.notify();而这个锁,由 synchronized关键字格式可知,可以是任意对象;那么,可以被任意对象调用的方法,一定是定义在了 Object类当中。 wait()notify()notifyAll()这些方法都被定义在了 Object类中,因为这些方法是要使用在多线程同步的等待唤醒机制当中,必须具备能被任意对象调用的特性。所以,这些方法要被定义在 Object类中。

    4、生产者消费者模型
      在实际生产时,会有多个线程负责生产,多个线程负责消费;那么在上述代码中启动新线程,来模拟多线程生产消费的情况。

示例代码:

package com.heisejiuhuche;

public class ProductionConsumptionModel {
public static void main(String[] args) {
Pizza pizza = new Pizza();

//两个线程负责生产,两个线程负责消费
new Thread(new Producer(pizza)).start();
new Thread(new Producer(pizza)).start();
new Thread(new Consumer(pizza)).start();
new Thread(new Consumer(pizza)).start();
}
}

用这样的方式,运行会出现如下结果:

Thread-0-生产-----pizza---198
Thread-1-生产-----pizza---199
Thread-2-消费--------------pizza---199

生产了两个披萨,但只消费了一个。现在0,1线程负责生产,2,3线程负责消费,原因推断:
1)当0线程生产完一个披萨,进入冻结;
2)1线程判断有披萨,进入冻结;
3)2线程消费一个披萨,唤醒0线程,进入冻结;
4)3线程判断没披萨,进入冻结;
5)现在出于运行状态的只有0线程,0线程生产一个披萨,唤醒1线程(1线程是线程池中第一个线程),进入冻结;
6)1线程又生产了一个披萨

这导致了生产两个,只消费一个的问题。这个问题的发生是因为,第50线程唤醒1线程的时候,由于1线程的等待代码在if语句中,1线程醒了之后,不需要再判断flag的值所导致。如果1线程被唤醒,还要继续判断flag的值,就不会产生这个情况。因此,要将if判断,改为while循环,让线程被唤醒之后,再次判断flag的值。

示例代码:

while (flag) {
try {
wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}

}

每次被唤醒,都要判断flag的值。代码运行结果如下:

Thread-0-生产-----pizza---1
Thread-2-消费--------------pizza---1
Thread-0-生产-----pizza---2
Thread-3-消费--------------pizza---2

程序出现了无响应,因为使用while循环,可能会出现所有线程全部进入冻结状态的情况。要解决这个问题,必须用到另一个方法notifyAll();唤醒所有线程。由于用了while循环,所有线程被唤醒之后第一件事是判断flag的值,所以不会再出现多生产或多消费问题。至此,程序运行正常。

示例代码:

public synchronized void consumePizza() {
while(!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "-消费--------------" + this.pizza);
//如果没有pizza,生产完之后,将flag设为true
flag = false;
//线程进入冻结状态之前,唤醒所有其他线程
notifyAll();
}
}

程序运行部分结果:

Thread-2-消费--------------pizza---198
Thread-0-生产-----pizza---199
Thread-3-消费--------------pizza---199
Thread-1-生产-----pizza---200
Thread-3-消费--------------pizza---200

二、jdk5新特性

    1.概述
      jdk5开始,提供了多线程同步的升级解决方案。将 synchronized关键字,替换成 Lock接口;将 Object对象,替换为 Condition对象;将 wai()notify()notifyAll()方法,替换为 await()signal()signalAll()方法。一个锁,可以对应多个 Condition对象。这个特性的出现,可以让多线程在唤醒其他线程时,不必唤醒本方的线程,只唤醒对方线程。例如在生产者消费者模型中,使用 LockCondition类,可以实现只唤醒消费者线程,或只唤醒生产者线程。

    2.Lock接口和Condition接口
      1)Lock接口已知实现类中,有ReentrantLock类。这个子类可以用来实例化,创建ReentrantLock对象

ReentrantLock lock = new ReentrantLock();

      2)Condition接口的实例可以通过newCondition()方法获得

Condition conditon = Lock.newCondition();

      3)一个Lock对象可以对应多个Condition对象

Condition condition1 = Lock.newCondition();
Condition condition2 = Lock.newCondition();

    3.新特性应用
      将此新特性应用在消费者生产者模型中,实现只唤醒对方线程。

修改之后的Pizza类代码如下:

class Pizza {
private String pizza;
private int count = 1;
private boolean flag = false;

//获取Lock和Condition对象
private final ReentrantLock lock = new ReentrantLock();
//分别指定生产者和消费者的Condition对象
private final Condition conditionPro = lock.newCondition();
private final Condition conditionCon = lock.newCondition();

public void producePizza(String pizza) {
//上锁
lock.lock();
try {
while (flag) {
//如果有披萨,线程冻结
conditionPro.await();
}
this.pizza = pizza + "---" + count++;
System.out.println(Thread.currentThread().getName() + "-生产-----"
+ this.pizza);
flag = true;
//只唤醒消费者线程中的一个
conditionCon.signal();
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
//这是一定要执行的代码,解锁
lock.unlock();
}

}

public void consumePizza() {
lock.lock();
try {
while(!flag) {
conditionCon.await();
}
System.out.println(Thread.currentThread().getName()
+ "-消费--------------" + this.pizza);
flag = false;
conditionPro.signal();
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

分别创建的conditionProconditionCon对象,用于实现只唤醒对方线程,代码更优。

三、停止线程

    1.线程停止原理
      stop()方法已经过时,停止的唯一标准就是 run()方法结束。开启多线程运行,运行代码通常都是循环结构,只要控制住循环,就可以让 run()方法结束,就可以让线程结束。

注意:
当线程处于冻结状态,无法读取控制循环的标记,线程就不会结束。

    2.interrupt()方法
      将处于冻结状态的线程,强制恢复到运行状态。 interrupt()方法是在清除线程的冻结状态。

示例代码:

package com.heisejiuhuche;

public class InterruptTest {
public static void main(String[] args) {
int x = 0;

Interrupt inter = new Interrupt();

Thread t1 = new Thread(inter);
Thread t2 = new Thread(inter);

t1.start();
t2.start();

while(true) {
System.out.println(Thread.currentThread().getName() + "run....");
if(x++ == 60) {
//强制t1 t2恢复运行状态,抛出异常
t1.interrupt();
t2.interrupt();
break;
}
}
System.out.println("over");
}
}

class Interrupt implements Runnable {
//循环控制变量
private boolean flag = true;

public synchronized void run() {
while(flag) {
try {
//让t1 t2进入冻结状态
this.wait();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "Interrupt Exception.....");
//处理完异常,改变flag的值,下次判断时,结束循环
changeFlag();
}
System.out.println(Thread.currentThread().getName() + " Interrupt run.....");
}
}

public void changeFlag() {
flag = false;
}
}

如果不调用t1t2线程的interrupt()方法,程序会无响应,因为两个线程都处于冻结状态,无法继续运行。

上述程序运行结果:

mainrun....
over
Thread-1Interrupt Exception.....
Thread-1 Interrupt run.....
Thread-0Interrupt Exception.....
Thread-0 Interrupt run.....

四、Thread类其他方法

    1.setDaemon()方法
      将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。该方法必须在启动线程前调用。守护线程可以理解为后台线程。后台线程开启后,会和前台线程(一般线程)一起抢夺CPU资源;当所有前台线程结束运行后,后台线程自动结束。可以理解为,后台线程依赖前台线程的运行。

示例代码:

package com.heisejiuhuche;

public class InterruptTest {
public static void main(String[] args) {
int x = 0;

Interrupt inter = new Interrupt();

Thread t1 = new Thread(inter);
Thread t2 = new Thread(inter);

t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();

在启动两个线程前,将两个线程设置为守护线程,其他代码不变;那么这两个线程依赖主线程运行;虽然这两个线程都处于冻结状态,但是当主线程运行完毕,这两个守护进程随之结束。

    2.join()方法
      调用 join()方法的线程,在申请CPU执行权。之前拥有CPU执行权的线程,将转入冻结状态,等调用 join()方法的线程执行完毕,再转回运行状态。

示例代码:

package com.heisejiuhuche;

public class JoinTest {
public static void main(String[] args) {
Join j = new Join();

Thread t1 = new Thread(j);
Thread t2 = new Thread(j);

t1.start();
try {
//主线程将CPU执行权交给t1线程,自己转入冻结
//等待t1线程执行完毕,主线程再运行
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();

for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "***" + x);
}
}
}

class Join implements Runnable {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "---" + x);
}
}
}

程序在启动t1线程之后,主线程先等待t1线程打印完100个数;主线程再继续和t2线程交替打印100个数。

    3.yield()方法
      调用 yield()方法的线程,会临时释放执行权,可以达到线程均衡运行的效果。

示例代码:

package com.heisejiuhuche;

public class YieldTest {
public static void main(String[] args) {
Yield j = new Yield();

Thread t1 = new Thread(j);
Thread t2 = new Thread(j);

t1.start();
t2.start();

for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "***" + x);
}
}
}

class Yield implements Runnable {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "---" + x);
Thread.yield();
}
}
}

程序运行部分结果:

Thread-1---51
main***79
Thread-0---46
main***80
Thread-1---52
main***81
Thread-0---47

三个线程均衡执行。

五、多线程开发应用

    多线程应用在程序中的运算需要同时进行的时候,可以提高程序运行的效率。例如, main()方法中有三个循环需要执行,如果是单线程,第二个循环要等待第一个循环执行完才能执行,第三个循环要等第二个循环执行完,如此一来,程序运行效率低下。此时,就可以运用多线程,让三个循环同时运行。

示例代码:

package com.heisejiuhuche;

public class ThreadApplycation {
public static void main(String[] args) {
//主线程执行
for(int x = 0; x < 100; x ++) {
System.out.println("Main thread running ...");
}

//匿名线程执行
new Thread() {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println("Anonymous thread running ...");
}
}
}.start();

//线程r执行
Runnable r = new Runnable() {
public void run() {
for(int x = 0; x < 100; x ++) {
System.out.println("r thread running ...");
}
}
};
new Thread(r).start();
}
}

让主线程,匿名线程和r线程,同时开始运算。