黑马程序员---第四讲 多线程的应用(2)

来源:互联网 发布:软件数据线6.0 编辑:程序博客网 时间:2024/05/21 10:36

第四讲 多线程的应用(2)

一、线程安全问题的另一解决方案

前面我们已经知道,同步代码块的锁是任意对象,同步方法的锁是this对象,静态方法的锁是类的字节码文件对象。但是前面的方法不够明确,我们很难看到代码是在哪锁的,又是在哪解锁的。为了更清晰的表达在哪里加锁,在哪里解锁,JDK5中提供了Lock锁。
代码实现如下:

package cn.itcast_01;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class MyLock implements Runnable {    private int ticket = 100;    private Lock lock = new ReentrantLock();    @Override    public void run() {        while (true) {            // 加锁            lock.lock();            if (ticket > 0) {                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread().getName()                            + "正在出售第" + (ticket--) + "张票");            }            // 释放锁            lock.unlock();        }    }}
package cn.itcast_01;public class MyLockDemo {    public static void main(String[] args) {        MyLock ml=new MyLock();        Thread t1=new Thread(ml,"窗口一");        Thread t2=new Thread(ml,"窗口二");        Thread t3=new Thread(ml,"窗口三");        t1.start();        t2.start();        t3.start();    }}

这样写代码有一个这样的问题,一旦加锁和释放锁之间的代码出现问题,程序就会被锁在这里,无法执行下面的代码,所以加锁的部分我们通常做如下改进。这样无论中间的代码出现什么问题,我都会释放锁。

package cn.itcast_01;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class MyLock implements Runnable {    private int ticket = 100;    private Lock lock = new ReentrantLock();    @Override    public void run() {        while (true) {            try {                // 加锁                lock.lock();                if (ticket > 0) {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName()                            + "正在出售第" + (ticket--) + "张票");                }            }            finally {                // 释放锁                lock.unlock();            }        }    }}

二、死锁问题

我们知道同步线程的执行效率较低,因为每个线程执行前都要先判断锁对象,但是这个不足我们是可以接受的,因为他毕竟解决了线程安全的问题。但是另外一个缺陷是我们接受不了的,就是“死锁”问题,通俗的理解就是钥匙卡在锁里了,谁也打不开锁了,谁也进不了家了。那么究竟什么是“死锁”呢?死锁是指两个或者两个以上的线程在执行过程中,因争夺资源产生的一种相互等待的现象。如果出现同步嵌套,就容易出现“死锁”问题。让我们来看一段代码理解“死锁”出现的情形。

package cn.itcast_02;public class MyLock {    public static final Object objA=new Object();    public static final Object objB=new Object();}
package cn.itcast_02;public class DieLock extends Thread {    private boolean flag = false;    public DieLock(boolean flag) {        this.flag = flag;    }    @Override    public void run() {        if (flag) {            synchronized (MyLock.objA) {                System.out.println("if objA");                synchronized (MyLock.objB) {                    System.out.println("if objB");                }            }        } else {            synchronized (MyLock.objB) {                System.out.println("else objB");                synchronized (MyLock.objA) {                    System.out.println("else objA");                }            }        }    }}
package cn.itcast_02;public class DieLockDemo {    public static void main(String[] args) {        DieLock dl1=new DieLock(true);        DieLock dl2=new DieLock(false);        dl1.start();        dl2.start();    }}

三、线程间的通信

1.多线程的两种模型

解决上述的死锁问题,就要用到线程间的通信。为了更透彻的理解死锁问题的解决方案,在这里我们先不谈线程通信怎么解决该问题,我们先谈谈什么是线程通信。
前面的售票的例子我们可以用下面的模型来表示,100张票是死的,三个窗口来卖。

enter description here

但是生活中的很多现象并不是这样的,例如卖煎饼我们可以用下面的模型来表示。卖煎饼的前端是买煎饼的也就是消费者,而卖煎饼的后边是后端也就是生产者。消费者和生产者在交易过程中是会沟通的,如果还有煎饼可卖,那么生产者就等着,并叫卖让消费者来买;反之,如果没有煎饼了,消费者会等着,并告知生产者需要生产煎饼了。

enter description here

2.设置、获取线程模型的实现

上面的例子就可以说明线程通信问题,即不同种类的线程间(生产者或消费者)针对同一资源(煎饼)的操作。生产者可以称为设置线程,消费者可以称为获取线程。下面用代码实现上述模型。

package cn.itcast_03;public class Student {    String name;    int age;}
package cn.itcast_03;public class SetThread implements Runnable {    @Override    public void run() {        Student s=new Student();        s.name="刘亦菲";        s.age=27;    }}
package cn.itcast_03;public class GetThread implements Runnable {    @Override    public void run() {        Student s=new Student();        System.out.println(s.name+"---"+s.age);    }}
package cn.itcast_03;public class StudentThread {    public static void main(String[] args) {        SetThread st=new SetThread();        GetThread gt=new GetThread();        Thread t1=new Thread(st);        Thread t2=new Thread(gt);        t1.start();        t2.start();    }}

通过执行上述代码发现,并没有出现我们预期的结果,有设置有获取。通过分析发现,原因是两个线程中的对象不是同一个对象,这样就不符合线程通信的“针对同一资源的操作”。举个简单的例子,买煎饼的和卖肉夹馍的在买卖过程中会有交流吗?答案是显而易见的。
那么我们就要对上述代码进行改进,改进的思路很简单,在测试类中新建对象,把该对象最为参数传递到线程中,就能保证操作的是同一个资源。但是这时候要注意,线程中必须存在该种构造方法。改进代码如下。

package cn.itcast_04;public class SetThread implements Runnable {    private Student s;    private int i = 0;    public SetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            if (i % 2 == 0) {                s.name = "刘亦菲";                s.age = 27;            } else {                s.name = "宋承宪";                s.age = 37;            }            i++;        }    }}
package cn.itcast_04;public class GetThread implements Runnable {    private Student s;    public GetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            System.out.println(s.name + "---" + s.age);        }    }}
package cn.itcast_04;public class StudentThread {    public static void main(String[] args) {        Student s=new Student();        SetThread st=new SetThread(s);        GetThread gt=new GetThread(s);        Thread t1=new Thread(st);        Thread t2=new Thread(gt);        t1.start();        t2.start();    }}

3.线程安全问题的解决

通过运行上述代码,很容易就发现代码存在安全问题。主要有两个问题:一是一个数据出现多次,二是姓名和年龄不匹配。经过分析可知,出现第一个问题的原因是CPU一点点时间片的执行权,就够执行多次循环。出现第二个问题的原因是线程运行的随机性。
通过上一讲我们可以知道,解决安全问题可以通过同步来实现,也就是给线程加锁。解决安全问题的代码实现如下。

package cn.itcast_05;public class SetThread implements Runnable {    private Student s;    private int i = 0;    public SetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            synchronized (s) {                if (i % 2 == 0) {                    s.name = "刘亦菲";                    s.age = 27;                } else {                    s.name = "宋承宪";                    s.age = 37;                }            }            i++;        }    }}
package cn.itcast_05;public class GetThread implements Runnable {    private Student s;    public GetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            synchronized (s) {                System.out.println(s.name + "---" + s.age);            }        }    }}

4.线程通信问题的实现

通过上述改进,线程的安全问题得到了解决。但是一个数据还是会出现多次,这显然不是我们想要的。如果两个线程之间能建立起联系就好了,假设设置线程先抢到CPU的执行权,他首先检查是否有数据,如果有数据设置线程就等待,并告诉获取线程可以获取值了,如果没有数据他就完成赋值操作,并告诉获取线程可以可以获取值了,以此类推。这就是所谓的等待唤醒机制,下面我们就用等待唤醒机制来修改之前的代码。
这里存在这样一个问题,wait()方法和notify()方法为什么会是Object类中的方法呢?通过查API我们发现notify()方法的描述是这样的,“唤醒在此对象监视器上等待的单个线程”,这里的此对象监视器也就是锁对象,所以wait()方法和notify()都是要通过锁对象来调用的,而锁对象是任意对象,所以决定上述两个方法都是Object类的方法。

package cn.itcast_07;public class Student {    String name;    int age;    boolean flag;}
package cn.itcast_07;public class SetThread implements Runnable {    private Student s;    private int i = 0;    public SetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            synchronized (s) {                if (s.flag) {                    try {                        s.wait();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }                if (i % 2 == 0) {                    s.name = "刘亦菲";                    s.age = 27;                } else {                    s.name = "宋承宪";                    s.age = 37;                }                i++;                // 修改标记                s.flag = true;                // 唤醒线程                s.notify();            }        }    }}
package cn.itcast_07;public class GetThread implements Runnable {    private Student s;    public GetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            synchronized (s) {                if(!s.flag){                    try {                        s.wait();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }                System.out.println(s.name + "---" + s.age);                //修改标记                s.flag=false;                //唤醒线程                s.notify();            }        }    }}

5.代码优化

下面的代码优化主要是优化了以下两项:一、将学生类的成员变量私有化;二、将设置和获取封装到了方法中,并在方法中实现同步。

package cn.itcast_08;public class Student {    private String name;    private int age;    private boolean flag;    public synchronized void set(String name, int age) {        if (this.flag) {            try {                this.wait();            } catch (InterruptedException e) {                e.printStackTrace();            }        }        this.name = name;        this.age = age;        // 修改标记        this.flag = true;        // 唤醒线程        this.notify();    }    public synchronized void get() {        if (!this.flag) {            try {                this.wait();            } catch (InterruptedException e) {                e.printStackTrace();            }        }        System.out.println(this.name + "----" + this.age);        // 修改标记        this.flag = false;        // 唤醒线程        this.notify();    }}
package cn.itcast_08;public class SetThread implements Runnable {    private Student s;    private int i = 0;    public SetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            if (i % 2 == 0) {                s.set("刘亦菲", 27);            } else {                s.set("宋承宪", 30);            }            i++;        }    }}
package cn.itcast_08;public class GetThread implements Runnable {    private Student s;    public GetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            s.get();        }    }}

6.线程状态转化

enter description here

四、线程组

线程组可以实现对线程的批量设置,比如将线程添加到线程组、获取线程组名称、设置线程组为后台线程、设置线程组最大优先级等,部分功能实现如下

package cn.itcast_09;public class Group implements Runnable {    @Override    public void run() {        for(int i=1;i<=100;i++){            System.out.println(Thread.currentThread().getName()+":"+i);        }    }}
package cn.itcast_09;public class GroupDemo {    public static void main(String[] args) {        //method1();        method2();    }    private static void method2() {        Group g = new Group();        ThreadGroup tg = new ThreadGroup("线程组A");        Thread t1 = new Thread(tg,g,"刘亦菲");        Thread t2 = new Thread(tg,g,"宋承宪");        t1.start();        t2.start();        System.out.println(t1.getThreadGroup().getName());        System.out.println(t2.getThreadGroup().getName());    }    private static void method1() {        Group g = new Group();        Thread t1 = new Thread(g);        Thread t2 = new Thread(g);        /*         * t1.start(); t2.start();         */        ThreadGroup tg1 = t1.getThreadGroup();        ThreadGroup tg2 = t2.getThreadGroup();        System.out.println(tg1.getName());        System.out.println(tg2.getName());        System.out.println(Thread.currentThread().getThreadGroup().getName());    }}

五、线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池中的每一个线程代码结束后,并不会死亡,而是再次回到了线程池成为空闲状态,等待下一个对象来使用。从JDK5开始Java内置支持线程池,提供了Executors类来产生线程池,有如下几个方法:
- public static ExecutorService newCachedThreaPool()
- public static ExecutorService newFixedThreaPool(int nThreads)
- public static ExecutorService newSingleThreaExecutor()

这些方法的返回值是ExecutorService类的对象,该对象就表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程,它提供了如下方法:
- Future

1、实现Runnable接口代码实现如下

package cn.itcast_10;public class MyRunnable implements Runnable {    @Override    public void run() {        for(int i = 1;i<=100;i++){            System.out.println(Thread.currentThread().getName()+":"+i);        }    }}
package cn.itcast_10;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ExecutorDemo {    public static void main(String[] args) {        ExecutorService pool = Executors.newFixedThreadPool(2);        pool.submit(new MyRunnable());        pool.submit(new MyRunnable());        pool.shutdown();    }}

2、实现Callable接口代码实现如下

package cn.itcast_11;import java.util.concurrent.Callable;public class MyCallable implements Callable<Integer> {    private int number;    public MyCallable(int number){        this.number=number;    }    @Override    public Integer call() throws Exception {        int sum=0;        for (int i = 1; i <= number; i++) {            sum+=i;        }        return sum;    }}
package cn.itcast_11;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;public class CallableDemo {    public static void main(String[] args) throws InterruptedException, ExecutionException {        ExecutorService pool = Executors.newFixedThreadPool(2);        Future<Integer> f1 = pool.submit(new MyCallable(100));        Future<Integer> f2 = pool.submit(new MyCallable(200));        Integer num1=f1.get();        Integer num2=f2.get();        System.out.println(num1);        System.out.println(num2);        pool.shutdown();    }}

六、匿名内部类开线程

在我们实际开发中有时仅仅是想开一个线程,不管用之前的继承Thread类还是实现Runnable接口都显得有些麻烦,这时便可以使用匿名内部类来开启线程。代码实现如下

package cn.itcast_12;public class ThreadDemo {    public static void main(String[] args) {        new Thread() {            public void run() {                for (int i = 1; i <= 100; i++) {                    System.out.println(Thread.currentThread().getName() + ":"                            + i);                }            }        }.start();        new Thread(new Runnable() {            @Override            public void run() {                for (int i = 1; i <= 100; i++) {                    System.out.println("重写接口" + ":"                            + i);                }            }        }) {        }.start();        /*new Thread(new Runnable() {            @Override            public void run() {                for (int i = 1; i <= 100; i++) {                    System.out.println("重写Runnable" + ":" + i);                }            }        }) {            public void run() {                for (int i = 1; i <= 100; i++) {                    System.out.println("内部类的方法" + ":" + i);                }            }        }.start();*/    }}
0 0