likes
comments
collection
share

JUC(4):Java "锁"事一览

作者站长头像
站长
· 阅读数 15

我正在参加「掘金·启航计划」

一、Lock

1.1 概述

Lock 是 Java.util.concurrent.locks 包下的接口,Lock 实现提供了比 synchronized 关键字更广泛的锁操作,锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象

JUC(4):Java "锁"事一览

1.2 Lock 接口

Lock 是一个接口,接口的实现类有 ReentrantLock 和内部类 ReentrantReadWriteLock

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

1.2.1 lock

lock () 方法是平常使用最多的一个方法,就是用来获取锁,如果锁已被其他线程获取,则进行等待。

采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用 Lock 必须在 try-catch 块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被释放,防止死锁的发生。通常使用 Lock 来进行同步的话,是以下面这种形式去使用的。

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

1.2.2 tryLock

该方法表示用来尝试获取锁,但是该方法是有返回值的,如果获取成功,则返回 true,如果获取失败,则返回 false,也就是说这个方法无论如何都会立即返回,在拿不到锁时也不会一直在那等待。

    private static Lock lock = new ReentrantLock();
 
    public static void main(String[] args) {
        
        if(lock.tryLock()) {
            try{
                System.out.println("成功获取锁!!");
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        }else {
            System.out.println("未获取锁,先干别的");
        }
    
    }

1.2.2 newCondition

关键字 synchronized 与 wait\notify 这两个方法可以一起使用实现等待通知功能,lock 锁的 newCondition 方法返回 Condition 对象,Condition 类也可以实现等待\通知模式。

用 notify()通知时,JVM 会随机唤醒某个等待的线程,使用 Condition 类可以进行选择性的通知。它有两个常用方法:

  • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal 时,线程会重新获得锁并继续执行。
  • signal()用于唤醒一个等待的线程。

1.2.3 ReentrantLock(可重入锁)

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

public class LockTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    
    public static void main(String[] args) {
        LockTest test = new LockTest();
        new Thread(()->{test.insert(Thread.currentThread());}).start();
        new Thread(()->{test.insert(Thread.currentThread());}).start();
        new Thread(()->{test.insert(Thread.currentThread());}).start();
    }
    
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock(); //注意这个地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            Thread.sleep(1000);
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }
    }
}

ReentrantLock 意为可重入锁,关于可重入锁的概念将在后面详细讲述。它与 synchronized 一样,一个线程可以多次获取同一个锁。然而它比 synchronized 更安全,不会在 trylock 失败的时候发生死锁。

1.2.4 ReadWriteLock

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

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     */
    Lock writeLock();
}

两个方法,一个用来获取写锁,另一个用来获取读锁。

也就是说将文件的读写操作分开,分成 2个锁来分配给线程,从而使得多个线程可以同时读操作,或者一个线程存在写操作,读写不能同时存在。

下面的 ReentrantReadWriteLock 实现了ReadWriteLock 接口。

该实现类里面提供了两个主要的方法:readLock()和 writeLock() 用来获取读锁和写锁;

public class ReadWriteLockTest {

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(()->{test.get(Thread.currentThread());}).start();
        new Thread(()->{test.get(Thread.currentThread());}).start();
        new Thread(()->{test.set(Thread.currentThread());}).start();
    }

    public void get(Thread thread)
    {
        rwl.readLock().lock();
        try {
            System.out.println(thread.getName()+"正在进行读操作");
            Thread.sleep(1000);
            System.out.println(thread.getName()+"读操作完毕");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally
        {
            rwl.readLock().unlock();
        }
    }
    public void set(Thread thread)
    {
        rwl.writeLock().lock();
        try {
            System.out.println(thread.getName()+"正在进行写操作");
            Thread.sleep(2000);
            System.out.println(thread.getName()+"写操作完毕");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally
        {
            rwl.writeLock().unlock();
        }
    }
}

线程1 和线程2 可以同时读取数据。而线程3写锁则互斥。

  • 如果一个线程已经占用了读锁,其他线程要申请写锁,就要等待释放读锁;
  • 如果一个线程已经占用了写锁,其他线程如果要申请读锁,就要等待释放写锁;

二、synchronized 与 Lock 的区别

1、首先 synchronized 是 Java 内置关键字,在 JVM 层面,Lock 是个 Java 类;

2、synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁;

3、synchronized 会自动释放锁(a 线程执行完同步代码会释放锁;b 线程执行过程中发生异常会释放锁)

  • Lock 需要在 finally 中手工释放锁(unLock方法释放锁),否则容易造成线程死锁。

4、用 synchronized 关键字的两个线程1 和线程2 ,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。

5、synchronized 是锁可重入、不可中断,非公平,而 Lock 锁可重入,可判断,可公平(两者皆可)

6、Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。

三、synchronized

synchronized 的作用是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

synchronized 是 Java 中解决并发问题的一个常用的方法。

主要作用有三个:

  • 原子性:确保先互斥访问同步代码
  • 可见性:保证共享变量的修改能够及时可见
  • 有序性:解决重排序问题。

3.1 三种应用

1、修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁

2、修饰静态方法,作用于类的 Class 对象,进入修饰的静态方法前需要先获取类的 Class 对象的锁

3、修饰代码块,需要指定加锁对象(记作 lockobj),在进入同步代码块前需要先获取 lockobj 的锁

3.1.1 synchronized 作用于实例对象

synchronize作用于实例方法需要注意:

  1. 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
  2. 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法

3.1.2 synchronized 作用于静态方法

当synchronized作用于静态方法时,锁的对象就是当前类的Class对象。

3.1.3 synchronized 同步代码块

3.2字节码分析

  • synchronized 同步代码块,实际使用的是 monitorenter 和 monitorexit 指令;
    • 一般情况下就是1 个 enter 对应 2个 exit
  • synchronized 普通同步方法
    • javap -v class 文件反编译
    • JUC(4):Java "锁"事一览
    • 调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程会将先持有 monitor 锁,然后再执行方法,最后在方法完成时释放 monitor
  • synchronized 静态同步方法
    • 比普通方法多了个 ACC_STATIC 锁,来判断是否静态同步方法

3.3 synchronized 底层原语分析

天生带着 Object 的属性。

四、悲观锁与悲观锁

4.1 悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized 关键字和 Lock 的实现类都是悲观锁。

  • 适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 显式的锁定之后再操作同步资源。
//=============悲观锁的调用方式
public synchronized void m1()
{
    //加锁后的业务逻辑......
}

// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
    lock.lock();
    try {
        // 操作同步资源
    }finally {
        lock.unlock();
    }
}

4.2 乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有么有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢

乐观锁一般有两种实现方式:

  1. 采用版本号机制
  2. CAS(Compare-and-Swap,即比较并替换)算法实现
//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

五、八锁案例

原则:能用无锁就不要用锁。能用对象锁,就不用要类锁。

5.1 案例说明

线程操纵资源类

5.1.1 情况1

class Phone{
    public synchronized void sendEmail(){
        System.out.println("发送邮件!");
    }
    public synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone.sendMsg();
        },"b").start();
    }
}

//发送邮件!
//发送短信!

5.1.2 情况2

在发送邮件方法中假如暂停 3秒钟,先打印哪个?

  • 答:是用的同一个对象,锁的是同一个对象。
class Phone{
    public synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone.sendMsg();
        },"b").start();
    }
}
======
发送邮件!
发送短信!

5.1.3 情况3

加入一个普通的方法。

答:hello 不用去拿锁

class Phone{
    public synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
    public  void hello(){
        System.out.println("hello!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone.hello();
        },"b").start();
    }
}
======
hello!
发送邮件!

5.1.4 情况4

两个对象

答:此时锁的不是同个对象

class Phone{
    public synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
    public  void hello(){
        System.out.println("hello!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone2.sendMsg();
        },"b").start();
    }
}
====
    发送短信!
发送邮件!

5.1.5 情况5

加上静态方法

答:静态方法锁的是类

class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public static synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
    public  void hello(){
        System.out.println("hello!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone.sendMsg();
        },"b").start();
    }
}
==
发送邮件!
发送短信!

5.1.6 情况6

class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public static synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
    public  void hello(){
        System.out.println("hello!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone2.sendMsg();
        },"b").start();
    }
}
====
发送邮件!
发送短信!

5.1.7 情况7

class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public  synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
    public  void hello(){
        System.out.println("hello!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone2.sendMsg();
        },"b").start();
    }
}
=====
发送短信!
发送邮件!

5.1.8 情况8

class Phone{
    public static synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送邮件!");
    }
    public  synchronized void sendMsg(){
        System.out.println("发送短信!");
    }
    public  void hello(){
        System.out.println("hello!");
    }
}
public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        new Thread(()->{
            phone.sendMsg();
        },"b").start();
    }
}
=====
发送短信!
发送邮件!

5.2 总结

由 情况1-2 得知:

一个对象里面如果有多个 synchronized 方法,某一时刻内,只要一个线程去调用其中的一个 synchronized 方法了。

其他的线程都只能等待,换句话说。某一个时刻内,只能有唯一的一个线程去访问这些 synchronized 方法。

锁的是当前对象的 this,被锁定后,其他的线程都不能进入当前对象的其他的 synchronized 方法。

加上静态方法,此时锁的是类

六、公平锁和非公平锁

公平锁:

  • 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
  • Lock lock = new ReetrantLock(true) true 表示公平锁,先来先得。

非公平锁:

  • 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某一个线程一直得不到锁)
  • Lock lock = new ReetrantLock(false) 默认就是非公平锁,可以不写

七、可重入锁(递归锁)

可重入锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronzied 修饰的递归方法,程序第二次进入被自己阻塞了岂不是天大的笑话。所以Java 中 ReetrantLock 和 synchronized 都是可重入锁。

可重入锁的一个优点是可一定程度避免死锁

7.1可重入锁这四个字分开来解析

可:可以。

重:再次

入:进入

锁:同步锁

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁

7.2 可重入锁种类

1、隐式锁(synchronized 关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁。

在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。

简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

同步块:

public class ReEntryLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println("-----外层调用");
                synchronized (objectLockA){
                    System.out.println("-----中层调用");
                    synchronized (objectLockA){
                        System.out.println("-----内层调用");
                    }
                }
            }
        },"a").start();
    }
}

同步方法:

public class ReEntryLockDemo{
    public synchronized void m1(){
        System.out.println("-----m1");
        m2();
    }
    public synchronized void m2(){
        System.out.println("-----m2");
        m3();
    }
    public synchronized void m3(){
        System.out.println("-----m3");
    }

    public static void main(String[] args){
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

        reEntryLockDemo.m1();
    }
}

2、显示锁(Lock)也有 ReetrantLock 这样的可重入锁

public class Demo4 {
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    private static void add() {
        lock.lock();
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo4.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo4.num);
    }
}

上面代码中add()方法中,当一个线程进入的时候,会执行2次获取锁的操作,运行程序可以正常结束,并输出和期望值一样的30000,假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一样,可重入锁。

代码中还有几点需要注意:

  1. lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
  2. unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
/**
 * @create 2020-05-14 11:59
 * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class ReEntryLockDemo{
    static Lock lock = new ReentrantLock();

    public static void main(String[] args){
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("----外层调用lock");
                lock.lock();
                try
                {
                    System.out.println("----内层调用lock");
                }finally {
                    // 这里故意注释,实现加锁次数和释放次数不一样
                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                    lock.unlock(); // 正常情况,加锁几次就要解锁几次
                }
            }finally {
                lock.unlock();
            }
        },"a").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("b thread----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"b").start();

    }
}

7.3 synchronized 可重入锁的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java 虚拟机会将该所对象的持有线程设置为当前线程,并且将其计数器加1 .

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1 ,否则需要等待,直至持有线程释放该锁。

当执行 monitorexit 时,Java 将锁对象的计数器 减少1,计数器为零代表锁被释放。

八、死锁

死锁是指两个或两个以上线程在执行过程中,因为争抢资源而造成的一种互相等待的现象。

JUC(4):Java "锁"事一览

8.1 产生死锁的原因

1、系统资源不足

2、进程运行推进的顺序不合适

3、资源分配不当

public class DeadLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockB)
                {
                    System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (objectLockB){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockA){
                    System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
                }
            }
        },"B").start();

    }
}

8.2 如何排查死锁

1、纯命令

jps -l
jstack 进程编号

2、图形化

jconsole