8、多线程(2)

四、线程同步

4.1 基本概念

  • 1、由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。java语言提供了专门的机制来解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
  • 2、由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括方法:synchronized方法和synchronized块。

4.2 相关例子

不使用同步时可能会出现冲突

package cn.itcast.day178.thread02;
public class SynDemo01 {
    public static void main(String[] args) {
        Web12306 web = new Web12306();// 真实角色
        // 代理对象
        Thread t1 = new Thread(web, "黄牛1");// 第二个参数是当前线程的名字
        Thread t2 = new Thread(web, "黄牛2");
        Thread t3 = new Thread(web, "黄牛3");
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

class Web12306 implements Runnable {
    private int num = 10;
    private boolean flag = true;

    public void run() {
        while (flag) {
            test1();
        }
    }

    // 线程不安全
    public void test1() {
        if (num <= 0) {
            this.flag = false;
            return;
        }
        try {
            Thread.sleep(500);// 500ms的延时
            // 加入延时之后可能会造成资源冲突的问题,这就是并发问题
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "抢到了第" + num--
                + "张票");
    }
}

说明:这个例子是模拟抢票的情况,如果不加入同步,则可能一张票同时被多个人抢到,显示这是有问题的,下面我们看使用同步方法来解决这个问题:

    // 线程安全,同步方法
    public synchronized void test2() {
        if (num <= 0) {
            this.flag = false;
            return;
        }
        try {
            Thread.sleep(500);// 500ms的延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "抢到了第" + num--
                + "张票");
    }

说明:在方法中加上synchronized关键字就可以将此方法变成一个同步方法,当一个运行一个线程的此方法时,如果此方法没有运行完,则其他线程的此方法是不能执行的。当然我们还可以使用同步块来达到这个目的:

    // 线程安全,同步块
    public void test3() {
        synchronized (this) {// 锁定this,即锁定当前线程
            if (num <= 0) {
                this.flag = false;
                return;
            }
            try {
                Thread.sleep(500);// 500ms的延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第"
                    + num-- + "张票");
        }
    }

说明:这里我们将方法中所要执行的代码全部放在同步块中,这样在同步块中的代码没有执行完的时候资源是被此线程锁定的,同时要注意:这里同步块需要给定锁定的线程对象,这里我们给出的是当前线程。当时有时候我们将要执行的代码全部放在同步块中会造成效率的下降,一般我们将可能出现并发错误的代码放在同步块中,达到最佳的效果,下面我们看一个错误的例子:

    // 线程不安全,同步块,锁定一部分,锁定范围不正确
    public void test4() {
        synchronized (this) {// 锁定this,即锁定当前线程
            if (num <= 0) {
                this.flag = false;
                return;
            }
        }
        try {
            Thread.sleep(500);// 500ms的延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "抢到了第" + num--
                + "张票");
    }

说明:这里可能发生并发错误的位置是票数量减少的代码,这里显然同步块位置是有问题的,所以并不能解决并发问题。放在同步块中的代码不仅要正确,我们锁定的资源对象也要正确,下面看锁定资源对象错误的一个例子:

    // 线程不安全,同步块,锁定资源不正确
    public void test5() {
        synchronized ((Integer) num) {// 对于基本类型需要包装
            if (num <= 0) {
                this.flag = false;
                return;
            }
            try {
                Thread.sleep(500);// 500ms的延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第"
                    + num-- + "张票");
        }
    }

说明:资源对象一般是某个线程对象(基本类型数据需要包装),但是这里却不是,所以也不能解决并发问题。还有一种同步块范围不对的情况:

    // 线程不安全,同步块,锁定资源不正确
    public void test6() {
        if (num <= 0) {
            this.flag = false;
            return;
        }
        //a b c
        synchronized (this) {
            try {
                Thread.sleep(500);// 500ms的延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第"
                    + num-- + "张票");
        }
    }

说明:这里我们可以看到,多个线程可能同时出现在同步块之前进行等待,那哪个线程进入同步块中执行呢?这显然是不确定的,这样就会造成冲突。上面我们讲解了同步块的两种形式:

synchronized(引用类型)
synchronized(this)

其实同步块还有一种形式synchronized(类.class)。先看一种设计模式:单例设计模式

package cn.itcast.day178.thread02;
//单例设计模式:确保一个类只有一个对象
public class SynDemo02 {
    public static void main(String[] args) {
        test2();
    }

    public static void test2() {
        // 此时我们看到单例就没有达到效果,我们在getnInstance方法中加入同步关键字
        JvmThread thread1 = new JvmThread(100);
        JvmThread thread2 = new JvmThread(500);
        thread1.start();
        thread2.start();
    }
    public static void test1() {
        Jvm jvm1 = Jvm.getInstance();
        Jvm jvm2 = Jvm.getInstance();
        // 单线程中下面两个对象是一样的,达到了单例的效果,但是在多线程中就不一定了
        System.out.println(jvm1);
        System.out.println(jvm2);
    }
}

class JvmThread extends Thread {
    private long time;

    public JvmThread() {
    }

    public JvmThread(long time) {
        this.time = time;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->"
                + Jvm.getInstance(time));
    }
}

// 确保一个类只有一个对象:
// 懒汉式
class Jvm {

    // 1、构造器私有化,避免外部直接创建对象
    private Jvm() {}
    // 2、声明一个私有静态变量
    private static Jvm instance = null;
    // 3、创建一个静态的公共方法访问该变量,如果变量没有对象,创建该对象
    public static Jvm getInstance() {
        if (instance == null) {
            instance = new Jvm();
        }
        return instance;
    }
}

说明:单例设计模式就是为了确保在程序运行过程中一个类只有一个实例对象。Jvm类我们使用了基本的单例设计模式,在单线程中可以确保只有一个对象实例,但是在多线程就不一定了(test1方法)。从这个类中我们可以知道单例设计模式的基本步骤。其中加入延时是为了放大出错的概率。从测试结果中可以看到并没有达到单例的效果(run方法打印出来的结果不一致)。当然解决这个问题最简单的方式就是在getInstance方法中加入synchronized关键字,但是这里我们主要看使用同步块如何解决,下面我们改进Jvm类:

class Jvm1 {
    // 1、构造器私有化,避免外部直接创建对象
    private Jvm1() {
    }

    // 2、声明一个私有静态变量
    private static Jvm1 instance = null;

    // 3、创建一个静态的公共方法访问该变量,如果变量没有对象,创建该对象
    public static Jvm1 getInstance1(long time) {
        if (instance == null) {
            try {
                Thread.sleep(time);// 加入延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Jvm1();
        }
        return instance;
    }

    // 加入同步,我们可以直接在方法上加上synchronized关键字,这里我们使用同步块,但是效率不高
    // 在下面我们进行改进
    public static Jvm1 getInstance2(long time) {
        synchronized (Jvm.class) {// 这里我们不能使用this了,因为this还没有创建出来,于是使用字节码
            if (instance == null) {
                try {
                    Thread.sleep(time);// 加入延时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Jvm1();
            }
            return instance;
        }
    }

    // 改进,这里比如有a,b,c三个线程,一开始对象为空,进入第一个if,然后a进入同步块,其他线程等待
    // 当a进去之后则对象就被创建了,于是当其他线程进入同步块的时候就不需要像上面那样等待了,直接返回已有
    // 对象
    public static Jvm1 getInstance3(long time) {
        if (instance == null) {
            synchronized (Jvm.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Jvm1();
                }
            }
        }
        return instance;
    }
}

说明:首先我们是对方法getInstance改进成了getInstance2,进入了同步关键字,但是参数不能再是this了,因为此时对象还没有创建出来。此时我们进行测试可以发现达到了同步的效果。但是这种实现的方式可能效率和之前的同步方法的效率一样,不太高,因为此时不管对象存在不存在都需要在同步块前面等待,我们改进为getInstance3方法,这样如果对象存在则不需要进入同步块中,直接拿到对象即可使用。而单例创建的方式有上面提到的懒汉式,还有其他方式:

package cn.itcast.day178.thread02;

/*单例创建的几种方式:
 * 1、懒汉式
 *  a、构造器私有化
 *  b、声明私有的静态属性
 *  c、对外提供访问属性的静态方法,确保该对象存在
 * */
public class MyJvm03 {
    private static MyJvm03 instance;
    private MyJvm03(){
        
    }
    public static MyJvm03 getInstance(){
        if(instance == null){//为了效率
            synchronized (MyJvm03.class) {
                if(instance == null){//为了安全
                    instance = new MyJvm03();
                }
            }
        }
        
        return instance;
    }
}

/*2、恶汉式
 *  a、构造器私有化
 *  b、声明私有的静态属性,同时创建该对象
 *  c、对外提供访问属性的静态方法,确保该对象存在
 * */
class MyJvm04{
    private static MyJvm04 instance = new MyJvm04();
    private MyJvm04(){
        
    }
    public static MyJvm04 getInstance(){
        return instance;
    }
    
}
//恶汉式提高效率的改进:类在使用的时候才让其加载,这样只要不调用
//getInstance方法,那么就不会加载类,这样延缓了类加载时机
class MyJvm05{
    private static class JVMholder{
        private static MyJvm05 instance = new MyJvm05();
    }
    
    private MyJvm05(){
        
    }
    public static MyJvm05 getInstance(){
        return JVMholder.instance;
    }
}

说明:相对来说,恶汉式的效率较高一点。

五、死锁

过多的同步容易造成死锁,就是一份资源同时被多个线程同时调用。

package cn.itcast.day178.thread02;
//两个线程使用的是同一份资源,可能就会造成死锁,但是这并不绝对
//过多的同步容易造成死锁
public class SynDemo03 {
    public static void main(String[] args) {
        Object goods = new Object();
        Object money = new Object();
        Test t1 = new Test(goods, money);
        Test1 t2 = new Test1(goods, money);
        Thread proxy1 = new Thread(t1);
        Thread proxy2 = new Thread(t2);
        proxy1.start();
        proxy2.start();
    }
}

class Test implements Runnable{
    Object goods;
    Object money;
    
    public Test(Object goods, Object money) {
        this.goods = goods;
        this.money = money;
    }

    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized (goods) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (money) {
            }
        }
        System.out.println("一手给钱");
    }
}


class Test1 implements Runnable{
    Object goods;
    Object money;
    
    public Test1(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }

    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized (money) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (goods) {
            }
        }
        System.out.println("一手交货");
    }
}

说明:此时我们发现不会的打印出任何内容,因为造成了死锁。解决死锁的思路就是使用生产者消费者设计模式。

生产者消费者模式

  • 1)生产者消费者模式也称有限资源缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小的缓冲区的线程-即所谓的生产者合格消费者-在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据。

  • 2)要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区中添加数据。同样,也可以让消费者在缓冲区空的时候进入休眠,等到生产者往缓冲区中添加数据之后,再唤醒消费者。通常常用的方法有信号灯法,管程等。如果解决方法不不够完善,则容易出现死锁的情况,出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。

这里我们介绍信号灯法,首先给出资源:

package cn.itcast.day178.thread02;
/*一个场景,一份共同的资源
 * 生产者消费者模式,采用信号灯法
 * wait会释放锁,而sleep则不释放锁
 * notify和notifyAll表示唤醒
 * 注意:上面说的方法必须和同步在一起使用,不然就使用不了
 * */
public class Movie {
    private String pic;
    //信号灯,当为true时表示生产者生产,消费者等待,生产完成之后通知消费者消费
    //当为false的时候,生产者等待,消费者消费,当消费完成之后通知生产者生产
    private boolean flag = true;
    public synchronized void play(String pic){
        if(!flag){//生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //开始生产
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产了: " + pic);
        //生产完毕
        this.pic = pic;
        //通知消费
        this.notify();
        //生产者停止
        this.flag = false;
    }
    
    public synchronized void watch(){
        if(flag){
            //消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //开始消费
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("消费了: " + pic);
        //消费完毕,通知生产
        this.notify();
        
        //消费停止
        this.flag = true;
    }
}

说明:资源是一个电影,那么生产者就是演员:

package cn.itcast.day178.thread02;
/*表演者,这里就相当于生产者 */
public class Player implements Runnable{
    private Movie movie;
    
    public Player(Movie movie) {
        super();
        this.movie = movie;
    }

    public void run() {
        for(int i = 0; i < 20; i++){
            if(i % 2 == 0){
                movie.play("左青龙");
            }else{
                movie.play("右白虎");
            }
        }
    }
}

说明:再给出消费者:

package cn.itcast.day178.thread02;
public class Watcher implements Runnable{
    private Movie movie;

    public Watcher(Movie movie) {
        super();
        this.movie = movie;
    }

    public void run() {
        for(int i = 0; i < 20; i++){
            movie.watch();
        }
    }
}

说明:下面我们使用:

package cn.itcast.day178.thread02;
public class App {
    public static void main(String[] args) {
        //共同的资源
        Movie m = new Movie();
        
        //多线程
        Player p = new Player(m);
        Watcher w = new Watcher(m);
        
        new Thread(p).start(); 
        new Thread(w).start(); 
    }
}

说明:我们在使用的时候同时开启了生产者和消费者线程,在运行过程中如果资源没有生产出来则消费者线程等待,资源生产出来之后消费者线程执行。

六、任务调度

  • 1)Timer定时器类
  • 2)TimerTask任务类
  • 3)通过timertimertask:(spring的任务调度就是通过它们来实现的)
  • 4)在这种实现方式中,Timer类实现的是类似闹钟的功能,也就是定时或者每个一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其他线程的。而TimerTask类是一个抽象类,该类实现了Runnable接口,所以按照前面的介绍,该类具备多线程的能力。
  • 5)在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
  • 6)在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间如果需要完全独立运行的话,最好还是一个Timer启动一个TimerTask实现。

下面看一个例子:

package cn.itcast.day178.thread02;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
 * 使用方法schedule指定任务
 * */
public class TimerDemo01 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //这里第一个参数表示指定一个任务,第二个参数表示什么时候开始执行,
        //第三个参数表示每隔多少秒执行一次,如果没有第三个参数则只运行一次
        timer.schedule(new TimerTask() {
            //线程体
            public void run() {
                System.out.println("线程体....");
            }
        }, new Date(System.currentTimeMillis() + 1000), 200);
    }
}

最后我们看一下notifynotifyAll的区别:
这里notify()notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。

  • void notify():唤醒一个正在等待该对象的线程。
  • void notifyAll():唤醒所有正在等待该对象的线程。

两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notifynotifyAll,它们等待的是被notifynotifyAll,而不是锁。

你可能感兴趣的:(8、多线程(2))