JUC 高并发编程(2):同步案例:synchronized,生产者与消费者,多线程的虚假唤醒

synchronize

案例

以售票问题为例,假设这里存在三个线程用来买票,则代码如下:

public class SellTicket implements Runnable {
    //定义一个成员变量表示有100张票
    private int tickets=100;
    public void run(){
     while (true){
         if(tickets>0){
             try {
                 //通过sleep()方法来等待
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets--+"张票");
         }else{
             //System.out.println("");
         }
     }
    }
}
@SuppressWarnings("all")
public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();

        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

如果运行上面代码,会出现下面两种异常情况:

  • 相同票数出现多次;
  • 出现了负票

原因分析

  • 出现相同票数的原因
    在这里插入图片描述
  • 出现负数的原因
    在这里插入图片描述
  • 总结:出票操作、更新票数操作以及输出操作没有整合在一起,随时可能被其他线程打断。

同步代码块

判断多线程程序是否具有安全问题的标准:

  • 是否有多线程坏境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

解决多线程问题的方法:
利用 synchronized 关键字把多条语句操作的共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

如下所示:

public class SellTicket implements Runnable {
    //定义一个成员变量表示有100张票
    private int tickets=100;
    
    private Object obj=new Object();

    public void run(){
     while (true){
       //这里放的锁要是同一把锁才可以
       synchronized(obj){
           if(tickets>0){
               try {
                   //通过sleep()方法来等待
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets--+"张票");
           }else{
               //System.out.println("");
           }
       }
     }
    }
}

同步的好处和弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都会判断同步上的锁,这是很浪费资源的,无形中会降低程序的运行效率

同步方法

同步方法锁

  • 注意同步代码块需要输入一个唯一标志——锁。这个锁必须是所有线程只有一把,有人进了,其他人就必须等待。 因此,为了免除重新创建对象作为锁的损耗,可以使用当前方法作为唯一标志,方法的默认锁对象为 this

        private int tickets = 100;
        private Object obj = new Object();
        private int x = 0;
    
        @Override
        public void run() {
            while (true) {
                if (x % 2 == 0) {
    //                synchronized (obj) {
            synchronized (this) {    
                        if (tickets > 0) {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                            tickets--;
                        }
                    }
                } else {
                    sellTicket();
                }
                x++;
            }
        }
    
      private synchronized void sellTicket() {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    
    }
    

同步静态锁

  • 当定义的同步方法是静态方法时,此时的锁对象为 .class
    如下所示:

    public class SynchronizedStatic {
    
        public static synchronized void  m1(){
            try {
                System.out.println(Thread.currentThread().getName());
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static synchronized void  m2(){
            try {
                System.out.println(Thread.currentThread().getName());
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

直接码角度分析 synchronize

我们可以通过反编译:javap -v -p *.class > 类.txt 将反编译代码进行输出到txt中

synchronized有三种应用方式

  • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

①、synchronized同步代码块

  • 实现使用的是monitorentermonitorexit指令。一般情况是下,一个monitorenter 两个 monitorexit 。因为需要释放一个异常锁,一个正常锁。
    在这里插入图片描述
  • 一定是一个enter和两个exit吗?
    (不一定,如果方法中直接抛出了异常处理,那么就是一个monitorenter和一个monitorexit)
    在这里插入图片描述

②、synchronized普通同步方法

  • 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置
  • 如果设置了,执行线程会将先持有monitor,然后再执行方法,最后再方法完成时释放minotor(无论是正常完成还是非正常完成)。即即执行线程不会在执行代码时显示持有monitor,如下图所示在这里插入图片描述

③、synchronized静态同步方法

  • ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法
    在这里插入图片描述

高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁

生产者和消费者模式概述

所谓生产消费者问题,实际上主要是包含了两类线程:

  • 一类是生产者线程用于生产数据
  • 一类是消费者线程用于消费数据

为了耦合生产者和消费者的关系,通常会采用共享的数据区域,就像一个仓库:

  • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
  • 消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为
    在这里插入图片描述

多线程加锁时徐亚注意的事项

线程四句口诀 掌握

  • 在高内聚低耦合的前提下,线程 - >操作 - >资源类

    假如有一个空调,三个人去操作这个空调,高内聚低耦合是指空调有制热制冷的效果,它会把这两个抽取成一个方法,对外以接口的形式去暴露,提供给操作空调的人或线程使用

  • 判断|操作|唤醒 [ 生产消费中 ]

  • 多线程交互中,必须要防止多线程的虚假唤醒,也即(判断使用while,不能使用if)
    (后面会提到)

  • 标志位

使用Sychronized实现(隐式锁)

为了体现生产和消费过程总的等待和唤醒,Object类提供了等待和唤醒方法(隐式锁)。

  • viod wait( ): 导致当前线程等待,直到另一个线程调用该对象的notify()方法和notifyAll()方法
  • void notify( ): 唤醒正在等待对象监视器的单个线程
  • void notifyAll( ): 唤醒正在等待对象监视器的所有线程
    (注意:wait、notify、notifyAll方法必须要在同步块或同步方法里且成对出现使用)

例子: 现在4个线程,可以操作初始值为0的一个变量,实现2个线程对该变量加1,
2个线程对该变量减1,交替执行,来10轮,变量的初始值为0。

/*

2.思想:
    1.在高内聚低耦合的前提下,线程->操作->资源类
    2.判断操作唤醒[生产消费中]
    3.多线程交互中,必须要放置多线程的虚假唤醒,也即(判断使用while,不能使用if)
* */

public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        AirCondition airCondition=new AirCondition();
        new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.increment();},"线程A").start();
        new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.decrement();},"线程B").start();
        new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.increment();},"线程C").start();
        new Thread(()->{ for (int i = 1; i <11 ; i++) airCondition.decrement();},"线程D").start();
    }
}
class AirCondition{
    private int number=0;

    public synchronized void increment(){
        //1。判断当前状态
     /*   if(number!=0){*/
        while(number!=0){
            try {
                //为什么不用if?解释如下
                //第一次A进来了,在number++后(number=1) C抢到执行权,进入wait状态
                //这个时候,A抢到cpu执行权,也进入wait状态,此时,B线程进行了一次消费
                //唤醒了线程,这个时候A抢到CPU执行权,不需要做判断,number++(1),唤醒线程
                //C也抢到CPU执行权,不需要做判断,number++(2)
                
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //2.干活
        number++;
        System.out.println(Thread.currentThread().getName()+":"+number);
        //3.唤醒
        this.notifyAll();
    }
    public  synchronized void decrement(){
        /*if (number==0){*/
        while (number==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName()+":"+number);
        this.notifyAll();
    }
}

使用ReentrantLock实现 (显示锁)

Lock 接口

Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁, 当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

  • 通常使用 Lock 来进行同步的话,是以下面这种形式去使用的:

    Lock lock = ...;
    lock.lock();
    try{
    	//处理任务
    }catch(Exception ex){
    }finally
    {
    	lock.unlock(); //释放锁
    }
    

该接口存在下面这些接口函数:

public interface Lock
{void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

newCondition

  • 关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
  • notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
    • await() 会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重
      新获得锁并继续执行。
    • signal() 用于唤醒一个等待的线程。

注意: 在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

ReentrantLock
  • ReentrantLock,意思是“可重入锁”,关于可重入锁的概念将在后面讲述。

  • ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更
    多的方法。下面通过一些实例看具体看一下如何使用。

    • ①. ReentrantLock​( ) :创建一个ReentrantLock的实例
    • ②. void lock( ) :获得锁
    • ③. void unlock( ) :释放锁

例子:生产者与消费者

/*
* 使用Lock代替Synchronized来实现新版的生产者和消费者模式 !
* */
@SuppressWarnings("all")
public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        AirCondition airCondition=new AirCondition();

        new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.decrement();},"线程A").start();
        new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.increment();},"线程B").start();
        new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.decrement();},"线程C").start();
        new Thread(()->{ for (int i = 0; i <10 ; i++) airCondition.increment();},"线程D").start();
    }
}
class AirCondition{
    private int number=0;
    //定义Lock锁对象
    final Lock lock=new ReentrantLock();
    final Condition condition  = lock.newCondition();

    //生产者,如果number=0就 number++
    public  void increment(){
       lock.lock();
       try {
           //1.判断
           while(number!=0){
               try {
                   condition.await();//this.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           //2.干活
           number++;
           System.out.println(Thread.currentThread().getName()+":\t"+number);
           //3.唤醒
           condition.signalAll();//this.notifyAll();
       }catch (Exception e){
           e.printStackTrace();
       }finally {
           lock.unlock();
       }
    }
    //消费者,如果number=1,就 number--
    public   void decrement(){
        lock.lock();
        try {
            //1.判断
            while(number==0){
                try {
                    condition.await();//this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //2.干活
            number--;
            System.out.println(Thread.currentThread().getName()+":\t"+number);
            //3.唤醒
            condition.signalAll();//this.notifyAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

ReadWriteLock 接口

ReadWriteLock 也是一个接口,在它里面只定义了两个方法:

//返回读锁
Lock readLock();
//返回写锁
Lock writeLock();

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。

ReentrantReadWriteLock

ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个 方法``readLock() writeLock()`用来获取读锁和写锁。

  • 线程进入读锁的前提条件:

    • 没有其他线程的写锁,
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
    • 也就是说可以多个线程进入读锁
  • 线程进入写锁的前提条件:

    • 没有其他线程的读锁
    • 没有其他线程的写锁
  • 读写锁有以下三个重要的特性:

    (1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

    (2)重进入:读锁和写锁都支持线程重进入。

    (3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

线程间的定制化通信:顺序执行线程

案例介绍

  • 多个线程之间按顺序调用,实现A->B->C
  • 三个线程启动,要求如下:

    AA打印5次,BB打印10次,CC打印15次
    接着
    AA打印5次,BB打印10次,CC打印15次
    …来10轮

由于需要考虑到线程顺序,因此,这里在获得锁和释放锁时,我们应当手动指定唤醒的对象

/*
    多个线程之间按顺序调用,实现A->B->C
三个线程启动,要求如下:
    AA打印5次,BB打印10次,CC打印15次
    接着
    AA打印5次,BB打印10次,CC打印15次
    ....来10轮
* */
public class ThreadOrderAccess {
    public static void main(String[] args) {
        ShareResource shareResource=new ShareResource();

        new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print5(); },"线程A").start();
        new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print10(); },"线程B").start();
        new Thread(()->{ for (int i = 1; i <=10; i++)shareResource.print15(); },"线程C").start();
    }
}

class ShareResource{

    //设置一个标识,如果是number=1,线程A执行...
    private int number=1;

    Lock lock=new ReentrantLock();
    Condition condition1=lock.newCondition();
    Condition condition2=lock.newCondition();
    Condition condition3=lock.newCondition();


    public void print5(){
        lock.lock();
        try {
            //1.判断
            while(number!=1){
                condition1.await();
            }
            //2.干活
            for (int i = 1; i <=5; i++) {
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
            //3.唤醒
            number=2;
            condition2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try {
            //1.判断
            while(number!=2){
                condition2.await();
            }
            //2.干活
            for (int i = 1; i <=10; i++) {
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
            //3.唤醒
            number=3;
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try {
            //1.判断
            while(number!=3){
                condition3.await();
            }
            //2.干活
            for (int i = 1; i <=15; i++) {
                System.out.println(Thread.currentThread().getName()+":\t"+i);
            }
            //3.唤醒
            number=1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

虚假唤醒

虚假唤醒问题的产生

例子:要求用代码实现下面需求:

  • 一个卖面的面馆,有一个做面的厨师和一个吃面的食客,需要保证,厨师做一碗面,食客吃一碗面,不能一次性多做几碗面,更不能没有面的时候吃面;按照上述操作,进行十轮做面吃面的操作。

两个线程的情况

两个线程不会出现虚假唤醒问题,四个或多个线程才会出现

class Noodles{

    //面的数量
    private int num = 0;

    //做面方法
    public synchronized void makeNoodles() throws InterruptedException {
        //如果面的数量不为0,则等待食客吃完面再做面
        if(num != 0){
            this.wait();
        }

        num++;
        System.out.println(Thread.currentThread().getName()+"做好了一份面,当前有"+num+"份面");
        //面做好后,唤醒食客来吃
        this.notifyAll();
    }

    //吃面方法
    public synchronized void eatNoodles() throws InterruptedException {
        //如果面的数量为0,则等待厨师做完面再吃面
        if(num == 0){
            this.wait();
        }

        num--;
        System.out.println(Thread.currentThread().getName()+"吃了一份面,当前有"+num+"份面");
        //吃完则唤醒厨师来做面
        this.notifyAll();
    }

}

public class Test {

    public static void main(String[] args) {

        Noodles noodles = new Noodles();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"厨师A").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客甲").start();
    }
}

在这里插入图片描述

多个线程的情况

如果有两个厨师,两个食客,都进行10次循环呢?(出现线程虚假唤醒问题)
Noodles类的代码不用动,在主类中多创建两个线程即可,主类代码如下:

public class Test {

    public static void main(String[] args) {

        Noodles noodles = new Noodles();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"厨师A").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"厨师B").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客甲").start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客乙").start();

    }
}

在这里插入图片描述

出现原因

从上面结果可以看到,我们本来需要不能一次性多做几碗面,更不能没有面的时候吃面,但是上面结果却出现了这种情况。
我们一步一步对吃面情况进行分析,如下:

  • ①. 初始状态

在这里插入图片描述

  • ②. 厨师A得到操作权,发现面的数量为0,可以做面,面的份数+1,然后唤醒所有线程;
    在这里插入图片描述
  • ③. 厨师B得到操作权,发现面的数量为1,不可以做面,执行wait操作;
    在这里插入图片描述
  • ④. 厨师A得到操作权,发现面的数量为1,不可以做面,执行wait操作;
    在这里插入图片描述
  • ⑤. 食客甲得到操作权,发现面的数量为1,可以吃面,吃完面后面的数量-1,并唤醒所有线程;

在这里插入图片描述

  • ⑥. 此时厨师A得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程

在这里插入图片描述

  • ⑦. 此时厨师B得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程

在这里插入图片描述
综上:此时就会生产处两碗面

解决方案

循环判断,而不是判断一次就走人

	if(num != 0){
		this.wait();
	}
	改为	
	while(num != 0){
		this.wait();
	}

	if(num == 0){
		this.wait();
	}
	改为
	while(num == 0){
		this.wait();
	}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值