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

第四讲 多线程的应用(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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值