多线程基础二:线程同步,生产者和消费者剖析。看不懂拿你两米大刀砍我!!!

一、线程同步问题

在12306购票系统中,一年一度的抢票春运相信小伙伴们都有深刻的印象。坑爹的验证码,万恶的黄牛党,都是历历在目的。那么在这个抢票过程中,每个用户可以看做是一个线程,那么就涉及到用户的买票和退票操作。如果有20张票,同时有20个人同时进行购票操作,我们来模拟一下,线程不同步会是什么样子的。

/**
假设有两个群体,学生和工人,此时剩余20张票,工人和学生各买10张。
*/
public class Deom01 {
    public static void main(String[] args) {
        Sale sale = new Sale();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sale.getTicket();
                }
            }
        },"学生").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sale.getTicket();
                }
            }
        },"工人").start();
    }
}

class Sale {
    /**假设还有20张票*/
    private int ticketNum = 20;

    public void getTicket() {
        System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
    }
}
/**
运行结果截图如下,出现了工人和学生都买到了第六张票,这个显然是不合理的。
否则两个人到时候要跟同一个位置发生争执了。
*/

学生拿到了第20张票
工人拿到了第19张票
学生拿到了第18张票
工人拿到了第17张票
学生拿到了第16张票
工人拿到了第14张票
学生拿到了第13张票
工人拿到了第11张票
工人拿到了第9张票
学生拿到了第8张票
工人拿到了第6张票
工人拿到了第4张票
工人拿到了第15张票
学生拿到了第3张票
学生拿到了第5张票
工人拿到了第7张票
学生拿到了第10张票
学生拿到了第12张票
工人拿到了第2张票
学生拿到了第1张票

那么如何解决这个问题呢?
方法一:
对于变量使用volatile关键字。被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。是目前最轻量级的同步机制。主要底层原理是缓存一致性协议。具体原理暂且按下不表。
实例:

package thread;

public class Deom01 {
    public static void main(String[] args) {

        Sale sale = new Sale();
        for (int i = 0; i <10 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sale.getTicket();
                }
            },"学生").start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    sale.getTicket();
                }
            },"工人").start();

        }
    }
}

class Sale {
    /**假设还有20张票*/
    private volatile static int ticketNum = 20;

    public void getTicket() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
    }
}

//输出结果:
学生拿到了第20张票
工人拿到了第16张票
工人拿到了第13张票
学生拿到了第17张票
工人拿到了第18张票
工人拿到了第10张票
学生拿到了第11张票
工人拿到了第19张票
工人拿到了第1张票
学生拿到了第2张票
工人拿到了第3张票
学生拿到了第4张票
工人拿到了第5张票
学生拿到了第6张票
学生拿到了第7张票
工人拿到了第8张票
工人拿到了第9张票
学生拿到了第12张票
学生拿到了第14张票
学生拿到了第15张票

从上述运行结果可知,能够保证线程的同步,但是在顺序上可能会差强人意。


方法二:
加锁:①使用synchronized关键字。可以加在方法块上直接使用,也可以加在类上或者方法体中都可以。
实例:

package thread;

public class Deom01 {
    public static void main(String[] args) {

        Sale sale = new Sale();
        for (int i = 0; i <10 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sale.getTicket();
                }
            },"学生").start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    sale.getTicket();
                }
            },"工人").start();

        }
    }
}

class Sale {
    /**假设还有20张票*/
    private int ticketNum = 20;

    public synchronized void getTicket() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
    }
}
学生拿到了第20张票
工人拿到了第19张票
学生拿到了第18张票
工人拿到了第17张票
学生拿到了第16张票
工人拿到了第15张票
工人拿到了第14张票
学生拿到了第13张票
工人拿到了第12张票
学生拿到了第11张票
工人拿到了第10张票
学生拿到了第9张票
学生拿到了第8张票
工人拿到了第7张票
学生拿到了第6张票
工人拿到了第5张票
学生拿到了第4张票
工人拿到了第3张票
工人拿到了第2张票
学生拿到了第1张票

运行结果,依据次序,先后取票。
②使用重量级锁:使用Lock接口,进行加锁和释放锁操作

package thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Deom01 {
    public static void main(String[] args) {

        Sale sale = new Sale();
        for (int i = 0; i <10 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sale.getTicket();
                }
            },"学生").start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    sale.getTicket();
                }
            },"工人").start();

        }
    }
}

class Sale {
    /**假设还有20张票*/
    private int ticketNum = 20;
    /**创建一个锁对象*/
    Lock lock = new ReentrantLock();

    public void getTicket() {
        //加锁
        lock.lock();
        try {
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //一定要记住要释放锁
            lock.unlock();
        }
    }
}

运行结果就不放了,和之前的加锁情况一样。

简单比较一下两个加锁的区别:
1、synchronized属于java关键字,Lock是一个类
2、synchronized无法获取锁的状态,而lock可以判断是否获取到了锁
3、synchronized会自动释放锁,lock需要手动释放,否则有可能会产生死锁
4、synchronized属于可重入锁,不可以中断,非公平,lock也是可重入锁,可以判断锁,非公平,也可以设置为公平(true)
5、synchronized适合小量代码块,lock适合大量代码块。

二、生产者和消费者模式

情景一: 在市场里,有一家工厂,有一个消费者,生产一个产品就消费一个。代码如下:

package thread;

public class Market {

    public static void main(String[] args) {
        Method method = new Method();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.factory();
            }
        }, "factory").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.consumer();
            }
        }, "consumer").start();
    }
}


class Method{
    //统计产品数量
    private int product = 0;
    /**
     * 生产者
     * @param
     * @return void
     * @author daodao
     * @date 2020/6/13 16:54
     */
    public synchronized void factory() {
        //如果工厂有产品,则等待,此时是用if判断
        if(product!=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则进行生产
        product++;
        System.out.println(Thread.currentThread().getName()+"生产有"+product+"产品");
        //唤醒其它所有线程
        this.notifyAll();
    }


    /**
     * 消费者模式
     * @param
     * @return void
     * @author daodao
     * @date 2020/6/13 16:57
     */
    public synchronized void consumer() {
        //判断市场是否有产品,没有产品,则等待,此处先用if判断
        if(product==0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //存在产品,则进行消费
        product--;
        System.out.println(Thread.currentThread().getName()+"消费了一件产品,还剩余"+product+"件产品");
        //唤醒其它线程
        this.notifyAll();
    }
}

其中,判断线程等待我们使用if条件并无问题,由于线程是一个生产者一个消费者,那么,当市场规模扩大,工厂扩建为两个,消费者也扩建成两个,此时使用if会有什么问题呢?

package thread;

public class Market {

    public static void main(String[] args) {
        Method method = new Method();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.factory();
            }
        }, "factory1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.factory();
            }
        }, "factory2").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.consumer();
            }
        }, "consumer1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.consumer();
            }
        }, "consumer2").start();
    }
}


class Method{
    //统计产品数量
    private int product = 0;
    /**
     * 生产者
     * @param
     * @return void
     * @author daodao
     * @date 2020/6/13 16:54
     */
    public synchronized void factory() {
        //如果工厂有产品,则等待,此时是用if判断
        if(product!=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则进行生产
        product++;
        System.out.println(Thread.currentThread().getName()+"生产有"+product+"产品");
        //唤醒其它所有线程
        this.notifyAll();
    }


    /**
     * 消费者模式
     * @param
     * @return void
     * @author daodao
     * @date 2020/6/13 16:57
     */
    public synchronized void consumer() {
        //判断市场是否有产品,没有产品,则等待,此处先用if判断
        if(product==0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //存在产品,则进行消费
        product--;
        System.out.println(Thread.currentThread().getName()+"消费了一件产品,还剩余"+product+"件产品");
        //唤醒其它线程
        this.notifyAll();
    }
}

factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
consumer2消费了一件产品,还剩余-1件产品
consumer2消费了一件产品,还剩余-2件产品
consumer2消费了一件产品,还剩余-3件产品
consumer2消费了一件产品,还剩余-4件产品
consumer2消费了一件产品,还剩余-5件产品
consumer2消费了一件产品,还剩余-6件产品
consumer2消费了一件产品,还剩余-7件产品
consumer2消费了一件产品,还剩余-8件产品
consumer2消费了一件产品,还剩余-9件产品
consumer1消费了一件产品,还剩余-10件产品
consumer1消费了一件产品,还剩余-11件产品
consumer1消费了一件产品,还剩余-12件产品
consumer1消费了一件产品,还剩余-13件产品
consumer1消费了一件产品,还剩余-14件产品
consumer1消费了一件产品,还剩余-15件产品
consumer1消费了一件产品,还剩余-16件产品
consumer1消费了一件产品,还剩余-17件产品
consumer1消费了一件产品,还剩余-18件产品
factory2生产有-17产品
factory1生产有-16产品
factory2生产有-15产品
factory1生产有-14产品
factory2生产有-13产品
factory1生产有-12产品
factory2生产有-11产品
factory1生产有-10产品
factory2生产有-9产品
factory1生产有-8产品
factory2生产有-7产品
factory1生产有-6产品
factory2生产有-5产品
factory1生产有-4产品
factory2生产有-3产品
factory1生产有-2产品
factory2生产有-1产品
factory1生产有0产品

首先是线程会出现换乱,整个市场乱套,完全不是我们想要的结果。那么该如何做呢?其实很简单,只要将if修改成while即可。

package thread;

public class Market {

    public static void main(String[] args) {
        Method method = new Method();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.factory();
            }
        }, "factory1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.factory();
            }
        }, "factory2").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.consumer();
            }
        }, "consumer1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                method.consumer();
            }
        }, "consumer2").start();
    }
}


class Method{
    //统计产品数量
    private int product = 0;
    /**
     * 生产者
     * @param
     * @return void
     * @author daodao
     * @date 2020/6/13 16:54
     */
    public synchronized void factory() {
        //此时修改成while,避免
        while(product!=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则进行生产
        product++;
        System.out.println(Thread.currentThread().getName()+"生产有"+product+"产品");
        //唤醒其它所有线程
        this.notifyAll();
    }


    /**
     * 消费者模式
     * @param
     * @return void
     * @author daodao
     * @date 2020/6/13 16:57
     */
    public synchronized void consumer() {
        //判断市场是否有产品,没有产品,则等待,此处先用if判断
        while(product==0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //存在产品,则进行消费
        product--;
        System.out.println(Thread.currentThread().getName()+"消费了一件产品,还剩余"+product+"件产品");
        //唤醒其它线程
        this.notifyAll();
    }
}
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品
factory1生产有1产品
consumer2消费了一件产品,还剩余0件产品
factory2生产有1产品
consumer1消费了一件产品,还剩余0件产品

可以看出,如果是多个线程情况下,启用while不会出问题,这是为什么呢?
在这里插入图片描述
过程详解:
step1:有两个消费者A和B,此时工厂还没有产品,因此都调用wait,进行等待。同时notifAll工厂进行生产
step2:工厂由wait变为运行状态,进行生产,生产完毕,此时工厂内产品为1,调用wait方法,同时通知notifyAll其他消费者快来进行消费。
step3:假设消费者A是渴望力量者,充过钱,是我们高贵的心悦会员,第一时间收到消息,先去工厂,从wait出发,调用判断,此时会跳出判断,与while和if无关。相当于拿到了工厂仓库的钥匙,拿走产品进行消费。同时仓库产品为0了。
在这里插入图片描述
step4(关键点):同时普通市民刘先生也赶到工厂,如果我们用的是if进行判断,由于step1已经调用了一次,不会再次进行数量判断,从wait处出发,直接跳出判断,此时刘先生也要消费,就会出现之前的产品数量为负数了。如果是while循环,刘先生从wait处出发,需要再次执行while,发现仓库大门钥匙获取不到了,就只能重新wait了。总而言之,等待总是应该存在循环当中。,才不会被虚假唤醒。
看完分析,应该就更加容易理解生产者和消费者模式了吧。如果您觉得有用,欢迎转载关注收藏和点赞。给与新人一点小小的鼓励和帮助。

扫描二维码关注公众号,回复: 11339877 查看本文章

我是刀刀,一个自学java的小白,如果您这边有合适的工作或者技术交流,可以私信或者扫描二维码与我联系,谢谢您!
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_39085109/article/details/106734991
今日推荐