2Lock锁 (重点)

Posted zxhbk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2Lock锁 (重点)相关的知识,希望对你有一定的参考价值。

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:也是可重入锁,自由度高,可以判断锁,非公平锁(可以手动设置为公平锁)。

以上是关于2Lock锁 (重点)的主要内容,如果未能解决你的问题,请参考以下文章

pthreads等待和信号疑问linux

lock 和synchronized 的区别

python-锁机制

多线程下的锁

JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段

java多线程之线程安全(重点,难点)