• 2、Lock锁 (重点)


    Lock锁

    传统 synchronized

    举例:买票的栗子

    真正的多线程开发,公司中的开发,需要降低耦合度

    线程是一个单独的资源,没有任何附属的操作!

    单独的资源包含属性、方法

    第一种:高耦合写法,Ticket线程类还有附属操作,不推荐使用

    public class SaleTicketDemo01 {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            new Thread(ticket).start();
        }
    }
    class Ticket implements Runnable{
    
        @Override
        public void run() {
    
        }
    }

    第二种:降低耦合度

    //举例买票的栗子
    
    /**
     * 真正的多线程开发,公司中的开发,降低耦合性
     * 线程就是一个单独的资源类,没有任何附属的操作!
     * 1、 属性、方法
     */
    public class SaleTicketDemo01 {
        public static void main(String[] args) {
            // 并发,就是多个线程操作同一份资源,使用时直接丢入线程
            Ticket ticket = new Ticket();
    
            // 3个人同时卖票
            // Runnable接口标注着 @FunctionalInterface 表示函数式接口,在jdk1.8,可以使用lambda表达式 ()->{}
            new Thread(() -> {
                for (int i = 0; i < 40; i++) {  //卖40张肯定会卖完
                    ticket.sale();  //调用买票方法,就是操作资源
                }
            }).start();
            new Thread(() -> {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }).start();
            new Thread(() -> {
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }).start();
    
        }
    }
    class Ticket{
    
        //剩余的票
        private int number = 30;
    
        // 售票方法
        // synchronized同步方法,本质:相当于队列,锁
        // 比如:食堂学生打饭,不排队学生就会一拥而至;排队的情况下,相当于一个人打饭会有一个锁,打完饭后释放锁;
        public synchronized void sale(){
            if(number > 0){
                System.out.println("已售出第" + number-- + "张票,剩余:" + number + "张");
            }
        }
    
    }

      多线程下产生并发问题,需要加上synchronized,同步方法;

     Lock接口

    解析

    1)实现类

    • 最常用的是可重入锁ReentrantLock

       2)使用方法

     3) 可重入锁:ReentrantLock类

     有两种构造方法,可以构造非公平锁和公平锁,默认是公平锁!

    公平锁:顾名思义非常的公平

    • 讲究一个先来后到
    • 比如:两个线程的执行时间分别是 3s 和 3h,那么 3h 在 3s 前面,那么必须等待 3h 之后才能执行!

    非公平锁:顾名思义它是不公平的

    • 可以被插队!(默认的)

     

    买票的栗子

    package com.zxh.demo01;
    
    //举例买票的栗子
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class SaleTicketDemo02 {
        public static void main(String[] args) {
            // 并发,就是多个线程操作同一份资源,使用时直接丢入线程
            Ticket ticket = new Ticket();
    
            // 3个人同时卖票
            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();
    
        }
    }
    
    // Lock三部曲
    // 1、new ReentrantLock(); 创建锁
    // 2、lock.lock();    // 加锁
    // 3、lock.unlock();  // 解锁
    class Ticket2 {
    
        //剩余的票
        private int number = 30;
    
        Lock lock = new ReentrantLock();
    
        // 售票方法
        public void sale(){
    
            lock.lock();    // 加锁
    //        lock.tryLock(); //尝试获取锁,只有在调用时它不被另一个线程占用才能获取锁,获取成功返回true,否则返回false
            try {
                // 业务代码
                if(number > 0){
                    System.out.println(Thread.currentThread().getName() + "已售出第" + number-- + "张票,剩余:" + number + "张");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // 解锁
            }
        }
    
    }

    可重入锁的解析

    问题:什么是可重入锁呢?

    广义上的可重入锁:

    可重入锁就是可以重复可递归调用的锁,就是在外层使用锁后,内层依旧可以使用该锁,并且不会发生死锁(可重入锁:前提锁的是同一个对象或者class,如果锁的东西不同,那就不是可重入锁了)。

    可重入锁是以线程为单位的,比如:当一个线程获取对象加锁后,该线程可以再次获取该对象的锁,但是其他线程不行,需要等待该线程释放锁。

    synchronizedReentrantLock都是可重入锁。

     

    解释了这么多,可能还是不太明白,接下来通过栗子进行解释什么是可重入锁。

    可重入锁:synchronized

    public class MyTest {
        public static void main(String[] args) {
            Data data = new Data();
            /*
                synchronized:锁的是方法的调用者,就是对象 data
                现在创建两条线程,各自去调用10次get方法,或者更多
                你会发现,一个线程获取了两次锁,并没有发生死锁,并且哪个线程谁先拿到锁,其他的线程只能等待
                    (运行结果可能存在,B线程在A线程之间调用了get和set方法,
                        那是因为A线程释放了锁,并且CPU刚好去执行B线程了,尽管如此你还是会发现,get()set()方法都是连在一起执行的)
                所以:这就是可重入锁,该锁可以被重复递归的调用
              */
            // 这里为了截图方便调用3次get()方法
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start();
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start();
        }
    }
    class Data{
        /**
         * 两个方法都是同步方法,作用:打印线程的名字
         * get()方法调用set(),一个同步方法调用另一个同步方法
         */
        public synchronized void get(){
            System.out.println(Thread.currentThread().getName() + "=> get()");
            set();
        }
    
        public synchronized void set(){
            System.out.println(Thread.currentThread().getName() + "=> set()");
        }
    }

     可重入锁:ReentrantLock

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class MyTest {
        public static void main(String[] args) {
            Data data = new Data();
            // 这里为了截图方便调用3次get()方法
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start();
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start();
        }
    }
    class Data{
        Lock lock = new ReentrantLock();    // 可重入锁
    
        /**
         * 作用:打印线程的名字
         * get()方法调用set(),一个同步方法调用另一个同步方法
         */
        public void get(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> get()");
                set();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void set(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> set()");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    可重入锁(实现原理)

    自定义不可重入锁

    • 现在我们自己定义一个简单的锁,并且不是可重入锁,我们可以看一下会发生什么

    public class MyTest {
        public static void main(String[] args) {
            Data data = new Data();
            // 这里为了截图方便调用3次get()方法
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start();
    //        new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start();
        }
    }
    class Data{
        MyLock lock = new MyLock();    // 可重入锁
    
        /**
         * 作用:打印线程的名字
         * get()方法调用set(),一个同步方法调用另一个同步方法
         */
        public void get(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> get()");
                set();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void set(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> set()");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    // 自定义简单锁
    class MyLock{
        private boolean flag = false;   // 使用变量控制该锁是否被占用
        // 加锁
        public synchronized void lock() {
            while (flag){   // 如果是true,那么表示该锁被占用
                try {    
                    this.wait();    // 线程等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            flag = true;    // 加锁
        }
        // 解锁
        public synchronized void unlock(){
            flag = false;   // 释放锁
            notifyAll();    // 唤醒其他线程
        }
    
    }

    发现即便使用单个线程,同样发生了死锁,同一个线程调用get()方法后,无法调用set()方法(也就是无法再次获取该锁),显然我们自定义的锁无法实现重复的调用。所以我们现在定义的锁,也就是不可重入锁。

     

    接下来说一下可重入锁的实现原理?

    实现原理:通过为每一个锁关联一个请求计数器和一个占用它的线程。当计数器为0,代表该锁没有被占用;当线程请求一个未被占用的锁,那么JVM将记录锁的占用者,并且将请求计数器置为1。

    如果同一个线程再次请求该锁,那么计数器会递增+1。

    每次占用线程退出同步块,请求计数器会 -1,直到计数器为0才释放锁。

     

    修改自定义的锁为可重入锁

    public class MyTest {
        public static void main(String[] args) {
            Data data = new Data();
            // 这里为了截图方便调用3次get()方法
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start();
            new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start();
        }
    }
    class Data{
        MyLock lock = new MyLock();    // 可重入锁
    
        /**
         * 作用:打印线程的名字
         * get()方法调用set(),一个同步方法调用另一个同步方法
         */
        public void get(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> get()");
                set();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void set(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> set()");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    // 自定义简单锁
    class MyLock{
        private boolean flag = false;   // 使用变量控制该锁是否被占用
        private int lockCount = 0;  // 关联一个请求计数器
        private Thread thread = null;   // 关联一个占用它的线程
        // 加锁
        public synchronized void lock() {
            Thread currentThread = Thread.currentThread();
            while (flag && this.thread != currentThread){   // 如果该锁被占用,并且进入的线程不是当前占用的线程
                try {
                    this.wait();    // 线程等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 锁没有被占用 或者 锁被占用但是为同一个线程进入
            flag = true;    // 加锁
            lockCount++;    // 请求计数器+1
            this.thread = currentThread;    // 被当前进入的线程占用
        }
        // 解锁
        public synchronized void unlock(){
            if (this.thread == Thread.currentThread()){ // 如果进入的线程是当前占用的线程
                lockCount--;    // 请求计数器 -1
                if (lockCount == 0){    // 如果请求计数器为0
                    flag = false;   // 释放锁
                    notifyAll();    // 唤醒其他线程
                }
            }
        }
    
    }

    synchronized 和 Lock 区别

    特征区别

    1. synchronized:是Java的关键字,Lock:是Java的类
    2. synchronized:适合锁少量的同步代码,而Lock:适合锁大量的同步代码

    详细区别

    1. synchronized:会自动释放锁,Lock:需要手动释放锁。
    2. synchronized:无法判断锁的状态,Lock:可以判断锁的状态(锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁,会根据线程之间的竞争从前到后转换)
    3. synchronized:多个线程会等待(比如:线程1(阻塞),线程2(死死等待)),Lock:就不一定会等待,可以通过 lock.tryLock() 尝试获取锁。
    4. synchronized:是可重入锁,不可以中断,非公平锁,Lock:也是可重入锁,自由度高,可以判断锁,非公平锁(可以手动设置为公平锁)。
    致力于记录学习过程中的笔记,希望大家有所帮助(*^▽^*)!
  • 相关阅读:
    Javascript 返回上一页
    html中link和import方式导入CSS文件的区别
    Leecode no.76 最小覆盖子串
    Leecode no.344 反转字符串
    Leecode no.167 两数之和 II 输入有序数组
    Leecode no.567 字符串的排列
    遍历目录和文件信息
    JAVASCRIPT显示农历的日历
    asp.net上传图片加水印(c#)
    asp.net XML操作类
  • 原文地址:https://www.cnblogs.com/zxhbk/p/12943949.html
Copyright © 2020-2023  润新知