Java基础—多线程(二)

多线程(二)

一、线程间通信

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线程,同时开始运算。