Java并发编程实战读书笔记(四)

显示锁

Lock 与 ReentrantLock

Lock 接口定义了一组抽象的加锁操作,与内置加锁机制不同,Lock 提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在 Lock 的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面可以有所不同。

ReentrantLock 实现了 Lock 接口,并提供了与 synchronized 相同的互斥性和内存可见性。在获取 ReentrantLock 时,有着与进入同步代码块相同的内存语义,在释放 ReentrantLock 时,同样有着与退出同步代码块相同的内存语义。此外,与 synchronized 一样,ReentrantLock 还提供了可重入的加锁语义。

ReentrantLock 支持在 Lock 接口中定义的所有获取锁模式,并且与 synchronized 相比,它还为处理锁的不可用性问题提供了更高的灵活性。

ReentrantLock是Java并发编程中的一个类,它实现了Lock接口,提供了与synchronized关键字类似的同步功能,但具有更高的灵活性和扩展性。ReentrantLock允许线程尝试获取锁,而不是像synchronized那样阻塞等待。此外,ReentrantLock还支持公平锁和非公平锁,以及可重入锁的功能。

使用示例:

import java.util.concurrent.locks.ReentrantLock;

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

    public void doSomething() {
        lock.lock(); // 获取锁
        try {
            // 临界区代码
            System.out.println("执行临界区代码");
        } finally {
            lock.unlock(); // 释放锁
        }
    }

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

在这个示例中,我们创建了一个ReentrantLock实例,并在doSomething方法中使用lock()方法获取锁。在try块中,我们执行临界区的代码,然后在finally块中使用unlock()方法释放锁。这样可以确保即使在临界区代码抛出异常时,锁也能被正确释放。

原子变量与非阻塞同步机制

比较并交换

在大多数处理器架构,如IA32和Sparc中,采用了比较并交换(CAS)指令来实现原子操作。在其他处理器如PowerPC中,则使用一对指令——关联加载和条件存储来实现相同的功能。CAS操作包括三个操作数:内存位置V、用于比较的值A和新值B。仅当V的值等于A时,CAS会将V更新为新值B,否则不执行操作。无论操作是否成功,CAS都会返回V的原始值。这种操作被称为“比较并设置”,因为它总是返回结果。CAS是基于乐观锁原理,假设更新操作会成功,并且能在其他线程更新变量后检测到错误。

原子变量类

原子变量类是一种泛化的volatile变量,支持原子的和有条件的读-改-写操作。AtomicInteger是一个例子,它表示一个int类型的值,并提供get和set方法,这些方法在读取和写入时具有volatile变量的内存语义。此外,它还提供了原子的compareAndSet方法(如果成功执行,将具有与读取/写入volatile变量相同的内存效果),以及原子的添加、递增和递减等方法。

共有12个原子变量类,分为四组:标量类、更新器类、数组类和复合变量类。最常用的原子变量包括标量类:AtomicInteger、AtomicLong、AtomicBoolean和AtomicReference。所有这些类都支持CAS,AtomicInteger和AtomicLong还支持算术运算。

原子数组类(只支持Integer、Long和Reference版本)允许数组元素原子更新。原子数组类为数组元素提供volatile类型的访问语义,这是普通数组所不具备的。

尽管原子标量类扩展了Number类,但并没有扩展基本类型的包装类,如Integer或Long。实际上,它们不能扩展这些类,因为基本类型的包装类是不可修改的,而原子变量类是可修改的。原子变量类也没有重新定义hashCode或equals方法,因此每个实例都是不同的。与其他可变对象一样,它们也不宜用作基于散列的容器中的键值。

Java内存模型

重排序

重排序是指编译器、处理器和内存系统为了提高性能而对代码中的操作顺序进行重新排序的行为。在多线程程序中,这种重排序可能会导致竞态条件和原子性故障,从而使得程序的执行结果与预期不符。

在Java中,JMM(Java Memory Model)允许不同线程看到的操作执行顺序是不同的,这增加了在缺乏同步的情况下推断操作执行顺序的复杂性。重排序的各种原因,如编译器优化、缓存一致性协议等,都可能导致操作延迟或看似乱序执行。

内存模型简介

Java内存模型(JMM)是通过各种操作来定义的,包括对变量的读/写操作、监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中的所有操作定义了一个偏序关系,称为Happens-Before。要确保执行操作B的线程看到操作A的结果(无论A和B是否在同一个线程中执行),在A和B之间必须满足Happens-Before关系。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地重排序。

当一个变量被多个线程读取并且至少被一个线程写入时,如果在读操作和写操作之间没有依照Happens-Before来排序,那么就会产生数据竞争问题。在正确同步的程序中不存在数据竞争,并会表现出串行一致性,这意味着程序中的所有操作都会按照一种固定的和全局的顺序执行。

Happens-Before的规则包括:

  1. 程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
  2. 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  3. volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。
  4. 线程启动规则:在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行。
  5. 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
  6. 中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
  7. 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  8. 传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

虽然这些操作只满足偏序关系,但同步操作,如锁的获取与释放等操作,以及volatile变量的读取与写入操作,都满足全序关系。因此,在描述Happens-Before关系时,就可以使用“后续的锁获取操作”和“后续的volatile变量读取操作”等表达术语。

  • 21
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴代庄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值