一篇文章讲完java中的锁

悲观锁和乐观锁都是并发编程中常用的锁机制,用于处理多线程对共享资源的访问。

悲观锁:认为并发访问会导致冲突,因此在访问共享资源前,悲观地认为其他线程可能会修改该资源,因此采取加锁的方式,保证当前线程独占该资源的访问权,其他线程必须等待当前线程释放锁后才能访问该资源。例如,Java中的synchronized关键字就是一种悲观锁机制。

乐观锁:相反,乐观锁认为并发访问不会导致冲突,因此不加锁,而是在访问共享资源时,先获取该资源的版本号或者时间戳等标识,然后进行操作,在更新该资源时,比较当前版本号或时间戳是否与之前获取的相同,如果相同,则说明该资源没有被其他线程修改过,可以进行更新,否则说明该资源已经被其他线程修改,操作失败,需要重试。例如,Java中的CAS(Compare and Swap)操作就是一种乐观锁机制。

悲观锁和乐观锁都有各自的优缺点。悲观锁虽然保证了数据的安全性,但是需要频繁地加锁和释放锁,会带来较大的性能开销,尤其在高并发的情况下会造成资源的浪费。乐观锁虽然避免了加锁和释放锁的开销,但是需要进行额外的版本号或时间戳的比较和更新操作,如果并发冲突较多,重试次数会增多,影响性能。因此,在实际开发中需要根据具体情况选择合适的锁机制。

乐观锁实现方式

乐观锁通常使用版本号(Version Number)或时间戳(Timestamp)等方式来实现。

使用版本号实现乐观锁的步骤如下:

  1. 在需要并发访问的数据结构中,增加一个版本号字段,初始值为1。
  2. 当某个线程需要读取该数据时,先读取版本号并保存到本地。
  3. 当该线程需要对数据进行更新时,先对本地保存的数据进行修改,并将版本号加1。
  4. 当该线程尝试将更新后的数据提交回数据结构时,需要比较本地保存的版本号和当前数据结构中的版本号是否相同。如果相同,则表示该数据没有被其他线程修改,可以更新数据结构中的数据和版本号;否则,需要重试更新操作。

使用时间戳实现乐观锁的步骤如下:

  1. 在需要并发访问的数据结构中,增加一个时间戳字段,记录最后一次修改该数据的时间。
  2. 当某个线程需要读取该数据时,先读取时间戳并保存到本地。
  3. 当该线程需要对数据进行更新时,先对本地保存的数据进行修改,并将时间戳更新为当前时间。
  4. 当该线程尝试将更新后的数据提交回数据结构时,需要比较本地保存的时间戳和当前数据结构中的时间戳是否相同。如果相同,则表示该数据没有被其他线程修改,可以更新数据结构中的数据和时间戳;否则,需要重试更新操作。

乐观锁的实现方式相对简单,但需要考虑到并发冲突的情况,需要设计合适的重试机制,避免出现死锁或者数据不一致的情况。

Java 锁的分类

Java中锁的分类可以根据不同的维度进行划分,下面是一些常见的分类方式:

  1. 按照锁的粒度分类:
  • 细粒度锁:例如synchronized关键字,锁的粒度较小,可以用于保护单个对象的状态。
  • 粗粒度锁:例如ReentrantLock等,锁的粒度较大,可以用于保护多个对象的状态。
  1. 按照锁的可重入性分类:
  • 可重入锁:例如ReentrantLock等,同一个线程可以多次获得该锁。
  • 不可重入锁:例如ReadWriteLock中的读锁,同一个线程不能多次获得该锁。
  1. 按照锁的公平性分类:
  • 公平锁:按照线程请求锁的顺序来获取锁,避免线程饥饿现象。
  • 非公平锁:不考虑线程请求锁的顺序,有可能会导致某些线程一直无法获得锁。
  1. 按照锁的实现方式分类:
  • 内置锁:例如synchronized关键字,由Java虚拟机实现,通常与对象关联。
  • 显式锁:例如ReentrantLock等,需要程序员手动创建和管理,提供了更加灵活的控制方式。
  1. 按照锁的功能分类:
  • 独占锁:只允许一个线程获得锁,其他线程需要等待。
  • 共享锁:允许多个线程同时获得锁,例如ReadWriteLock中的读锁就是共享锁。

不同类型的锁各有优缺点,需要根据具体场景进行选择。例如,对于访问单个对象的情况,可以使用内置锁;对于需要细粒度控制的情况,可以使用ReentrantLock等显式锁;对于需要高并发读写的情况,可以使用ReadWriteLock等读写锁。

公平锁与非公平锁之间的区别

公平锁和非公平锁是Java中锁的两种不同实现方式,它们的主要区别在于线程获取锁的顺序不同:

  1. 公平锁:按照线程请求锁的顺序来获取锁,即等待时间最长的线程将最先获得锁。当锁释放后,等待时间最长的线程会优先获取锁,避免了某些线程长时间等待的问题,确保了线程的公平性。
  2. 非公平锁:不考虑线程请求锁的顺序,有可能会导致某些线程一直无法获得锁。当锁释放后,不一定是等待时间最长的线程获得锁,而是竞争最激烈的线程优先获得锁。这种方式可以提高锁的吞吐量,减少线程的上下文切换次数,但是可能会导致某些线程长时间等待。

公平锁和非公平锁的选择取决于具体的应用场景。对于对公平性要求比较高的场景,例如资源分配、任务调度等,应该优先选择公平锁,以避免某些线程一直无法获得锁,造成线程饥饿现象。而对于需要高并发的场景,例如缓存、连接池等,为了提高锁的吞吐量,可以使用非公平锁,以减少线程的上下文切换次数。

需要注意的是,公平锁在竞争激烈的情况下可能会导致线程间的上下文切换过多,降低系统性能。而非公平锁在高并发情况下可能会导致某些线程一直无法获得锁,造成线程饥饿现象。因此,在选择锁的类型时,需要根据具体情况进行综合考虑,权衡各种因素。

CAS(Compare and Swap)

CAS(Compare and Swap)是一种乐观锁技术,用于实现多线程之间的同步操作。它利用CPU底层提供的原子指令来实现无锁操作,从而避免了使用传统锁机制所带来的性能问题。

CAS操作通常包括三个参数:内存位置、期望的值和新的值。当执行CAS操作时,只有当内存位置的值与期望的值相同时,才会将新的值写入内存,并返回true,否则不会进行任何操作,并返回false。

CAS操作的基本流程如下:

  1. 将内存位置的值与期望的值进行比较。
  2. 如果相等,则将新的值写入内存位置。
  3. 如果不相等,则不做任何操作。

在多线程环境中,CAS操作可以用来解决并发访问共享数据时的同步问题。如果多个线程同时尝试更新同一个共享数据,则只有一个线程会成功执行CAS操作,并更新共享数据。其他线程由于CAS操作失败,需要重新获取共享数据并再次尝试执行CAS操作。

使用CAS操作的优点是可以避免传统锁机制所带来的性能问题,因为它不会导致线程的阻塞和切换。另外,CAS操作通常可以实现无锁或轻量级锁的效果,从而提高并发性能。

然而,CAS操作也存在一些缺点。首先,由于CAS操作是基于底层硬件提供的原子指令实现的,因此在高并发场景下,多个线程同时尝试执行CAS操作可能会导致竞争激烈,从而出现CAS自旋等待的情况,这会浪费一些CPU资源。其次,CAS操作只能保证单个变量的原子性,不能保证多个变量之间的原子性。

在Java中,CAS操作通常通过sun.misc.Unsafe类来实现。同时,Java也提供了一些基于CAS操作的原子类,如AtomicInteger、AtomicLong、AtomicBoolean等,这些类都提供了一些常用的原子操作方法,如getAndIncrement()、compareAndSet()等,方便我们进行并发编程。

AQS(AbstractQueuedSynchronizer)

AQS(AbstractQueuedSynchronizer)是Java中用于实现锁和同步器的基础框架,其核心思想是使用一个FIFO的双向链表来实现线程的阻塞和唤醒。AQS提供了一些基本的同步操作方法,如获取锁、释放锁、阻塞线程、唤醒线程等,同时也允许用户自定义同步器,并实现自己的同步操作方法。

AQS的设计非常巧妙,其基本思路是将线程的阻塞和唤醒操作交给同步器来管理,而同步器通过维护一个双向链表来管理等待线程,从而实现线程的阻塞和唤醒。具体来说,AQS通过内部类Node来表示等待线程,每个Node包含一个等待状态(waitStatus)和一个指向前一个和后一个Node的引用(prev和next),同时还有一个Thread类型的字段表示持有该Node的线程。当一个线程请求获取锁时,如果锁已经被占用,则该线程会被封装成一个Node并加入等待队列中,并在后续的自旋过程中不断尝试获取锁。当锁的持有者释放锁时,会唤醒等待队列中的一个线程,并将其从等待队列中移除。

AQS提供了两种同步模式:独占模式和共享模式。在独占模式下,同一时刻只能有一个线程持有锁,其他线程必须等待锁的释放。而在共享模式下,多个线程可以同时获取锁,并进行并发访问。

AQS的基本操作方法包括:

  1. tryAcquire(int arg):尝试获取锁,如果获取成功则返回true,否则返回false。
  2. tryRelease(int arg):释放锁,如果释放成功则返回true,否则返回false。
  3. tryAcquireShared(int arg):尝试获取共享锁,如果获取成功则返回一个大于等于0的整数,否则返回一个小于0的整数。
  4. tryReleaseShared(int arg):释放共享锁,如果释放成功则返回true,否则返回false。
  5. isHeldExclusively():判断当前线程是否持有独占锁。

AQS的实现非常复杂,涉及到许多底层细节,因此不适合直接使用。但是,Java提供了一些基于AQS的同步类,如ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等,这些类都使用了AQS提供的同步基础框架,可以直接使用,并提供了一些常用的同步操作方法,方便我们进行并发编程。

ReentrantLock

ReentrantLock是Java中的一种独占锁,它是一种可重入锁,也就是同一个线程可以多次获取锁而不会死锁,这种锁可以有效地防止资源的竞争和数据的不一致。

ReentrantLock实现了Lock接口,提供了一些与锁相关的操作方法,包括:

  1. lock():获取锁,如果锁已经被占用,则当前线程会阻塞,直到获取到锁。
  2. tryLock():尝试获取锁,如果锁未被占用,则立即获取锁并返回true,否则立即返回false。
  3. tryLock(long timeout, TimeUnit unit):尝试获取锁,如果在指定的时间内(以指定的时间单位为准)未能获取到锁,则返回false。
  4. unlock():释放锁,如果当前线程持有锁,则释放锁。

ReentrantLock的实现原理比较复杂,它底层基于AQS(AbstractQueuedSynchronizer)实现了锁的语义,具体来说,它使用一个state变量来表示锁的状态,当state为0时表示锁未被占用,当state为1时表示锁已被占用。当一个线程请求获取锁时,如果锁未被占用,则该线程可以立即获取锁,并将state设置为1;如果锁已经被占用,则该线程会被封装成一个Node并加入等待队列中,并在后续的自旋过程中不断尝试获取锁。当锁的持有者释放锁时,会唤醒等待队列中的一个线程,并将其从等待队列中移除。

ReentrantLock相较于synchronized关键字具有一些优点,包括:

  1. 可中断性:如果一个线程在等待获取锁的过程中,可以被中断,则该线程将会从等待队列中移除,并抛出InterruptedException异常。
  2. 公平性:ReentrantLock可以通过构造函数指定锁的公平性,如果指定为公平锁,则锁将按照FIFO的顺序进行获取;如果指定为非公平锁,则锁将根据CPU的调度而获取,可能会存在饥饿线程。
  3. 条件变量:ReentrantLock提供了Condition接口来实现条件变量,可以让线程在特定条件下等待和唤醒。

总的来说,ReentrantLock是一种高级的同步机制,可以有效地控制并发访问,并提供了比synchronized关键字更多的特性和灵活性。但是,由于它的实现较为复杂,使用时需要谨慎考虑,避免出现死锁等问题。

ReentrantLock是Java中一个可重入锁的实现,它允许线程获取已经持有的锁,同时还能保证锁的互斥性和可见性。下面是一个简单的示例,说明如何使用ReentrantLock。

import java.util.concurrent.locks.ReentrantLock;

public class Example {
    private ReentrantLock lock = new ReentrantLock();

    public void foo() {
        lock.lock();
        try {
            // 线程安全的代码
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock是Java中一个可重入锁的实现,它允许线程获取已经持有的锁,同时还能保证锁的互斥性和可见性。下面是一个简单的示例,说明如何使用ReentrantLock。

javaCopy codeimport java.util.concurrent.locks.ReentrantLock;

public class Example {
    private ReentrantLock lock = new ReentrantLock();

    public void foo() {
        lock.lock();
        try {
            // 线程安全的代码
        } finally {
            lock.unlock();
        }
    }
}

在这个示例中,我们首先创建了一个ReentrantLock对象,然后在foo方法中使用lock方法获取锁,执行线程安全的代码,最后使用unlock方法释放锁。请注意,我们使用了try-finally语句块来确保在任何情况下都会释放锁。

ReentrantLock还提供了其他方法,如tryLock、tryLock(long timeout, TimeUnit unit)等,它们允许我们在尝试获取锁时设置超时时间。此外,它还提供了一些高级功能,如公平锁、条件变量等,可以根据需要使用。

总的来说,ReentrantLock是Java中一个非常强大的锁实现,可以满足大部分线程同步的需求。

手写实现 ReentrantLock

ReentrantLock是Java中的可重入锁实现类,可以用于替代synchronized实现线程同步。以下是ReentrantLock的简单手写实现,仅供参考。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyReentrantLock implements Lock {

    private boolean isLocked = false;
    private Thread lockedBy = null;
    private int lockCount = 0;

    private final Object lock = new Object();

    @Override
    public void lock() {
        synchronized (lock) {
            Thread currentThread = Thread.currentThread();
            while (isLocked && lockedBy != currentThread) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            isLocked = true;
            lockedBy = currentThread;
            lockCount++;
        }
    }

    @Override
    public void unlock() {
        synchronized (lock) {
            if (Thread.currentThread() != lockedBy) {
                throw new IllegalMonitorStateException("Calling thread has not locked this lock");
            }
            lockCount--;
            if (lockCount == 0) {
                isLocked = false;
                lockedBy = null;
                lock.notifyAll();
            }
        }
    }

    //以下方法暂不实现

    @Override
    public void lockInterruptibly() throws InterruptedException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public boolean tryLock() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}

需要注意的是,上述代码是一个简单的可重入锁的实现,可能存在性能和安全方面的问题,仅适用于学习和理解ReentrantLock的原理。在实际应用中,应该使用Java中提供的ReentrantLock实现类。

Semaphore

Semaphore是一种计数信号量,它可以用来控制同时访问某个资源的线程数,或者限制流量的大小。

Semaphore主要有两个操作:acquire()和release()。acquire()操作会尝试获取一个许可证,如果当前有许可证可用,则获取成功并立即返回;如果没有许可证可用,则当前线程会被阻塞,直到有许可证可用。release()操作会释放一个许可证,并通知等待的线程有许可证可用。

Semaphore可以被用于解决多线程并发问题,例如限制并发访问某个资源的线程数。下面是一个简单的例子,假设有一个容器类Container,其中有一个put()方法用于往容器中添加元素,一个get()方法用于从容器中获取元素,但是容器中最多只能存放5个元素,当容器中元素个数超过5个时,新添加的元素需要等待容器中的元素被消费才能继续添加。

import java.util.concurrent.Semaphore;

public class Container {
    private final Semaphore mutex = new Semaphore(1);
    private final Semaphore full = new Semaphore(0);
    private final Semaphore empty = new Semaphore(5);
    private final Object[] elements = new Object[5];
    private int putIndex = 0, getIndex = 0, count = 0;

    public void put(Object element) throws InterruptedException {
        empty.acquire();
        mutex.acquire();
        try {
            elements[putIndex] = element;
            putIndex = (putIndex + 1) % 5;
            count++;
        } finally {
            mutex.release();
            full.release();
        }
    }

    public Object get() throws InterruptedException {
        full.acquire();
        mutex.acquire();
        try {
            Object element = elements[getIndex];
            elements[getIndex] = null;
            getIndex = (getIndex + 1) % 5;
            count--;
            return element;
        } finally {
            mutex.release();
            empty.release();
        }
    }
}

在上面的例子中,容器类中维护了3个Semaphore对象:mutex用于保证对容器的操作互斥进行,full用于表示容器中元素个数不为0,empty用于表示容器中还可以添加的元素个数

CountDownLatch

CountDownLatch是Java并发包中的一个类,它可以用于实现线程间的协作和同步。它可以让一个或多个线程等待其他线程完成某些操作,然后再继续执行。下面对CountDownLatch进行深入讲解。

构造方法

CountDownLatch有一个构造方法,需要传入一个int类型的参数count,表示需要等待的操作数量。当CountDownLatch的计数器变为0时,等待该CountDownLatch的线程才会被唤醒。示例代码如下:

CountDownLatch latch = new CountDownLatch(3);

上面的代码表示需要等待3个操作完成后,CountDownLatch的计数器才会变为0。

方法介绍

CountDownLatch提供了如下几个方法:

  • void countDown():将CountDownLatch的计数器减1。
  • void await():等待计数器变为0,如果当前计数器为0,则该方法立即返回。
  • boolean await(long timeout, TimeUnit unit):等待计数器变为0,如果超过指定时间,则该方法会返回false,否则返回true。
  • long getCount():获取当前计数器的值。

使用示例

下面是一个使用CountDownLatch的示例。该示例中有三个线程,它们都需要执行某些操作后才能继续执行。我们可以使用CountDownLatch来实现这个需求。

import java.util.concurrent.CountDownLatch;

public class Example {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 is working...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1 is done.");
            latch.countDown();
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 is working...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 is done.");
            latch.countDown();
        });

        Thread thread3 = new Thread(() -> {
            System.out.println("Thread 3 is working...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 3 is done.");
            latch.countDown();
        });

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

        latch.await();
        System.out.println("All threads are done.");
    }
}

在上面的示例中,我们创建了一个CountDownLatch对象,计数器的值为3。然后创建了三个线程,每个线程都需要执行一些操作后才能调用CountDownLatch的countDown方法,将计数器减1。最后,主线程调用CountDownLatch的await方法等待计数器变为0,然后输出"All threads are done."。

运行上面的示例,可以看到如下输出:

Thread 1 is working...
Thread 3 is working...
Thread 2 is working...
Thread 1 is done.
Thread 2 is done.
Thread 3 is done.
All threads

CyclicBarrier

CyclicBarrier是Java并发包中的一个类,用于实现多个线程之间的同步。CyclicBarrier可以让多个线程在一个集合点处进行等待,当所有线程都到达集合点后,再一起继续执行。下面对CyclicBarrier进行深入讲解。

构造方法

CyclicBarrier有两个构造方法,分别是:

public CyclicBarrier(int parties, Runnable barrierAction);
public CyclicBarrier(int parties);

第一个构造方法需要传入一个int类型的参数parties,表示需要等待的线程数量,以及一个Runnable类型的barrierAction,表示所有线程到达集合点后需要执行的操作。第二个构造方法只需要传入一个int类型的参数parties,表示需要等待的线程数量。

方法介绍

CyclicBarrier提供了如下几个方法:

  • int await():当前线程到达集合点,等待其他线程到达,当所有线程都到达集合点时,返回一个唯一的整数,表示当前线程是第几个到达集合点的线程。
  • int await(long timeout, TimeUnit unit):等待指定时间后,如果当前线程还没有到达集合点,则会抛出TimeoutException异常。
  • int getParties():获取需要等待的线程数量。
  • int getNumberWaiting():获取已经到达集合点但还在等待的线程数量。
  • boolean isBroken():获取所有线程是否都已经到达集合点,如果有任意一个线程抛出异常,则返回true。

使用示例

下面是一个使用CyclicBarrier的示例。该示例中有三个线程,每个线程都需要执行一些操作后才能调用CyclicBarrier的await方法,等待其他线程到达集合点。当所有线程都到达集合点后,输出"All threads are done."。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Example {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("All threads are arrived.");
        });

        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 is working...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1 is done.");
            try {
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 is working...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 is done.");
            try {
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        Thread thread3 = new Thread(() -> {
            System.out.println("Thread 3 is working...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 3 is done.");
            try {
                barrier.await();

            } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
            }
          });
          
            thread1.start();
            thread2.start();
            thread3.start();
        }
     }

运行上述示例,输出结果如下:

Thread 1 is working...
Thread 2 is working...
Thread 3 is working...
Thread 1 is done.
Thread 2 is done.
Thread 3 is done.
All threads are arrived.

可以看到,三个线程都到达了集合点,才会执行CyclicBarrier的barrierAction,并输出"All threads are arrived."。

另外,需要注意的是,CyclicBarrier可以被重用,即在所有线程到达集合点后,可以继续使用CyclicBarrier进行同步等待。但是,需要保证所有线程已经执行完当前任务,否则可能会出现死锁情况。

举报
评论 0