线程同步、死锁与生产者消费者

一、问题引出

多个线程访问同一个资源时,如果操作不当就很容易产生意想不到的错误,比如常见的抢票程序:

public class Demo1 {
    public static void main(String[] args) {
        Ticket tt = new Ticket();
        new Thread(tt, "甲").start();
        new Thread(tt, "乙").start();
    }
}

class Ticket implements Runnable {
    private int ticketCount = 10;
    @Override
    public void run() {
        while (ticketCount > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第【" + (10 - ticketCount + 1) + "】张火车票");
            ticketCount--;
        }
    }
}

结果:

乙抢到了第【1】张火车票
甲抢到了第【2】张火车票
乙抢到了第【3】张火车票
甲抢到了第【4】张火车票
乙抢到了第【5】张火车票
甲抢到了第【6】张火车票
乙抢到了第【7】张火车票
甲抢到了第【8】张火车票
乙抢到了第【9】张火车票
甲抢到了第【9】张火车票
乙抢到了第【11】张火车票

以上代码可以看出,第9张票被两个人抢,且出现了第11张票,这不符合实际。为什么会出现这种现象?原因是当乙抢到第9张票但还未执行ticketCount–;语句时,甲也进入了run()方法,此时票数仍然是第9张票,也打印出了第9票,因此第9张票被抢了两遍。之后甲和乙都会执行ticketCount–;语句,当ticketCount =0时,无论哪个线程抢到资源都会打印抢到了第【11】张火车票,当ticketCount = -1时,停止。

综上分析,问题出现的主要原因就是甲、乙两个线程同时访问同一资源,造成了资源污染。

二、线程同步

上面可知当多个线程同时访问同一资源,就可能会造成了资源污染,解决这个问题的方法就是在某个线程访问资源时,其他线程在资源或者方法外面等待,也就是线程同步,或者叫加锁。
线程同步就是指多个操作在同一时间段内只能有一个线程进行,而其他线程要等待此线程完成之后才可以继续进行。
要实现线程同步,需要通过关键字synchronized关键字,利用这个关键字可以定义同步方法或者代码块。格式如下:

synchronized(同步对象){
	操作;
}

一般要进行同步对象处理时,采用当前对象this进行同步。上面的抢票代码加上同步后如下:

public class Demo1 {
    public static void main(String[] args) {
        Ticket tt = new Ticket();
        new Thread(tt, "甲").start();
        new Thread(tt, "乙").start();
    }
}

class Ticket implements Runnable {
    private int ticketCount = 10;
    @Override
    public void run() {
        synchronized(this){
            while (ticketCount > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第【" + (10 - ticketCount + 1) + "】张火车票");
                ticketCount--;
            }
        }
    }
}

结果:

甲抢到了第【1】张火车票
甲抢到了第【2】张火车票
甲抢到了第【3】张火车票
甲抢到了第【4】张火车票
甲抢到了第【5】张火车票
甲抢到了第【6】张火车票
甲抢到了第【7】张火车票
甲抢到了第【8】张火车票
甲抢到了第【9】张火车票
甲抢到了第【10】张火车票

加锁或者同步处理以后,虽然多线程同时访问同一资源的问题虽然解决了,但是线程同步会降低整体性能。

三、死锁

死锁就是多个线程间相互等待的状态。

class MyThread implements Runnable{
    int flag = 1;
    // 必须是静态资源
    static Object o1 = new Object();
    static Object o2 = new Object();

    @Override
    public void run() {
        System.out.println("flag= " + flag);
        if(flag == 1){
            synchronized (o1){
                System.out.println(Thread.currentThread().getName() + "我抢到了o1,还需要o2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("111");
                }
            }
        }
        if(flag == 0){
            synchronized (o2){
                System.out.println(Thread.currentThread().getName() + "我抢到了o2,还需要o1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("222");
                }
            }
        }
    }
}

public class DeadLock {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        // 设置线程1先抢占o1
        myThread1.flag = 1;
        // 设置线程1先抢占o2
        myThread2.flag = 0;
        Thread t1 = new Thread(myThread1);
        Thread t2 = new Thread(myThread2);
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
有两个线程,都需要锁住同样的2个对象(a、b)才能完成操作,其中线程1已经锁住了a对象,线程2锁住了b对象,两个线程都不释放锁就会造成死锁。程序无法停止。

四、生产者和消费者模式

生产者和消费者模式是用来解决死锁问题的。
为什么可以解决呢?
生产者和消费者模式中,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能,需要根据信号来生产或获取:当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来。这里实际体现了信号灯法:flag = true,生产者生产,消费者等待;反之,生产者等待,消费者生产。

下面以蒸包子和吃包子为例,厨师作为生产者输出包子,吃包子的作为消费者,如果没有采用生产者消费者模式,那就可能出现吃包子的没包子吃或者蒸包子的产能过剩的情况;而采用生产者消费者模式后,厨师蒸好包子后不是接着蒸,而是把蒸好的包子放在篮子里然后停下来告诉消费者可以吃了,然后消费者接到信号开始从篮子里拿包子吃,直到吃完后会给厨师一个信号,告诉他可以接着蒸包子了,这时消费者停下,厨师开始蒸包子,接着循环下去。

/**
 * 装包子的篮子类
 */
class Container{
    private String baozi;
    // 篮子状态信号标志:flag = true,篮子空了,生产者生产;flag = false,篮子满了,消费者消费
    private boolean flag = true;

    // 模拟生产过程,生产过程要加锁,防止生产过程中消费者过来消费
    public synchronized void play(String baozi) throws InterruptedException {
        // 生产者等待
        if(!flag){
            this.wait();
        }
        // 生产者生产
        Thread.sleep(500); // 模拟生产耗时
        // 包子蒸好了
        this.baozi = baozi;
        System.out.println("生产" + baozi);
        // 通知消费者来吃
        this.notify();
        // 停止生产
        this.flag = false;
    }

    // 模拟消费过程,消费过程要加锁,防止消费过程中生产者过来生产
    public synchronized void eat() throws InterruptedException {
        // 消费者等待
        if(flag){
            this.wait();
        }
        // 消费者消费
        Thread.sleep(100); // 模拟消费耗时
        System.out.println("篮子里的包子已经吃完了");
        // 通知生产者需要蒸包子了
        this.notify();
        // 停止吃包子
        this.flag = true;
    }
}

/**
 * 定义生产者
 */
class Player implements Runnable{
    private Container container;
    public Player(Container container) {
        this.container = container;
    }

    // 单日生产素馅的包子,双日生产肉馅的包子
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            if(i%2 == 0){
                try {
                    container.play("肉馅的包子");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                try {
                    container.play("素馅的包子");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * 定义消费者
 */
class Consumer implements Runnable{
    private Container container;
    public Consumer(Container container) {
        this.container = container;
    }

    // 不管啥馅的包子我都吃
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            try {
                container.eat();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class CpDemo {
    public static void main(String[] args) {
        // 同一个篮子
        Container container = new Container();

        Player player = new Player(container);
        Consumer consumer = new Consumer(container);
        new Thread(player, "生产者").start();
        new Thread(consumer, "消费者").start();
    }
}
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了

以上,生产者线程和消费者线程交替进行,就能很好的避免死锁问题。

五、volatile和synchronized的区别

volatile关键字主要是在属性定义上使用,表示此属性为直接数据操作,而不进行副本的拷贝处理。

volatile和synchronized的区别:

  • volatile主要是在属性定义上使用,而synchronized是在代码块或方法上使用
  • volatile无法描述同步处理,是一种直接内存处理,避免了副本操作;synchronized是同步操作。

六、sleep和wait的区别

① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。

sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

② 锁: sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中,使得其他线程可以使用同步控制块或者方法。

sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。

③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

七、notify和notifyAll的区别?

先要理解锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

notify和notifyAll的区别

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
    当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或
  • notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章