Java 锁和原子变量教程

x33g5p2x  于2021-10-16 转载在 Java  
字(4.5k)|赞(0)|评价(0)|浏览(224)

在多线程程序中,必须同步访问共享变量以防止竞争条件。

在之前的教程中,我们学习了如何使用 synchronized 方法和 synchronized 块来保护对共享变量的并发访问并避免竞争条件。

Java 的 synchronized 关键字在内部使用与对象关联的内在锁来获得对对象成员字段的独占访问。

除了通过 synchronized 关键字使用内在锁之外,您还可以使用 Java 的并发 API 提供的各种锁定类来对锁定机制进行更细粒度的控制。

在本教程中,我们将学习如何使用 Java 提供的这些 Locking 类来同步对共享变量的访问。

最后,我们还将通过 Java 并发 API 提供的各种 Atomic 类来查看线程同步的现代方式。

1. ReentrantLock

ReentrantLock 是一种互斥锁,其行为与通过 synchronized 关键字访问的内在/隐式锁相同。

ReentrantLock,顾名思义,具有可重入的特性。这意味着当前拥有锁的线程可以多次获取它而不会出现任何问题。

以下是一个示例,展示了如何使用 ReentrantLock- 创建线程安全的方法

import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockCounter {
    private final ReentrantLock lock = new ReentrantLock();

    private int count = 0;

    // Thread Safe Increment
    public void increment() {
        lock.lock();
        try {
            count = count + 1;
        } finally {
            lock.unlock();
        }
    }
}

这个想法很简单——任何调用 increment() 方法的线程都会首先获取锁,然后增加 count 变量。当变量递增完成后,它可以释放锁,以便其他等待锁的线程可以获取它。

另外,请注意,我在上面的示例中使用了 try/finally 块。 finally 块确保即使发生某些异常也会释放锁。

ReentrantLock 还提供了各种方法来进行更细粒度的控制——

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockMethodsCounter {
    private final ReentrantLock lock = new ReentrantLock();

    private int count = 0;

    public int incrementAndGet() {
        // Check if the lock is currently acquired by any thread
        System.out.println("IsLocked : " + lock.isLocked());

        // Check if the lock is acquired by the current thread itself.
        System.out.println("IsHeldByCurrentThread : " + lock.isHeldByCurrentThread());

        // Try to acquire the lock
        boolean isAcquired = lock.tryLock();
        System.out.println("Lock Acquired : " + isAcquired + "\n");

        if(isAcquired) {
            try {
                Thread.sleep(2000);
                count = count + 1;
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            } finally {
                lock.unlock();
            }
        }
        return count;
    }
}

public class ReentrantLockMethodsExample {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        ReentrantLockMethodsCounter lockMethodsCounter = new ReentrantLockMethodsCounter();

        executorService.submit(() -> {
           System.out.println("IncrementCount (First Thread) : " +
                   lockMethodsCounter.incrementAndGet() + "\n");
        });

        executorService.submit(() -> {
            System.out.println("IncrementCount (Second Thread) : " +
                    lockMethodsCounter.incrementAndGet() + "\n");
        });

        executorService.shutdown();
    }
}
# Output
IsLocked : false
IsHeldByCurrentThread : false
Lock Acquired : true

IsLocked : true
IsHeldByCurrentThread : false
Lock Acquired : false

IncrementCount (Second Thread) : 0

IncrementCount (First Thread) : 1

tryLock() 方法尝试在不暂停线程的情况下获取锁。也就是说,如果该线程无法获取锁,因为它被其他线程持有,那么它会立即返回而不是等待锁被释放。

您还可以在 tryLock() 方法中指定超时以等待锁可用 -

lock.tryLock(1, TimeUnit.SECONDS);

该线程现在将暂停一秒钟并等待锁可用。如果无法在 1 秒内获得锁,则线程返回。

2. 读写锁

ReadWriteLock 由一对锁组成——一个用于读访问,一个用于写访问。只要写锁不被任何线程持有,读锁就可以被多个线程同时持有。

ReadWriteLock 允许提高并发级别。与写入少于读取的应用程序中的其他锁相比,它的性能更好。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteCounter {
    ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count = 0;

    public int incrementAndGetCount() {
        lock.writeLock().lock();
        try {
            count = count + 1;
            return count;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public int getCount() {
        lock.readLock().lock();
        try {
            return count;
        } finally {
            lock.readLock().unlock();
        }
    }
}

在上面的例子中,只要没有线程调用incrementAndGetCount(),多个线程就可以执行getCount()方法。如果任何线程调用 incrementAndGetCount() 方法并获得写锁,则所有读取线程将暂停其执行并等待写入线程返回。

原子变量

Java 的并发 api 在 java.util.concurrent.atomic 包中定义了几个支持对单个变量进行原子操作的类。

原子类在内部使用现代 CPU 支持的 compare-and-swap 指令来实现同步。这些指令通常比锁快得多。

考虑以下示例,其中我们使用 AtomicInteger 类来确保计数变量的增量以原子方式发生。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public int incrementAndGet() {
        return count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

public class AtomicIntegerExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        AtomicCounter atomicCounter = new AtomicCounter();

        for(int i = 0; i < 1000; i++) {
            executorService.submit(() -> atomicCounter.incrementAndGet());
        }

        executorService.shutdown();
        executorService.awaitTermination(60, TimeUnit.SECONDS);

        System.out.println("Final Count is : " + atomicCounter.getCount());
    }
}
# Output
Final Count is : 1000

AtomicInteger.incrementAndGet() 方法是原子的,因此您可以安全地同时从多个线程调用它,并确保对 count 变量的访问是同步的。

以下是在 java.util.concurrent.atomic 包中定义的其他一些原子类。 ——

AtomicBoolean
*
AtomicLong
*
AtomicReference

您应该尽可能使用这些原子类而不是同步关键字和锁,因为它们更快、更易于使用、可读和可扩展。

结论

恭喜您完成了我的 Java 并发教程系列的最后一部分。在本教程中,我们学习了如何使用锁和原子变量进行线程同步。您可以在 my github repository 中找到本教程中使用的所有代码示例。

感谢您的阅读。请在下面的评论部分提出任何问题。

相关文章

微信公众号

最新文章

更多