对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁

目录

什么是JUC?

通过写售票代码回顾Synchronized和认识Lock

Synchronized

Lock

synchorized与Lock锁的区别

Synochorized的生产者和消费者问题

虚假唤醒问题

Lock中的生产者和消费者问题

深入理解Java锁


什么是JUC?

在Java 5.0时提供了 java.lang.concurrent 这个包,这个包简称JUC包,提供了一些处理并发编程下的一些工具类。

通过写售票代码回顾Synchronized和认识Lock

售票这段代码在学习多线程的时候是必定绕不开的一环,多个线程同时去争抢票这个资源。

那么我们再写写这个卖票的代码,思路核心是多线程操作同一资源类,资源类是单独的,没有任何附属操作,它就只有属性和方法。

Synchronized

资源类 Ticket

/**
 * 资源类 Ticket
 */
class Ticket2 {
    private int ticket = 30;
    public  void sale() {
            if (ticket>0){
                System.out.println(Thread.currentThread().getName() + "售出第" + ticket-- + "张票" + "还剩" + ticket + "张票");
            }
    }
}

线程类:使用了匿名内部类来减少了代码量

    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        }, "A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }            }
        }, "B").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }            }
        }, "C").start();
    }

但推荐使用Lambda表达式,看起来更加简洁。

    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } }, "A").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } }, "B").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } }, "C").start();
    }

目前为止,代码是线程不安全的,要想实现线程同步,初学的时候使用的是Sychorized,同步代码块或者方法,在资源类的方法上加上synchronize即可,这就是一个普通的线程同步方法。

    public synchronized void sale() {
            if (ticket>0){
                System.out.println(Thread.currentThread().getName() + "售出第" + ticket-- + "张票" + "还剩" + ticket + "张票");
            }
    }

学习J.U.C,就是学习java.util.concurrent包下的一些工具类来解决并发的问题。

接下来使用Lock类来解决线程安全问题。

Lock

Java 1.8中的Lock接口的三个实现类。

我们现在使用的是RenntrantLock,它需要显示的加锁和解锁,并需要与trycatch一起使用。

  • 实例化Reentrantlock
  • lock.lock(加锁)
  • lock.unlock(解锁)

这样,就使用了Lock来实现了线程安全。

/**
 * 资源类 Ticket
 */
class Ticket2 {
    private int ticket = 30;
    // 实例化ReentrantLock
     ReentrantLock lock = new ReentrantLock();
        public void sale() {
            // 开启锁
            lock.lock();
        try {
            if (ticket>0){
                System.out.println(Thread.currentThread().getName() + "售出第" + ticket-- + "张票" + "还剩" + ticket + "张票");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭锁
            lock.unlock();
        }
    }
}

synchorized与Lock锁的区别

  • Synchorized 是内置的Java关键字,Lock锁是一个java类
  • Synchorized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
  • Synchorized 会自动释放锁,lock必须要手动释放锁,如果不手动释放锁,会导致死锁
  • 如果发生了某个线程阻塞,Synchorized会一直等待,Lock锁不一定会等待下去(通过lock.trylock去尝试获得锁)
  • Synchorized 可重入锁,不可以中断,是非公平锁;Lock可重入锁,可以判断锁,非公平(可以自己设置)
  • Synchorized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码

解释一下可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

Synochorized的生产者和消费者问题

生产者和消费者问题用于理解线程之间的通讯,其思路是一个共同资源类被多线程操作并且交替执行。

如果不能理解如何写这种多线程操作并且交替执行的代码,先想想思路:

  • 资源类
    • 生产:判断等待-->业务(生产)-->通知其他线程
    • 消费:判断等待-->业务(消费)-->通知其他线程
  • 主方法
    • 创建线程并开启线程
/**
 * 生产者与消费者问题
 * 问题:现在两个线程,可以操作初始值为零的一个变量
 * 实现一个线程对该变量+1,一个线程对该变量-1
 * 实现10轮交替,变量初始值为0
 *
 *
 * 思路:高内聚低耦合的思路,线程操作资源类
 * --->    判断/业务/通知
 *
 * @author Claw
 * @date 2020/6/8 16:36.
 */
public class ConsumersAndProducers {
    public static void main(String[] args) {
        Product product = new Product();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                product.toProduct();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                product.toConsume();
            }
        }, "B").start();
    }
}

/**
 * 资源类
 */
class Product {

    private int product = 0;

    /**
     * 生产者
     */
    public synchronized void toProduct() {
        // 判断等待
        if (product !=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 业务操作
        product++;
        System.out.println(Thread.currentThread().getName()+"------->"+product);
        // 通知
        this.notifyAll();

    }

    /**
     * 消费者
     */
    public synchronized void toConsume() {
        // 判断等待
        if (product==0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 业务操作
        product--;
        System.out.println(Thread.currentThread().getName()+"------->"+product);
        // 通知
        this.notifyAll();
    }

结果:

A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0
A------->1
B------->0

这样看似没有什么问题,但如果有更多的线程进来,会导致一个问题,叫做虚假唤醒。

虚假唤醒问题

在主方法中新加两个线程

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                product.toProduct();
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                product.toConsume();
            }
        }, "D").start();

执行结果:在这里截取了结果异常的一段,可以看到出现了异常的数量2和3。

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第1张图片

这是为什么呢?API里解释了线程也可以被唤醒,而不会被通知,中断或超时,这就是所谓的虚假唤醒,等待应该总是出现在循环之中

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第2张图片

在上面的代码中,条件判断使用了if而不是while,这导致了虚假唤醒

多线程的交互必须用while

While的本质是循环+判断,线程被唤醒后,需要重新判断是否符合运行条件。

而if只判断了一次

看看这个错误的结果,为什么两个线程没有问题,而4个线程出现了问题?

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第3张图片

A和C是生产者,B和D是消费者

当A线程生产完毕后,ABCD四个线程都准备操作product,理想情况下,生产一个消费一个是我们想要的模式,所以我们会想着生产线程完毕,消费的线程进来。但事实上不是如此,当A线程生产完毕后,进来的是生产者线程C,此时product为1,我们使用if作为条件判断,当product !=0时,条件判断为true,C线程执行到wait()处,开始等待,释放了锁。此时product仍然为1,ABD三条线程开始想操作product,现在进入方法的仍然是A,执行到wait()后开始等待。

然后A和C两个生产线程都进行等待了,剩下的线程只剩下消费者线程。此时product为1,消费线程消费完product,唤醒其他线程,因为和C两个线程已经等待许久,系统会优先让A和C先执行,此时product为0,但因为使用了if语句作为条件判断,已经不满足条件了,但if不会判断第二次,唤醒后的A和C两个线程同时进行了++,product由1变成2。

所以需要使用while作为条件判断。

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第4张图片

Lock中的生产者和消费者问题

上面的synchorized来体会的生产者消费者的问题并不是对JUC学习的目的,而是回顾。

J.U.C中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。

synchronized让线程通讯的方法是:wait(),notify(),notifyAll().

Condition的与此相对应的方法是:await(),sign(),signAll()

Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

参考:JAVA并发-Condition

所以使用Lock来实现线程同步和通讯中,生产者和消费者问题的代码是这样的:

/**
 * 资源类
 */
class Product2 {

    private int product = 0;
    // 实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    // 实例化Condition
    private Condition condition = lock.newCondition();

    /**
     * 生产者
     */
    public synchronized void toProduct() {
        // 判断等待
        while (product !=0){
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 业务操作
        product++;
        System.out.println(Thread.currentThread().getName()+"------->"+product);
        // 通知
        condition.signalAll();
    }

    /**
     * 消费者
     */
    public synchronized void toConsume() {
        // 判断等待
        while (product==0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 业务操作
        product--;
        System.out.println(Thread.currentThread().getName()+"------->"+product);
        // 通知
        condition.signalAll();
    }
}

这样看来,与使用synchronize并没有什么优势的地方。

但Condition的强大之处是它可以为多个线程建立不同的Condition,让线程精准通讯,比如控制A线程执行完以后执行B,B执行完以后执行C。

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

* AA打印3次,BB打印3次,CC打印3次 * 循环3次 * * @author Claw * @date 2020/6/9 3:14. */ public class DataShare { public static void main(String[] args) { ShareResources sr = new ShareResources(); new Thread(() -> { for (int i = 0; i < 3; i++) { sr.printA(); } }, "线程1").start(); new Thread(() -> { for (int i = 0; i < 3; i++) { sr.printB(); } }, "线程2").start(); new Thread(() -> { for (int i = 0; i < 3; i++) { sr.printC(); } }, "线程3").start(); } } /** * 资源类 */ class ShareResources { // 实例化ReentrantLock ReentrantLock lock = new ReentrantLock(); // 实例化 Condition Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); Condition c3 = lock.newCondition(); /** * 标志位:A:1 B:2 C:3 */ private int number = 1; public void printA() { // 加锁 lock.lock(); // 判断等待 try { while (number != 1) { c1.await(); } // 业务 for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "\t" + "AA"); } // 设置标志位 number = 2; // 通知 c2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printB() { // 加锁 lock.lock(); try { while (number != 2) { // 判断等待 c2.await(); } for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "\t" + "BBB"); } // 标志位更改 number = 3; // 通知 c3.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printC() { // 加锁 lock.lock(); try { while (number != 3) { // 判断等待 c3.await(); } // 业务 for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "\t" + "CCC"); } // 标志位更改 number = 1; // 通知 c1.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }

深入理解Java锁

请看这段代码:

public class SynchronizedTest {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> { phone.sendEmail(); }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> { phone.sendMsg(); }).start();
    }
}

/**
 * 资源类
 */
class Phone {

    public synchronized void sendEmail() {
        System.out.println("发邮件");
    }
    public synchronized void sendMsg() {
        System.out.println("发短信");
    }
}
  • 标准访问,请问是先打印邮件还是短信?

结果:发邮件-->发短信

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第5张图片

  • 暂停4秒在邮件方法,请问打邮件还是短信?

在sendEmail中让线程休眠4秒

    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发邮件");
    }

结果: 仍然为发邮件--->发短信

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第6张图片

为什么?问题1和问题2是一样的

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中一个synchronized方法了,其他线程只能等待,换句话说,某一个时刻内,其他线程都不能进入到当前对象的其他synchronized方法。

锁的是当前对象this(也就是资源类Phone),被锁定后,其他线程线程都不能进入到当前对象的其他的synchronized方法。

两个方法用的都是同一把锁,所以一次只能进入一个线程。

  • 新增普通SayHello方法,请问先打印邮件还是hello?
/**
 * 资源类
 */
class Phone {

    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发邮件");
    }
    public synchronized void sendMsg() {
        System.out.println("发短信");
    }
    // 增加一个普通方法
    public void hello(){
        System.out.println("hello");
    }
}

让线程去调用发邮件以及hello的方法

    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> { phone.sendEmail(); }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> { phone.hello(); }).start();
    }

结果:

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第7张图片

为什么?

因为普通方法与同步锁无关

  • 两部手机,请问先打印邮件还是短信?

再实例化一个Phone类,一个实例对象去调用发邮件,一个实例去调用发短信,那么结果是什么?

public class SynchronizedTest {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> { phone.sendEmail(); }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> { phone2.sendMsg(); }).start();
    }
}

/**
 * 资源类
 */
class Phone {

    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发邮件");
    }
    public synchronized void sendMsg() {
        System.out.println("发短信");
    }
    public void hello(){
        System.out.println("hello");
    }
}

结果:

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第8张图片

为什么?

因为是两个对象,不是再共用一把锁,不冲突也不影响。

  • 两个静态同步方法,同一部手机,请问先打印邮件还是短信?

将资源类的发邮件和发短信改为静态同步方法

public class SynchronizedTest {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> { phone.sendEmail(); }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> { phone.sendMsg(); }).start();
    }
}

/**
 * 资源类
 */
class Phone {

    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发邮件");
    }
    public static synchronized void sendMsg() {
        System.out.println("发短信");
    }
    public void hello(){
        System.out.println("hello");
    }
}

结果:

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第9张图片

为什么?

synchronized修饰的同步方法,锁的是当前对象this,而对于静态同步方法,锁的就是整个Person类

  • 两个静态同步方法,2部手机,请问先打印邮件还是短信?

如果是两个手机,执行结果如何?这个跟上面的方法是一个意思。静态同步方法锁的对象是当前类的Class对象,因此谁先拿到锁,谁先执行。

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第10张图片

  • 1个静态同步方法,1个普通同步方法,同一部手机,请问先打印邮件还是短信?
    public static synchronized void sendEmail() {
       try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发邮件");
    }
    public  synchronized void sendMsg() {
        System.out.println("发短信");
    }

为什么?

静态同步方法和普通同步方法不是同一把锁,因此调用方法时不再需要等待。

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第11张图片

  • 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信?

对JUC的学习和理解(一):Lock、生产者和消费者问题、虚假唤醒、多线程8锁_第12张图片

为什么?

因为既不是同一把锁,也不是争抢同一资源,不会互相影响。

 

 

 

你可能感兴趣的:(Java,J.U.C)