JUC-5. 锁(一)

想了解更多JUC的知识——JUC并发编程合集

1. Lock接口

  • Lock接口有三个实现类:ReentrantLock,ReentrantLockReadWriteLock.ReadLock,ReentrantLockReadWrite.WriteLock

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zslDoOpl-1659443954304)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220726142035304.png)]

  • ReentrantLock:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RILoSzQy-1659443954306)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220726142456960.png)]

  • Lock接口的主要方法:

    • void lock():获取锁,如果锁被暂用则一直等待
    • void unlock():释放锁
    • boolean tryLock(): 尝试获取锁,如果获取锁的时候锁被占用就返回false,否则返回true
    • boolean tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
    • void lockInterruptibly():获取锁,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事

2. synchronized和lock演示

  • 传统的synchronized

    public class SaleTicketDemo01 {
        public static void main(String[] args) {
            //并发:多线程操作操作同一个资源类,把资源列放入线程
            Ticket ticket = new Ticket();
    
            //@FunctionalInterface:函数式接口,jdk8 lambda表达式(参数)->{ 代码 }
            new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"A").start();
            new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"B").start();
            new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"C").start();
        }
    }
    
    //资源类 OOP面向对象编程
    class Ticket{
        //属性、方法
        private int number = 30;
    
        //卖票的方式
        public synchronized void sale(){
            if (number > 0){
                System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余:" + number);
            }
        }
    }
    
  • lock锁

    public class SaleTicketDemo02 {
        public static void main(String[] args) {
            //并发:多线程操作操作同一个资源类,把资源列放入线程
            Ticket2 ticket = new Ticket2();
            //@FunctionalInterface:函数式接口,jdk8 lambda表达式(参数)->{ 代码 }
            new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"A").start();
            new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"B").start();
            new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"C").start();
        }
    }
    //lock三部曲
    //1.Lock lock=new ReentrantLock();
    //2.lock.lock() 加锁
    //3.finally=> 解锁:lock.unlock();
    //资源类 OOP
    class Ticket2{
        //属性、方法
        private int number = 30;
        Lock lock = new ReentrantLock();
    
        //卖票的方式
        public void sale(){
            lock.lock();
            try{
                //业务代码
                if (number > 0){
                    System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余:" + number);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    

3. Synchronized与Lock的区别

  1. 存在的层次
    • synchronized是Java的一个关键字,在jvm层面上
    • Lock是一个接口
  2. 锁的释放
    • synchronized会自动释放锁
      • 获取这个锁的线程执行完同步代码,释放锁
      • 线程执行发生异常,jvm会让线程释放锁
    • Lock必须手动加锁和手动释放锁(在try/catch/finally的finally中释放锁),如果不手动释放锁,容易造成死锁
  3. 锁的状态
    • synchronized无法判断和获取锁的状态
    • Lock可以判断锁的状态
  4. 锁的获取
    • synchronized,如果有两个线程,线程1获取锁,线程2必须等待线程1释放锁;如果线程1阻塞,线程2会一直等待
    • Lock不一定会一直等待,可以通过tryLock去尝试获取锁,不会造成过久的等待
  5. 锁的类型
    • synchronized是可重入、不可中断、非公平的锁
    • Lock可重入、可判断的锁,而且可以根据需要自己设置公平锁和非公平锁
  6. 性能
    • synchronized适合锁少量的代码同步问题
    • Lock适合锁大量的同步代码

4. 生产者消费者问题

4.1 synchronized版本
public class ConsumeAndProduct {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

class Data {
    private int num = 0;
    // +1
    public synchronized void increment() throws InterruptedException {
        // 判断等待
        if (num != 0) {
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        // 通知其他线程 +1 执行完毕
        this.notifyAll();
    }
    // -1
    public synchronized void decrement() throws InterruptedException {
        // 判断等待
        if (num == 0) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        // 通知其他线程 -1 执行完毕
        this.notifyAll();
    }
}

如果上述的流程从创建两个线程变为创建四个线程,则会出现虚假唤醒的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0XjwHpNP-1659443954306)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220726185722491.png)]

解决的方式:将if判断换成while判断,即:

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

用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码;而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

4.2 Lock版本
public class ConsumeAndProduct {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0;i<10;i++){
                data.increment();
            }
        },"A").start();
        new Thread(() -> {
            for (int i = 0;i<10;i++){
                data.decrement();
            }
        },"B").start();
        new Thread(() -> {
            for (int i = 0;i<10;i++){
                data.increment();
            }
        },"C").start();
        new Thread(() -> {
            for (int i = 0;i<10;i++){
                data.decrement();
            }
        },"D").start();
    }
}

class Data{
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void increment(){
        //加锁
        lock.lock();
        try {
            while (number != 0){
                //等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + ":" + number);
            //通知其他线程
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //解锁
            lock.unlock();
        }
    }

    public void decrement(){
        lock.lock();
        try {
            while (number == 0){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + ":" + number);
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

从中可以得出:

  • synchronized是自动释放锁的,而lock是通过lock.lock()来手动加锁和lock.unlock()来手动释放锁;

  • synchronize是通过wait()来等待,而lock是lock.newCondition()创建一个Condition对象,通过Condition的await()方法来等待

  • synchronize是通过notifyAll()来通知其他线程,lock是通过Condition的condition.signalAll()来通知其他线程

4.3 Condition的优势
  • Condition可以精准的通知和唤醒的线程

    //案例,让A执行完调用B,B执行完调用C,C执行完调用A
    public class ConditionDemo {
        public static void main(String[] args) {
            Data data = new Data();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.printA();
                }
            },"A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.printB();
                }
            },"B").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    data.printC();
                }
            },"C").start();
        }
    }
    
    class Data {
        private Lock lock = new ReentrantLock();
        private Condition condition1 = lock.newCondition();
        private Condition condition2 = lock.newCondition();
        private Condition condition3 = lock.newCondition();
        private int num = 1; // 1A 2B 3C
    
        public void printA() {
            lock.lock();
            try {
                while (num != 1) {
                    condition1.await();
                }
                System.out.println(Thread.currentThread().getName() + "==> A" );
                num = 2;
                condition2.signal();
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        public void printB() {
            lock.lock();
            try {
                while (num != 2) {
                    condition2.await();
                }
                System.out.println(Thread.currentThread().getName() + "==> B" );
                num = 3;
                condition3.signal();
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        public void printC() {
            lock.lock();
            try {
                while (num != 3) {
                    condition3.await();
                }
                System.out.println(Thread.currentThread().getName() + "==> C" );
                num = 1;
                condition1.signal();
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    

5. 八锁现象(彻底理解)

  • 如何判断的是谁?(锁住对象?锁住Class)

  • 案例一:两个同步方法,先执行发短信还是打电话?

    public class dome01 {
        public static void main(String[] args) {
            Phone phone = new Phone();
            new Thread(() -> { phone.sendMsg(); },"A").start();
            //睡眠一秒
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone.call(); },"B").start();
        }
    }
    
    class Phone {
        public synchronized void sendMsg() {
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例一的输出结果总是先输出"发短信",后输出"打电话"。

    思考:是因为顺序执行吗?

    结论:并不是因为顺序执行,Java的线程调度是抢占式的,即线程A和线程B都有可能抢占到CPU。在线程A和线程B调用了start方法后,A和B都进入了就绪状态,但是TimeUnit.SECONDS.sleep(1)的存在会让A和B不是同时进入就绪态,即A总会比B先抢占到CPU。而synchronized锁住的是对象的调用者,两个方法共用同一把锁,谁先拿到谁先用,另外一个等待。所以输出结果总是先输出"发短信",后输出"打电话"。

  • 案例二:在案例一的基础上,让发短信延时4s

    public class demo02 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone = new Phone();
    
            new Thread(() -> { phone.sendMsg(); },"A").start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone.call(); },"B").start();
        }
    }
    
    class Phone {
        public synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例二的输出结果总是先输出"发短信",后输出"打电话"。

    思考:为什么结果与案例一一样?

    结论:原因与案例一一致,在方法sendMsg里延时4s不会影响线程A先获取锁的使用权

  • 案例三:在案例二的基础上再加一个普通方法

    public class demo03 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone = new Phone();
    
            new Thread(() -> { phone.sendMsg(); }).start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone.hello(); }).start();
        }
    }
    
    class Phone {
        public synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
        public void hello(){
            System.out.println("hello!");
        }
    }
    

    案例三的输出结果总是先输出"hello!“,后输出"发短信”。

    结论:因为hello()只是一个普通方法,不受synchronized锁的影响,不用等待锁的释放。(间接证明了此时synchronized锁锁住的是对象对方法的调用

  • 案例四:如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?

    public class demo04 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone1 = new Phone();
            Phone phone2 = new Phone();
    
            new Thread(() -> { phone1.sendMsg(); }).start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone2.call(); }).start();
        }
    }
    
    class Phone {
        public synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例四的输出结果总是先输出"打电话",后输出"发短信"。

    结论:两个对象用了两把锁,线程A与线程B获取两个不同的锁互不影响,不会出现等待的情况,但是方法sendMsg在输出"发短信"前睡眠了4s,所以会先输出"打电话"。

  • 案例五:如果我们把synchronized的方法加上static变成静态方法,那么顺序又是怎么样的呢?

    public class demo05 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone = new Phone();
    
            new Thread(() -> { phone.sendMsg(); }).start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone.call(); }).start();
        }
    }
    
    class Phone {
        public static synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public static synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例五的输出结果总是先输出"发短信",后输出"打电话"。

    结论:原因还是线程A先获得CPU,也就先获得了锁,但是这里的锁已经发生了变化。

  • 案例六:在案例五的基础上使用两个对象调用两个方法

    public class demo06 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone1 = new Phone();
            Phone phone2 = new Phone();
    
            new Thread(() -> { phone1.sendMsg(); }).start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone2.call(); }).start();
        }
    }
    
    class Phone {
        public static synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public static synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例六的输出结果总是先输出"发短信",后输出"打电话"。

    思考:为什么不是和案例四一样的结果?

    结论:分析后我们不难得知,两个线程仍共用一把锁,原因也很简单,我们在两个方法前加了static关键字。对于static静态方法来说,整个类Class只有一份,对于不同的实例对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个Class对象,不管多少个实例对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待。

  • 案例七:如果使用一个静态同步方法、一个同步方法、一个对象调用顺序是什么

    public class demo07 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone = new Phone();
    
            new Thread(() -> { phone.sendMsg(); }).start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone.call(); }).start();
        }
    }
    
    class Phone {
        public static synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例七的输出结果总是先输出"打电话",后输出"发短信"。

    结论:此时有两把锁,一把锁锁的是Class类这个模板,另一把锁锁的是对象的调用,故不存在等待情况,而方法sendMsg里睡眠了4s,故先输出"打电话"。

  • 案例八:如果我们使用一个静态同步方法、一个同步方法、两个对象调用顺序是什么

    public class demo01 {
        public static void main(String[] args) throws InterruptedException {
            Phone phone1 = new Phone();
            Phone phone2 = new Phone();
    
            new Thread(() -> { phone1.sendMsg(); }).start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(() -> { phone2.call(); }).start();
        }
    }
    
    class Phone {
        public static synchronized void sendMsg(){
            try {
                //睡眠4s
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发短信");
        }
        public synchronized void call() {
            System.out.println("打电话");
        }
    }
    

    案例八的输出结果总是先输出"打电话",后输出"发短信"。

    结论:道理与案例七一样,两把不同的锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Daylan Du

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值