Java中JUC包详解

时间:2024-07-14 13:54:21

文章目录

    • J.U.C.包
      • Lock
      • ReadWriteLock
      • LockSupport
      • AQS
      • ReentrantLock
        • 对比synchronized
        • 加锁原理
        • 释放锁原理
      • CountDownLatch
      • CyclicBarrier
      • Semaphore

J.U.C.包

java.util.concurrent,简称 J.U.C.。是Java并发工具包,提供了在多线程编程中常用的工具类和框架,帮助开发者简化并发编程的复杂性,并提高程序的性能和可靠性。

java.util.concurrent.locks包下常用的类与接口是JDK1.5后新增的。lock的出现是为了弥补synchronized关键字解决不了的一些问题。例如,当一个代码块被synchronized修饰了,一个线程获取了对应的锁,并执行该代码块时,其他线程只能一直等待,等待获取锁的线程释放锁。如果这个线程因为某些原因被堵塞了,没有释放锁,那么其他线程只能一直等待下去,导致效率很低。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去,比如只等待一定的时间或者能够响应中断,通过Lock就可以办到。

java.util.concurrent包中的锁在locks包下:

在这里插入图片描述

LockReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLockReadWriteLock的代表实现类是ReentrantReadWriteLock

除了锁之外,java.util.concurrent包还提供了一些其他的工具类和框架,如SemaphoreCountDownLatchCyclicBarrier等。

Lock

Lock接口在Java的java.util.concurrent.locks包中定义,用于实现更灵活的线程同步机制。与传统的 synchronized 关键字相比,Lock接口提供了更多的操作和更细粒度的控制。在实际使用中,自然是能够替代synchronized关键字的。

Lock接口中的方法:

  • lock()lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已经被另一个线程持有,则当前线程将会被阻塞,直到锁被释放。如果使用lock方法必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此使用Lock必须在try-catch块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
    public void increment() {
        lock.lock();
        try {
            counter++;
            System.out.println(Thread.currentThread().getName() + ": " + counter);
        } finally {
            lock.unlock();
        }
    }
    
  • lockInterruptibly():获取锁,但与lock()方法不同,它允许线程在等待获取锁的过程中被中断。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,如果此时线程A获取到了锁,而线程B在等待,那么对线程B调用threadB.interrupt()能够中断线程B的等待过程。当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
    public class LockInterruptiblyExample {
        private final Lock lock = new ReentrantLock();
        private int counter = 0;
    
        public void increment() throws InterruptedException {
            lock.lockInterruptibly();
            try {
                counter++;
                System.out.println(Thread.currentThread().getName() + ": " + counter);
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            LockInterruptiblyExample example = new LockInterruptiblyExample();
    
            Runnable task = () -> {
                try {
                    example.increment();
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + " was interrupted.");
                }
            };
    
            Thread thread1 = new Thread(task);
            Thread thread2 = new Thread(task);
    
            thread1.start();
            thread2.start();
            thread2.interrupt(); // Interrupt the second thread
        }
    }
    
  • trylock():该方法的作用是尝试获取锁,如果锁可用则返回true,不可用则返回false
    public class TryLockExample {
        private final Lock lock = new ReentrantLock();
        private int counter = 0;
    
        public void increment() {
            if (lock.tryLock()) {
                try {
                    counter++;
                    System.out.println(Thread.currentThread().getName() + ": " + counter);
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " could not acquire the lock.");
            }
        }
    
        public static void main(String[] args) {
            TryLockExample example = new TryLockExample();
    
            Runnable task = example::increment;
    
            Thread thread1 = new Thread(task);
            Thread thread2 = new Thread(task);
    
            thread1.start();
            thread2.start();
        }
    }
    
  • newConditionLock接口提供了方法Condition newCondition();,返回的Condition类型也是一个接口,Condition提供了更细粒度的线程通信控制,用于实现复杂的线程间协作。类似于Object类中的wait()notify()notifyAll()方法。
    • await():当前线程等待,直到被通知或被中断。
    • signal():唤醒一个等待线程。如果所有线程都在等待,则任意选择一个线程唤醒。
    • signalAll():唤醒所有等待线程。
    public class ConditionExample {
      private final Lock lock = new ReentrantLock();
      private final Condition condition = lock.newCondition();
      private int counter = 0;
    
      public void increment() {
          lock.lock();
          try {
              while (counter == 0) {
                  condition.await();
              }
              counter++;
              System.out.println(Thread.currentThread().getName() + ": " + counter);
              condition.signal();
          } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
          } finally {
              lock.unlock();
          }
      }
    
      public void reset() {
          lock.lock();
          try {
              counter = 0;
              condition.signal();
          } finally {
              lock.unlock();
          }
      }
    
      public static void main(String[] args) {
          ConditionExample example = new ConditionExample();
    
          Runnable incrementTask = example::increment;
          Runnable resetTask = example::reset;
    
          Thread thread1 = new Thread(incrementTask);
          Thread thread2 = new Thread(resetTask);
    
          thread1.start();
          thread2.start();
      }
    }
    

ReadWriteLock

ReadWriteLock接口提供了一种用于在某些情况下可以显著提升并发性能的锁定机制。它允许多个读线程同时访问共享资源,但对写线程使用排他锁,这样读操作不会互相阻塞,而写操作会阻塞所有其他操作。

该接口有两个方法:

  • readLock():返回用于读取操作的锁。
  • writeLock():返回用于写入操作的锁。

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。对于ReetrantReadWriteLock其读锁是共享锁而写锁是独占锁,读锁的共享可保证并发读是非常高效的。需要注意的是,读写、写读、写写的过程是互斥的,只有读读不是互斥的。

public class ReadWriteLockExample {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();
    private int value = 0;

    // 读操作
    public int readValue() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " Reading: " + value);
            return value;
        } finally {
            readLock.unlock();
        }
    }

    // 写操作
    public void writeValue(int value) {
        writeLock.lock();
        try {
            this.value = value;
            System.out.println(Thread.currentThread().getName() + " Writing: " + value);
        } finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();

        Runnable readTask = () -> {
            for (int i = 0; i < 5; i++) {
                example.readValue();
                try {
                    Thread.sleep(100); // 模拟读取时间
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Runnable writeTask = () -> {
            for (int i = 0; i < 5; i++) {
                example.writeValue(i);
                try {
                    Thread.sleep(150); // 模拟写入时间
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Thread thread1 = new Thread(readTask);
        Thread thread2 = new Thread(readTask);
        Thread thread3 = new Thread(writeTask);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

LockSupport

LockSupportjava.util.concurrent.locks包下的一个工具类。它提供了最基本的线程阻塞和解除阻塞的功能,通常用来构建更高级的同步机制。其中有两个重要的方法,通过park()unpark()方法来实现阻塞和唤醒线程的操作,可以理解为wait()notify()的加强版。

  • park():阻塞当前线程,直到线程被其他线程中断或调用unpark()方法唤醒。
  • unpark():唤醒指定线程。如果该线程尚未阻塞,则下一次调用park()方法时不会阻塞。

传统等待唤醒机制是使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程。或者使用JUC包中Conditionawait()方法让线程等待,使用signal()方法唤醒线程。

wait()notify()/await()signal()方法必须要在同步块或同步方法里且成对出现使用,如果没有在synchronized代码块使用则抛出java.lang.IllegalMonitorStateException。必须先wait()/await()notify()/signal(),如果先notify()wait()会出现另一个线程一直处于等待状态。

LockSupport对比传统等待唤醒机制,能够解决传统等待唤醒问题。LockSupport使用的是许可机制,而wait/notify使用的是监视器机制。每个线程最多只有一个许可,调用park()会消耗一个许可,如果有许可则会直接消耗这张许可然后退出,如果没有许可就堵塞等待许可可用。调用unpark()则会增加一个许可,连续调用多次unpark()和调用一次一样,只会增加一个许可。而且LockSupportpark()unpark()是可中断的,且无需在同步块中使用。

public class LockSupportProducerConsumer {
    private static Object resource = null;

    public static void main(String[] args) {
        Thread consumer = new Thread(() -> {
            System.out.println("Consumer waiting for resource");
            while (resource == null) {
                LockSupport.park();
            }
            System.out.println("Consumer consumed resource");
        });

        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000); // Simulate some work with sleep
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            resource = new Object();
            System.out.println("Producer produced resource");
            LockSupport.unpark(consumer);
        });

        consumer.start();
        producer.start();
    }
}

LockSupport类使用了一种名为Permit的概念来做到阻塞和唤醒线程的功能,每个线程都有一个PermitPermit只有两个值1和0,默认是0。官网解释LockSupport是用来创建锁和同步其他类的基本线程的阻塞原语。LockSupport最终调用的Unsafe中的native方法。以unpark、park为例:

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

AQS

AQS是指java.util.concurrent.locks包下的一个抽象类AbstractQueuedSynchronizer译为,抽象的队列同步器

同步器是在多线程编程中用于管理线程间协作和同步的机制。同步器通常用于协调线程的执行顺序、控制共享资源的访问以及管理线程的状态。常见的同步器包括:CountDownLatch、CyclicBarrier、Semaphore等。

在JUC包下,能够看到有许多类都继承了AQS,如ReentrantLockCountDownLatchReentrantReadWriteLockSemaphore

在这里插入图片描述

AQS是用来构建锁或其它同步器组件的重要基础框架,以及是整个JUC体系的基石,它用于实现依赖先进先出队列的阻塞锁和相关的同步器。
AQS提供了一个框架,用于创建在等待队列中具有独占或共享模式的同步器。
在这里插入图片描述

AQS可以理解为一个框架,因为它定义了一些JUC包下常用"锁"的标准。AQS简单来说,包含一个status和一个队列。status保存线程持有锁的状态,用于判断该线程获没获取到锁,没获取到锁就去队列中排队。AQS中的队列,是指CLH队列(Craig, Landin, and Hagerste[三个人名组成])锁队列的变体,是一个双向队列。队列中的元素即Node结点,每个Node中包含:头结点、尾结点、等待状态、存放的线程等。Node遵循从尾部入队,从头部出队的规则,即先进先出原则。

在这里插入图片描述

在多线程并发环境下,使用lock加锁,当处在加锁与解锁之间的代码,只能有一个线程来执行。这时候其他线程不能够获取锁,如果不处理线程就会造成了堵塞。在AQS框架中,会将暂时获取不到锁的线程加入到队列里,这个队列就是AQS的抽象表现。它会将这些线程封装成队列的结点,通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

ReentrantLock

ReentrantLock译为可重入锁,是一种锁的实现类,它提供了比synchronized关键字更广泛的锁定操作选项,提供了公平锁和非公平锁两种模式。

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock();
        try {
            counter++;
            System.out.println(Thread.currentThread().getName() + " incremented counter to " + counter);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();
    }
}
对比synchronized

Java提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是JVM实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock

比较 synchronized ReentrantLock
锁的实现 JVM实现 JDK实现
性能 synchronized 与 ReentrantLock 大致相同 synchronized 与 ReentrantLock 大致相同
等待可中断 不可中断 可中断
公平锁 非公平锁 默认非公平锁,也可以是公平锁
锁绑定多个条件 不能绑定 可以同时绑定多个Condition对象
可重入 可重入锁 可重入锁
释放锁 自动释放锁 调用 unlock() 释放锁
等待唤醒 搭配wait()、notify或notifyAll()使用 搭配await()/singal()使用

synchronizedReentrantLock最直观的区别就是,在使用ReentrantLock的时候需要调用unlock方法释放锁,所以为了保证一定释放,通常都是和 try-finally 配合使用的。在实际开发中除非需要使用ReentrantLock的高级功能,否则优先使用synchronized。这是因为synchronized是JVM实现的一种锁机制,JVM原生地支持它,而ReentrantLock不是所有的JDK版本都支持。并且使用synchronized不用担心没有释放锁而导致死锁问题,因为JVM会确保锁的释放。

加锁原理

ReentrantLock原理用到了AQS,而AQS包括一个线程队列和一个state变量,state,它的值有3种状态:没