乐趣区

关于后端:一文看懂Java中的锁

浏览本文你能够取得 Synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock、Condition、Semaphore、CountDownLatch、CyclicBarrier、JMM、Volatile、Happens-Before。

全文共 16000 字左右(蕴含示例代码)、欢送珍藏、在看、转发分批食用

一、了解同步与并发

在学习锁之前,咱们先理解一下同步与并发的概念,

在 Java 中,同步和并发是两个重要的概念。了解这两个概念对于学习锁机制十分重要。

同步是指多个线程在访问共享资源时,对资源进行同步化的操作,以确保它们不会发生冲突。当多个线程同时拜访同一个共享资源时,如果没有进行同步解决,就会产生竞争条件的问题,导致程序呈现不可预测的行为。为了解决这个问题,Java 提供了多种同步机制,如 synchronized 关键字、ReentrantLock 等。

并发是指在同一时间内同时执行多个线程的能力。Java 中的线程是轻量级的过程,它们能够并发地执行,从而进步了程序的性能和响应速度。Java 中的线程模型是基于共享内存的并发模型,多个线程共享同一块内存空间,它们能够通过读写共享变量来实现线程间的通信和交互。

多个线程同时访问共享资源时,就会产生并发问题,如线程平安、死锁等。为了解决这些问题,须要应用 Java 中提供的同步机制来保障线程的安全性和正确性。

因而,在学习锁机制之前,须要了解 Java 中的同步和并发的概念,以及多个线程同时访问共享资源时可能呈现的问题。这样能力更好地了解锁机制的作用和原理。

二、锁的基本原理

避免竞争条件,保障线程平安,可见性,防止死锁

锁的基本原理是避免竞争条件,保障线程安全性和可见性,防止死锁等问题。上面是对于锁的基本原理的具体介绍:

  1. 避免竞争条件

当多个线程同时访问共享资源时,可能会产生竞争条件。竞争条件是指当多个线程同时执行同一段代码时,因为执行程序的不同而导致后果的不确定性。

锁的作用就是在多个线程访问共享资源时保障同一时刻只有一个线程拜访,从而防止竞争条件的产生。当一个线程获取到锁时,其余线程必须期待锁的开释能力持续访问共享资源。

  1. 保障线程安全性和可见性

线程安全性和可见性是 Java 并发编程中十分重要的概念。线程安全性是指当多个线程同时访问共享资源时,不会呈现数据损坏或程序解体等问题。可见性是指当一个线程批改了共享资源时,其余线程可能立刻看到这个批改。

锁机制能够保障线程安全性和可见性。当一个线程获取到锁时,其余线程无奈批改共享资源,从而防止了数据损坏和程序解体等问题。而锁机制也能够保障共享资源的可见性,因为当一个线程开释锁时,其余线程可能立刻看到共享资源的最新状态。

  1. 防止死锁

死锁是指两个或多个线程互相期待对方开释锁,从而导致程序无奈继续执行的状况。死锁是 Java 并发编程中一个十分重大的问题,必须防止产生。

为了防止死锁,必须采取一些策略,例如防止嵌套锁、防止长时间占用锁、依照雷同的程序获取锁等。另外,还能够应用专门的工具来检测和防止死锁,例如死锁检测器和防止死锁算法等。

总之,锁机制是 Java 并发编程中十分重要的一部分,理解锁的基本原理,包含如何避免竞争条件、如何保障线程安全性和可见性,以及如何防止死锁等问题,对于编写高效、牢靠的并发程序十分有帮忙。

三、Java 中的锁类型

synchronized,reentrantlock,reentrantReadWriteLock,stampedLock,ReadWriteLock

无锁、偏差所、轻量级锁、重量级锁

Semaphore、CountDownLatch、CyclicBarrier

等等。。。

Java 中提供了多种锁机制,每种锁机制都有其特点和实用场景。上面是这些锁机制的简要介绍:

3.1、synchronized 关键字锁

synchronized 关键字是 Java 中最根本和最罕用的锁机制。应用 synchronized 关键字能够将一段代码块或一个办法标记为同步代码块,以保障在任何时刻最多只能有一个线程执行它们。synchronized 锁是 Java 内置锁的一种实现。

  1. 同步办法:能够应用 synchronized 关键字润饰办法,将整个办法申明为同步办法。同步办法会对整个办法体进行加锁,确保同一时间只有一个线程能够执行该办法。
  2. 同步代码块:能够应用 synchronized 关键字润饰代码块,将特定的代码块申明为同步代码块。同步代码块应用指定的对象作为锁,在执行代码块时获取锁,确保同一时间只有一个线程能够执行该代码块。
  3. 锁对象:synchronized 关键字须要一个锁对象来实现同步。对于同步办法,锁对象是该办法所属对象(this);对于同步代码块,能够通过指定一个对象来作为锁。多个线程在获取同一个锁对象时才会互斥执行。
  4. 内置锁(Intrinsic Lock):synchronized 关键字应用的是 Java 中的内置锁,也称为监视器锁。每个对象都有一个内置锁,当线程获取到内置锁时,其余线程须要期待该锁开释能力拜访同步代码。
  5. 互斥性:synchronized 关键字保障同一时间只有一个线程能够获取到锁,从而实现互斥访问共享资源,防止数据竞争和不一致性。
  6. 可重入性:同一个线程能够屡次获取同一个锁而不会产生死锁,这种个性称为可重入性。即在同一个线程中,能够嵌套调用同步办法或同步代码块,而不会因为反复获取锁而造成阻塞。
  7. 非公平性:synchronized 关键字默认采纳非偏心锁的形式,即多个线程竞争锁时,没有特定的程序。但能够通过应用 ReentrantLock 类来实现偏心锁。

3.1.1、底层原理

在 Java 中,synchronized 关键字的底层原理波及到对象头(Object Header)和监视器(Monitor)的概念。

每个 Java 对象都有一个对象头,对象头蕴含一些用于对象治理的元数据信息,其中之一是用于实现同步的锁信息。当一个对象被 synchronized 关键字润饰时,对象头中的锁信息被用于实现同步操作。

当线程进入一个 synchronized 办法或代码块时,它首先会尝试获取对象的锁。如果锁是可用的,即没有其余线程持有该锁,那么以后线程会胜利获取锁并继续执行同步代码。在获取锁时,Java 会将锁的计数器加 1,示意以后线程取得了锁。

如果锁曾经被其余线程持有,那么以后线程就会进入阻塞状态,直到获取到锁为止。在期待期间,该线程会被搁置在对象的期待集(Wait Set)中,期待其余线程开释锁并告诉它能够继续执行。这种期待和唤醒的机制由监视器来治理。

监视器是一种同步机制,用于治理对象的锁和线程的期待集。每个 Java 对象都与一个监视器相关联,监视器蕴含了与该对象关联的锁和期待集。只有持有锁的线程能力执行同步代码,其余线程必须期待锁的开释。

当线程执行完 synchronized 办法或代码块中的代码后,会开释锁,并将锁的计数器减 1。如果计数器变为 0,示意以后线程齐全开释了锁,其余线程能够竞争获取该锁。

总结来说,synchronized 关键字的底层原理是通过对象的锁信息和监视器来实现线程的互斥拜访和同步操作。它利用了对象头中的锁信息以及监视器的期待集 (Wait Set) 来治理线程的锁获取和开释。这种机制确保了同一时间只有一个线程可能执行 synchronized 代码块或办法,从而保障了线程安全性。

3.1.1.1、对象头

在 JDK 8 中,Java 虚拟机(JVM)的对象头信息包含以下几个局部的组成:

  1. Mark Word(标记字段):Mark Word 是对象头信息中最重要的局部,占用 64 位(在 64 位 JVM 中)。它蕴含了一些标记位和存储对象相干的信息。其中可能蕴含锁状态、GC 相干标记、哈希码等信息。
  2. Class Metadata Address(类元数据指针):对象头还蕴含一个指向对象所属类的元数据的指针。这个指针指向办法区中的类元数据,用于确定对象的类型信息,包含类的办法、字段等。
  3. Array Length(数组长度):如果对象是一个数组对象,对象头中会蕴含数组长度的信息。这样能够在运行时疾速获取数组的长度。
  4. Biased Locking Information(偏差锁信息):JDK 6 及之后的版本引入了偏差锁优化技术,用于进步无竞争状况下的同步性能。对象头中可能蕴含偏差锁的相干信息,用于标记对象是否处于偏差锁状态。

须要留神的是,对象头的具体组成可能因不同的 JVM 实现、JVM 版本或对象的状态而有所不同。上述组成是通常状况下的个别概念,理论的实现细节可能会有所变动。

在 JDK 8 中,Mark Word(标记字段)中的标记锁状态信息应用不同的位来进行标记。具体的标记位包含以下几个:

  1. 锁状态(Lock State):标记字段中的几个位用于示意对象的锁状态。其中,最低两位用于示意锁状态,能够有以下几种状态:

    • 00:无锁状态(Unlocked):对象未被任何线程锁定。
    • 01:偏差锁状态(Biased Lock):对象被某个线程偏差锁定。
    • 10:轻量级锁状态(Lightweight Lock):对象被某个线程轻量级锁定。
    • 11:重量级锁状态(Heavyweight Lock):对象被某个线程重量级锁定。
  2. 偏差线程 ID(Thread ID):如果对象处于偏差锁状态(01),那么标记字段的一部分用于存储持有偏差锁的线程的 ID。这样能够疾速判断以后线程是否能够获取偏差锁。
  3. 偏差工夫戳(Epoch):在 JDK 8 中,偏差锁引入了一个偏差工夫戳,用于标记偏差锁的持续时间。当偏差锁的工夫戳过期时,将会撤销偏差锁。

须要留神的是,具体的标记位布局和应用可能会因 JVM 实现和配置参数而有所不同。上述标记形式是一种常见的实现形式,但并不是相对的标准。JVM 的具体实现能够依据需要进行优化和调整。

标记字段的应用是为了反对 JVM 的锁优化技术,如偏差锁、轻量级锁和重量级锁,以进步同步操作的性能和效率。

在 Java 中,锁状态的转换是由 JVM 主动解决的,依据线程的竞争状况和同步操作的后果来进行状态转换。以下是几种常见的锁状态转换形式:

  1. 无锁状态到偏差锁状态:当一个线程第一次拜访一个对象时,对象处于无锁状态。如果 JVM 启用了偏差锁优化并且该线程获取到了锁,那么对象的状态会转换为偏差锁状态,同时标记字段中的偏差线程 ID 会记录以后线程的 ID。
  2. 偏差锁状态到轻量级锁状态:如果一个线程在偏差锁状态下拜访一个对象时,另一个线程也想要获取该对象的锁,那么偏差锁会被撤销,对象的状态会转换为轻量级锁状态。在这种状况下,JVM 会尝试应用 CAS(比拟并替换)操作来尝试获取锁,如果胜利则对象的状态转换为轻量级锁状态。
  3. 轻量级锁状态到重量级锁状态:如果轻量级锁获取锁的过程中产生竞争(即另一个线程也想要获取锁),那么轻量级锁会降级为重量级锁。降级为重量级锁的过程包含锁收缩(Lock Inflation)和互斥同步(Mutex Synchronization),这样所有竞争的线程都须要通过互斥同步来获取锁。
  4. 重量级锁状态到无锁状态:当持有重量级锁的线程开释锁时,对象的状态会转换回无锁状态,期待其余线程再次竞争获取锁。

须要留神的是,锁状态的转换是由 JVM 外部主动治理的,程序员无需显式干涉。JVM 会依据竞争状况、同步操作的后果以及具体的锁优化策略来动静决定锁状态的转换。锁状态的转换是为了进步同步操作的性能和效率,缩小竞争带来的性能损失。

3.1.1.2、监视器

监视器(Monitor)是 Java 中用于实现同步的根本机制,用于爱护对象的互斥拜访。每个 Java 对象都与一个监视器相关联,包含以下几个次要组成部分:

  1. 互斥锁(Mutex Lock):互斥锁是监视器的外围局部,用于实现对象的互斥拜访。互斥锁是一个二进制状态变量,用于示意对象是否被锁定。它确保在任意时刻只有一个线程能够持有该对象的锁,其余线程必须期待锁的开释能力进入临界区。
  2. 期待汇合(Wait Set):期待汇合是一个线程的汇合,用于寄存因为期待对象锁而进入期待状态的线程。当线程调用对象的 wait()办法时,它会被放入期待汇合中,开释对象的锁,并进入期待状态。只有当其余线程调用 notify()或 notifyAll()办法时,期待汇合中的线程才会被唤醒。
  3. 条件队列(Condition Queue):条件队列是一种用于线程通信的机制。每个监视器都能够关联一个或多个条件队列。线程能够调用条件队列的 await()办法进入期待状态,并在满足特定条件时被唤醒。当线程被唤醒时,它会从新尝试获取对象的锁。
  4. 计数器(Counters):监视器中通常会蕴含一些计数器,用于记录期待线程的数量、锁重入的次数等信息。

这些组成部分独特形成了监视器的根本构造,实现了线程的同步和互斥拜访。它们容许线程在临界区内操作共享资源时依照肯定的程序拜访,并确保线程间的互斥性和协调性。监视器的实现和具体细节由 JVM 负责,开发人员能够应用 synchronized 关键字或显式地调用监视器相干的办法来实现对象的同步。

3.2、ReentrantLock 锁

ReentrantLock 是一个可重入的互斥锁,提供了比 synchronized 更多的高级个性,如偏心锁和非偏心锁、可中断锁、条件变量等。ReentrantLock 锁是显式锁的一种实现。

ReentrantLock 是 Java 中提供的一种可重入锁的实现。它是一个基于显示锁的类,提供了比 synchronized 关键字更多的灵活性和性能。ReentrantLock 类提供了 lock() 和 unlock() 办法来获取和开释锁,以及其余一些用于治理锁状态的办法。

可重入锁是指一个线程在获取了锁之后,能够再次获取该锁而不会造成死锁。也就是说,线程能够屡次进入同一个锁,而不会被本人所持有的锁所阻塞。

偏心锁和非偏心锁是指在多个线程期待锁时,锁的获取程序是否合乎线程的申请程序。ReentrantLock 提供了构造函数来指定锁的公平性,默认状况下是非偏心锁。偏心锁会依照线程申请的程序来获取锁,而非偏心锁则容许线程插队获取锁。

应用 ReentrantLock 类,能够通过调用 lock() 办法来获取锁,并通过调用 unlock() 办法来开释锁。在取得锁之前,线程会被阻塞,直到锁可用。当线程开释锁后,其余线程才有机会获取该锁。

Condition 接口提供了对锁的条件期待和唤醒机制的反对。ReentrantLock 类中的 newCondition() 办法能够创立一个与该锁关联的 Condition 实例。通过 Condition,线程能够在某个条件不满足时期待,并在条件满足时被唤醒。

ReentrantLock 应用一个重入计数器来跟踪线程对锁的重入次数。当线程重入锁时,计数器会递增。只有当线程齐全开释锁时,计数器才会递加。这样能够确保同一个线程屡次获取锁而不会引发死锁。

3.2.1、原理

ReentrantLock(可重入锁)的原理是基于独占锁(Exclusive Locking)的概念。它应用一种称为 AQS(AbstractQueuedSynchronizer,形象队列同步器)的同步框架来实现。

AQS 是一个用于构建锁和其余同步器的框架,它提供了一个期待队列来治理期待获取锁的线程,并通过内置的 CAS(Compare and Swap)操作来实现对锁的获取和开释。

ReentrantLock 外部保护了一个状态变量,示意锁的状态,能够是被某个线程持有或者闲暇状态。当一个线程申请获取锁时,如果锁处于闲暇状态,该线程就能够间接获取到锁,而后将锁的状态设置为被该线程持有。如果锁处于被其余线程持有的状态,申请线程会被放入期待队列中,期待锁的开释。

在 ReentrantLock 中,当一个线程反复获取同一个锁时(即重入锁),锁的状态会递增,而不会造成死锁。每次开释锁时,锁的状态递加,直到锁的状态为 0,示意锁齐全开释,其余期待线程能够获取锁。

ReentrantLock 还反对公平性和非公平性。在偏心锁模式下,等待时间较长的线程会有更高的获取锁的优先级,而在非偏心锁模式下,线程能够通过抢占形式获取锁,不思考等待时间。

总结来说,ReentrantLock 的原理是通过 AQS 框架实现对锁的获取和开释,应用状态变量来示意锁的状态,反对重入个性,同时提供了公平性和非公平性的抉择。这使得 ReentrantLock 在多线程环境下提供了牢靠的同步机制。

3.2.2、AQS

AbstractQueuedSynchronizer(AQS)是 Java 中用于构建锁和同步器的形象框架。它提供了一个基于期待队列的机制,用于治理多线程对共享资源的拜访。

AQS 提供了一种基于独占模式和共享模式的同步器形象。独占模式意味着只有一个线程能够持有锁,而共享模式容许多个线程同时拜访资源。

AQS 的核心思想是应用一个状态变量来示意同步器的状态,并应用 CAS(Compare and Swap)操作来进行状态的更新和线程的排队。AQS 外部保护了一个期待队列,用于存储期待获取同步器的线程。线程在获取同步器时,如果发现同步器已被占用,则会被放入期待队列中,进入期待状态。

具体来说,当一个线程尝试获取同步器时,AQS 会应用 CAS 操作来竞争获取同步器的状态。如果获取胜利,线程就能够继续执行;如果获取失败,AQS 会将线程退出期待队列,并使线程进入期待状态。当同步器被开释时,AQS 会从期待队列中抉择一个线程来获取同步器,并将其从期待状态转换为运行状态。

AQS 还提供了一些办法供子类实现,例如 tryAcquire()、tryRelease() 等,用于管制同步器的获取和开释。子类能够依据具体的需要实现这些办法来实现自定义的同步逻辑。

AQS 是 Java 并发包中很多同步器的根底,包含 ReentrantLock、CountDownLatch、Semaphore 等。通过应用 AQS,开发人员能够更轻松地构建自定义的同步器,以实现线程间的协调和资源的平安拜访。

3.2.3、示例

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class MySync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1L;
    
    @Override
    protected boolean tryAcquire(int arg) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int arg) {if (getState() == 0)
            throw new IllegalMonitorStateException();
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock() {acquire(1);
    }

    public void unlock() {release(1);
    }

    public boolean isLocked() {return isHeldExclusively();
    }
}

public class CustomSyncExample {private static MySync sync = new MySync();

    public static void main(String[] args) {Runnable task = () -> {System.out.println("Thread" + Thread.currentThread().getId() + "is trying to acquire the lock.");
            sync.lock();
            System.out.println("Thread" + Thread.currentThread().getId() + "has acquired the lock.");
            try {Thread.sleep(2000);
            } catch (InterruptedException e) {e.printStackTrace();
            } finally {sync.unlock();
                System.out.println("Thread" + Thread.currentThread().getId() + "has released the lock.");
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();}
}

在下面的代码中,咱们创立了一个名为 MySync 的自定义同步器,它继承了 AbstractQueuedSynchronizer。在 MySync 中,咱们实现了 tryAcquire()tryRelease() 办法来管制同步器的获取和开释。lock() 办法和 unlock() 办法调用了 acquire()release() 办法,用于获取和开释锁。

CustomSyncExample 类中,咱们创立了两个线程,每个线程都会尝试获取同步器的锁。当一个线程胜利获取锁后,它会进入临界区并执行一些操作,而后开释锁。另一个线程在第一个线程开释锁之前,会被阻塞期待。

输入后果相似于以下内容:

Thread 11 is trying to acquire the lock.
Thread 12 is trying to acquire the lock.
Thread 11 has acquired the lock.
Thread 11 has released the lock.
Thread 12 has acquired the lock.
Thread 12 has released the lock.

3.3、ReentrantReadWriteLock 锁

ReentrantReadWriteLock 是 Java 中的读写锁机制,它容许多个线程同时读取共享资源,但只容许一个线程写入共享资源。Java 中提供的 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。

相比于独占锁(比方 ReentrantLock),读写锁在并发读取场景下能够提供更高的吞吐量。这是因为多个线程能够同时读取共享资源,而互不烦扰,从而进步了并发性能。然而,在写入场景下,依然只容许一个线程写入共享资源,以保证数据的一致性。

ReentrantReadWriteLock 外部蕴含两个锁:读锁和写锁。多个线程能够同时获取读锁,但只有一个线程能够获取写锁。当写锁被持有时,其余线程无奈获取读锁或写锁。当读锁被持有时,其余线程依然能够获取读锁,但不能获取写锁。

读写锁的重入性是指一个线程能够屡次获取读锁或写锁,而不会造成死锁。也就是说,线程能够在持有读锁的状况下再次获取读锁,或者在持有写锁的状况下再次获取写锁。

ReentrantReadWriteLock 提供了以下几个重要的办法:

  • readLock():返回一个读锁对象,用于获取读锁。
  • writeLock():返回一个写锁对象,用于获取写锁。
  • readLock().lock():获取读锁。
  • writeLock().lock():获取写锁。
  • readLock().unlock():开释读锁。
  • writeLock().unlock():开释写锁。

读锁和写锁的获取和开释形式与一般锁(如 ReentrantLock)类似。通过应用读写锁,咱们能够依据具体需要来管制对共享资源的读取和写入操作,进步并发性能和数据一致性

3.4、StampedLock 锁

StampedLock 是 Java8 中引入的一种乐观锁机制,与 ReadWriteLock 相似,它也容许多个线程同时访问共享资源,但与 ReadWriteLock 不同的是,它应用一个 stamp 值来标记以后锁状态,而不是应用读锁和写锁的状态。

它提供了三种模式的拜访:读模式、写模式和乐观读模式。

与传统的读写锁不同,StampedLock 并没有严格的互斥关系,而是容许多个线程同时读取共享资源,以进步并发性能。写模式下,依然只容许一个线程写入共享资源,以保证数据的一致性。

StampedLock 应用一个名为 stamp 的标记来示意锁的状态。在读锁和写锁的获取操作中,会返回一个 stamp 值,用于后续的操作。通过比拟 stamp 的值,咱们能够判断锁的状态是否发生变化。

StampedLock 提供了以下几个重要的办法:

  • readLock():返回一个读锁对象,用于获取读锁。
  • writeLock():返回一个写锁对象,用于获取写锁。
  • tryOptimisticRead():尝试获取乐观读锁,返回一个非负的 stamp 值。
  • validate():校验乐观读锁的 stamp 值是否依然无效。
  • tryConvertToWriteLock():尝试将乐观读锁转换为写锁,胜利则返回一个非负的 stamp 值,失败则返回零。
  • unlockRead():开释读锁。
  • unlockWrite():开释写锁。

StampedLock 的个性是在乐观读锁下,读操作不会阻塞写操作。当须要进行写操作时,如果乐观读锁胜利,能够间接将乐观读锁降级为写锁,防止了阻塞其余读线程的状况。但如果乐观读锁失败,须要转换为一般的读锁或写锁来保证数据一致性。

应用 StampedLock 须要留神的是,它并不是可重入锁,也不反对条件变量。在应用过程中,须要审慎解决锁的获取和开释,以防止死锁或其余并发问题。

StampedLock 提供了一种灵便且高效的读写锁机制,能够依据具体场景的需要来抉择不同的拜访模式,以均衡并发性能和数据一致性的需要。

3.4.1、示例

import java.util.concurrent.locks.StampedLock;

public class Point {
    private double x, y;
    private final StampedLock lock = new StampedLock();

    public void set(double x, double y) {long stamp = lock.writeLock();
        try {
            this.x = x;
            this.y = y;
        } finally {lock.unlockWrite(stamp);
        }
    }

    public double distanceFromOrigin() {long stamp = lock.tryOptimisticRead();
        double currentX = x;
        double currentY = y;
        if (!lock.validate(stamp)) {stamp = lock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {lock.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

public class StampedLockExample {public static void main(String[] args) {Point point = new Point();
        point.set(3.0, 4.0);

        Runnable readTask = () -> {double distance = point.distanceFromOrigin();
            System.out.println("Distance from origin:" + distance);
        };

        Runnable writeTask = () -> {point.set(5.0, 12.0);
            System.out.println("Point coordinates updated.");
        };

        Thread t1 = new Thread(readTask);
        Thread t2 = new Thread(writeTask);

        t1.start();
        t2.start();}
}

在下面的示例中,咱们创立了一个名为 Point 的类,其中蕴含了两个坐标 xy,以及一个 StampedLock 对象用于爱护这些坐标的拜访。

set() 办法应用写锁来更新坐标的值,它首先获取写锁,而后批改 xy 的值,最初开释写锁。

distanceFromOrigin() 办法应用乐观读锁来计算点到原点的间隔。它首先尝试获取乐观读锁,并在获取后拷贝 xy 的值。而后,应用 validate() 办法验证乐观读锁的有效性。如果验证失败(锁状态发生变化),则应用读锁从新获取 xy 的值,最初开释读锁。

main() 办法中,咱们创立了两个线程,一个线程用于读取点到原点的间隔,另一个线程用于更新点的坐标。通过应用 StampedLock,多个线程能够同时读取点的坐标,而在更新坐标时会阻塞其余读线程,以保证数据的一致性。

输入后果相似于以下内容:

Point coordinates updated.
Distance from origin: 5.0

3.5、Condition 条件

在 Java 锁中,Condition 接口用于提供对锁的条件期待和唤醒机制。它是与特定锁关联的,用于在特定条件不满足时挂起线程,并在条件满足时唤醒期待的线程。

Condition 接口中定义了以下次要办法:

  • await():在以后线程期待,并开释关联的锁,直到接管到信号或被中断。
  • awaitUninterruptibly():与 await() 相似,但不响应中断。
  • awaitNanos(long nanosTimeout):在以后线程期待指定的纳秒数,直到接管到信号、被中断或超时。
  • awaitUntil(Date deadline):在以后线程期待直到指定的相对工夫,直到接管到信号、被中断或超时。
  • signal():唤醒一个期待中的线程。
  • signalAll():唤醒所有期待中的线程。

Condition 的作用是使线程可能在特定的条件下期待和唤醒。它容许线程依照某种条件进行期待,而不是简略地阻塞在锁上。通过将线程挂起和唤醒的责任交给 Condition,能够更加灵便地控制线程的执行程序和互斥性。

Condition 常与 ReentrantLockReentrantReadWriteLock 一起应用。通过调用 lock 对象的 newCondition() 办法,能够创立一个与该锁关联的 Condition 实例。而后,能够应用 await() 办法在条件不满足时期待,应用 signal() 办法唤醒期待中的线程。

例如,在生产者 - 消费者模型中,生产者线程能够在队列满时调用 await() 办法期待,直到有空间可用。消费者线程能够在队列为空时调用 await() 办法期待,直到有数据可用。当生产者向队列增加数据或消费者从队列取出数据时,能够调用 signal() 办法唤醒相应的线程,以实现线程间的协调和同步。

通过应用 Condition,能够更准确地控制线程的期待和唤醒,提供更高级的线程同步机制,以满足简单的线程交互需要。

3.5.1、示例

生产者消费者模型


import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumer {private Queue<Integer> queue = new LinkedList<>();
    private int maxSize = 5;
    private ReentrantLock lock = new ReentrantLock();
    private Condition producerCondition = lock.newCondition();
    private Condition consumerCondition = lock.newCondition();

    public void produce() throws InterruptedException {lock.lock();
        try {while (queue.size() == maxSize) {
                // 队列已满,生产者期待
                producerCondition.await();}
            int item = (int) (Math.random() * 100);
            queue.add(item);
            System.out.println("Produced item:" + item);
            consumerCondition.signal(); // 唤醒一个消费者线程} finally {lock.unlock();
        }
    }

    public void consume() throws InterruptedException {lock.lock();
        try {while (queue.isEmpty()) {
                // 队列为空,消费者期待
                consumerCondition.await();}
            int item = queue.remove();
            System.out.println("Consumed item:" + item);
            producerCondition.signal(); // 唤醒一个生产者线程} finally {lock.unlock();
        }
    }
}

public class ProducerConsumerExample {public static void main(String[] args) {ProducerConsumer producerConsumer = new ProducerConsumer();

        Runnable producerTask = () -> {
            try {while (true) {producerConsumer.produce();
                    Thread.sleep(1000); // 生产者休眠一秒
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        };

        Runnable consumerTask = () -> {
            try {while (true) {producerConsumer.consume();
                    Thread.sleep(1000); // 消费者休眠一秒
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        };

        Thread producerThread = new Thread(producerTask);
        Thread consumerThread = new Thread(consumerTask);

        producerThread.start();
        consumerThread.start();}
}

在下面的示例中,咱们创立了一个名为 ProducerConsumer 的类,它保护了一个最大容量为 5 的队列作为共享资源。生产者线程应用 produce() 办法向队列中增加随机生成的我的项目,消费者线程应用 consume() 办法从队列中生产我的项目。

produce() 办法中,咱们首先获取 ReentrantLock 对象的锁,并应用 while 循环查看队列是否已满。如果队列已满,生产者线程将通过调用 producerCondition.await() 办法进入期待状态。当队列有空间可用时,生产者线程生成一个随机我的项目,并将其增加到队列中。随后,咱们调用 consumerCondition.signal() 办法唤醒一个期待中的消费者线程。最初,咱们开释锁。

consume() 办法中,咱们也首先获取锁,并应用 while 循环查看队列是否为空。如果队列为空,消费者线程将通过调用 consumerCondition.await() 办法进入期待状态。当队列中有我的项目可用时,消费者线程从队列中移除一个我的项目,并打印进去。随后,咱们调用 producerCondition.signal() 办法唤醒一个期待中的生产者线程。最初,咱们开释锁。

main() 办法中,咱们创立了一个 ProducerConsumer 实例,并别离创立了一个生产者线程和一个消费者线程来执行相应的工作。最初,咱们启动这两个线程,并让它们运行。生产者线程每秒产生一个我的项目,消费者线程每秒生产一个我的项目。

3.6、Semaphore 信号量

Semaphore(信号量)是 Java 中的一个并发管制工具,用于治理对共享资源的拜访。它保护了一个计数器,该计数器示意可用的许可数量。线程能够通过获取和开释许可来管制对共享资源的拜访。

Semaphore 提供了以下次要办法:

  • Semaphore(int permits):创立一个 Semaphore 对象,并指定初始的许可数量。
  • void acquire():获取一个许可,如果没有可用的许可,则线程将阻塞期待。
  • void release():开释一个许可,将其返回给 Semaphore,以便其余线程能够获取它。
  • int availablePermits():获取以后可用的许可数量。
  • boolean tryAcquire():尝试获取一个许可,如果胜利获取到许可,则返回 true,否则返回 false

3.6.1、示例

Semaphore 的用法如下:

  1. 创立一个 Semaphore 对象,并指定许可的数量。
  2. 在须要访问共享资源的线程中,调用 acquire() 办法获取一个许可。如果没有可用的许可,线程将被阻塞,直到有许可可用。
  3. 当线程实现对共享资源的拜访后,调用 release() 办法开释许可,以便其余线程能够获取它。
  4. 能够通过调用 availablePermits() 办法获取以后可用的许可数量。
  5. 能够应用 tryAcquire() 办法尝试获取许可,如果胜利获取到许可,则返回 true,否则返回 false,不会阻塞线程。

上面是一个应用 Semaphore 的简略示例,展现了如何限度同时拜访某个资源的线程数量:

import java.util.concurrent.Semaphore;

public class SharedResource {private Semaphore semaphore = new Semaphore(3); // 最多容许 3 个线程同时拜访

    public void accessResource() {
        try {semaphore.acquire(); // 获取一个许可
            System.out.println(Thread.currentThread().getName() + "正在访问共享资源");
            Thread.sleep(2000); // 模仿访问共享资源的耗时操作
            System.out.println(Thread.currentThread().getName() + "访问共享资源完结");
        } catch (InterruptedException e) {e.printStackTrace();
        } finally {semaphore.release(); // 开释许可
        }
    }
}

public class SemaphoreExample {public static void main(String[] args) {SharedResource sharedResource = new SharedResource();

        // 创立多个线程访问共享资源
        for (int i = 1; i <= 5; i++) {Thread thread = new Thread(() -> sharedResource.accessResource());
            thread.start();}
    }
}

在上述示例中,咱们创立了一个 SharedResource 类,其中的 accessResource() 办法模仿了对共享资源的拜访。在拜访资源之前,线程会调用 semaphore.acquire() 办法获取一个许可,如果没有可用的许可,线程将被阻塞期待。拜访资源完结后,线程调用 semaphore.release() 办法开释许可,以便其余线程能够获取它。

main() 办法中,咱们创立了一个 SharedResource 实例,并创立了多个线程来访问共享资源。通过应用 Semaphore,咱们限度了同时拜访资源的线程数量为 3。这意味着最多只有 3 个线程能够同时访问共享资源,其余线程须要期待许可的开释。

3.7、CountDownLatch 计数器

CountDownLatch 是一种罕用的并发工具,它能够让一个或多个线程期待其余线程执行结束后再进行操作。

3.7.1、原理

CountDownLatch 的原理是基于一个计数器实现的。计数器的初始值由用户指定,每当一个线程实现肯定的工作后,计数器的值就会减 1。当计数器的值达到零时,所有期待的线程将被唤醒。

具体实现原理如下:

  1. 创立 CountDownLatch 对象时,指定计数器的初始值。
  2. 当一个线程实现了肯定的工作后,调用 countDown() 办法将计数器的值减 1。
  3. 在调用 await() 办法的线程中,会进入期待状态,直到计数器的值为零。
  4. 每次调用 countDown() 办法都会减小计数器的值,并查看计数器的值是否曾经达到零。
  5. 如果计数器的值为零,则所有期待的线程将被唤醒,继续执行。

CountDownLatch 的实现应用了 AQS(AbstractQueuedSynchronizer)的同步机制。在外部,它应用一个共享的同步状态来示意计数器的值,并通过 acquireShared()releaseShared() 办法实现线程的期待和唤醒操作。

当线程调用 await() 办法时,它会尝试通过 acquireShared() 办法获取同步状态。如果计数器的值为零,则获取操作将立刻胜利,线程能够继续执行。否则,线程将进入期待队列,期待其余线程调用 countDown() 办法来减小计数器的值。

当线程调用 countDown() 办法时,它会通过 releaseShared() 办法开释同步状态。开释操作将会减小计数器的值,并查看是否达到零。如果计数器的值为零,则会唤醒所有期待的线程,使它们从期待队列中被移出,并继续执行。

通过这种形式,CountDownLatch 实现了线程之间的协调和同步,容许一个或多个线程期待其余线程实现肯定的工作后再继续执行。

CountDownLatch 提供了以下次要办法:

  • CountDownLatch(int count):创立一个 CountDownLatch 对象,并指定初始计数值。
  • void countDown():将计数器的值减 1。
  • void await():期待计数器的值达到零,即期待所有线程实现工作。

3.7.2、示例

  1. 建一个 CountDownLatch 对象,并指定初始计数值。
  2. 在每个线程中,实现肯定的工作后,调用 countDown() 办法将计数器的值减 1。
  3. 在主线程或须要期待的线程中,调用 await() 办法期待计数器的值达到零。

上面是一个简略示例,展现了如何应用 CountDownLatch 实现线程的协同工作:

import java.util.concurrent.CountDownLatch;

public class Worker implements Runnable {
    private final CountDownLatch latch;

    public Worker(CountDownLatch latch) {this.latch = latch;}

    @Override
    public void run() {
        try {System.out.println(Thread.currentThread().getName() + "正在执行工作");
            Thread.sleep(2000); // 模仿工作执行工夫
            System.out.println(Thread.currentThread().getName() + "工作执行结束");
            latch.countDown(); // 工作实现后,计数器减 1} catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

public class CountDownLatchExample {public static void main(String[] args) {
        int numThreads = 3; // 3 个工作线程
        CountDownLatch latch = new CountDownLatch(numThreads);

        // 创立工作线程
        for (int i = 1; i <= numThreads; i++) {Thread thread = new Thread(new Worker(latch));
            thread.start();}

        try {latch.await(); // 主线程期待计数器的值达到零
            System.out.println("所有工作执行结束,主线程继续执行");
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

在上述示例中,咱们创立了一个 Worker 类来模仿工作线程,每个工作线程执行一个耗时的工作。主线程创立了 3 个工作线程,并应用 CountDownLatch 来期待这些工作线程实现工作。

在每个工作线程的 run() 办法中,线程会执行工作并在工作实现后调用 countDown() 办法将计数器的值减 1。

主线程通过调用 latch.await() 办法期待计数器的值达到零。当所有工作线程都实现工作并调用 countDown() 办法后,计数器归零,开始执行主线程下的工作。

3.8、CyclicBarrier 循环屏障

CyclicBarrier 是 Java 中的一个同步辅助类,用于实现线程之间的同步和期待。它容许一组线程相互期待,直到所有线程都达到一个独特的屏障点,而后能够抉择执行一个独特的动作。

3.8.1、原理

CyclicBarrier 的原理是基于一个屏障点和计数器实现的。计数器的初始值由用户指定,每个线程达到屏障点后会期待,直到所有线程都达到屏障点时,屏障点会关上,所有线程能够继续执行。

具体实现原理如下:

  1. 创立 CyclicBarrier 对象时,指定计数器的初始值和所有线程达到屏障点后须要执行的动作(可选)。
  2. 当一个线程达到屏障点时,调用 await() 办法,计数器的值会减 1。
  3. 如果计数器的值变为零,示意所有线程都曾经达到屏障点,屏障点会关上。
  4. 当屏障点关上后,所有期待的线程都会被唤醒,继续执行。
  5. 如果指定了独特的动作,那么所有线程在通过屏障点后会执行该动作,而后继续执行各自的工作。

3.8.2、示例

CyclicBarrier 的应用步骤如下:

  1. 创立一个 CyclicBarrier 对象,并指定计数器的初始值和须要执行的独特动作(可选)。
  2. 在须要同步的线程中,调用 await() 办法来达到屏障点,并期待其余线程。
  3. 当所有线程都达到屏障点后,屏障点关上,所有线程能够继续执行。
  4. 如果指定了独特动作,所有线程通过屏障点后会执行该动作,而后持续各自的工作。

上面是一个示例,展现了如何应用 CyclicBarrier 实现线程的同步和期待:

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

class Worker implements Runnable {
    private final CyclicBarrier barrier;

    public Worker(CyclicBarrier barrier) {this.barrier = barrier;}

    @Override
    public void run() {
        try {System.out.println(Thread.currentThread().getName() + "执行工作");
            Thread.sleep(2000); // 模仿工作执行工夫
            System.out.println(Thread.currentThread().getName() + "工作执行结束");

            barrier.await(); // 期待其余线程} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();
        }
    }
}

public class CyclicBarrierExample {public static void main(String[] args) {
        int numThreads = 3; // 3 个工作线程
        Runnable barrierAction = () -> System.out.println("所有线程达到屏障点,开始执行独特动作");

        CyclicBarrier barrier = new Cyclic

Barrier(numThreads, barrierAction);

        // 创立工作线程
        for (int i = 1; i <= numThreads; i++) {Thread thread = new Thread(new Worker(barrier));
            thread.start();}
    }
}

在上述示例中,咱们创立了一个 Worker 类来模仿工作线程,每个工作线程执行一个耗时的工作。主线程创立了 3 个工作线程,并应用 CyclicBarrier 来实现它们的同步和期待。

在每个工作线程的 run() 办法中,线程会执行工作,并在工作实现后调用 barrier.await() 办法来达到屏障点,并期待其余线程。

当所有线程都达到屏障点后,指定的独特动作(如果有)会被执行,而后所有线程能够持续各自的工作。在本示例中,独特动作是输入一条音讯。

通过应用 CyclicBarrier,咱们能够实现线程的同步和期待,确保多个线程在达到屏障点后同时继续执行,从而协调线程之间的操作。

3.9、CountDownLatch 与 CyclicBarrier 区别

CountDownLatchCyclicBarrier 都是 Java 中用于线程同步和协调的工具类,但它们之间存在一些区别。

  1. 性能不同

    • CountDownLatch 是一种倒计时器,它容许一个或多个线程期待其余线程实现肯定数量的工作后再继续执行。
    • CyclicBarrier 是一种屏障,它容许一组线程互相期待,直到所有线程都达到屏障点后再同时继续执行。
  2. 计数器的变动形式不同

    • CountDownLatch 的计数器是一次性的,即初始值一旦设定,就不能被重置。每当一个线程实现肯定数量的工作后,计数器的值就会减 1,直到计数器的值为零。
    • CyclicBarrier 的计数器是循环的,即能够重复使用。每当一个线程达到屏障点后,计数器的值会减 1,直到计数器的值为零,而后计数器会被重置为初始值,线程能够继续执行下一个周期。
  3. 期待形式不同

    • CountDownLatch 应用 await() 办法使线程进入期待状态,直到计数器的值为零。
    • CyclicBarrier 应用 await() 办法使线程进入期待状态,直到所有线程都达到屏障点后才继续执行。
  4. 独特动作的执行形式不同

    • CountDownLatch 没有提供独特动作的执行机制。
    • CyclicBarrier 能够通过构造函数指定一个独特动作,在所有线程达到屏障点后执行。

综上所述,CountDownLatch 实用于一个或多个线程期待其余线程实现肯定数量的工作后再继续执行的场景,而 CyclicBarrier 实用于一组线程互相期待,直到所有线程都达到屏障点后再同时继续执行的场景。

当咱们谈到 CountDownLatchCyclicBarrier 的区别时,能够应用生存中的例子来阐明:

  1. CountDownLatch

    假如你是一支足球队的教练,你想要确保所有的球员都做好了筹备才开始较量。你会应用 CountDownLatch 来期待所有球员都筹备好。你会在更衣室门口设置一个计数器(初始值为球员人数),当每个球员筹备好时,他们会告诉你,你会将计数器减 1。只有当所有球员都筹备好,即计数器的值为零时,你才会率领球队走出更衣室开始较量。

  2. CyclicBarrier

    假如你是一群敌人打算一起去登山,你们决定在某个固定的集合点汇合后再一起登程。你会应用 CyclicBarrier 来确保大家都达到集合点后再开始行程。每个人在本人的家里筹备好后,会返回集合点并期待其他人。当所有人都达到集合点后,就会触发屏障,大家会一起开始登山。

在这两个例子中,CountDownLatchCyclicBarrier 都用于协调参与者的口头。CountDownLatch 用于期待一组线程实现某项工作后再继续执行,而 CyclicBarrier 用于期待一组线程达到一个屏障点后再同时继续执行。

以上是 Java 中罕用的锁机制,每种锁机制都有其特点和实用场景,依据具体的业务场景抉择适宜的锁机制能够进步程序的性能和可靠性。

四、锁的应用技巧

锁的应用技巧和最佳实际是 Java 并发编程中十分重要的一部分。上面是一些无关锁的应用技巧和最佳实际:

  1. 锁的范畴应该尽可能小

当应用锁来爱护共享资源时,锁的范畴应该尽可能小。这样能够缩小锁的争用,进步程序的并发性能。如果锁的范畴过大,会导致其余线程长时间期待锁的开释,从而影响程序的性能。

  1. 防止适度同步

适度同步是指在不必要的中央应用锁,导致程序的性能降落。为了防止适度同步,应该在确保线程安全性和可见性的前提下,尽可能地缩小锁的应用。

  1. 应用局部变量

在应用锁来爱护共享资源时,应该尽可能地应用局部变量。这样能够缩小对共享资源的拜访,进步程序的并发性能。

  1. 防止死锁

死锁是 Java 并发编程中一个十分重大的问题,必须防止产生。为了防止死锁,应该防止嵌套锁、防止长时间占用锁、依照雷同的程序获取锁等。

  1. 应用并发汇合

Java 提供了多种并发汇合,例如 ConcurrentHashMap、ConcurrentLinkedQueue 等。这些并发汇合能够在多线程环境下平安地拜访和批改共享资源,从而缩小了对锁的依赖。

  1. 应用可重入锁

可重入锁是一种非凡的锁,它能够被同一个线程屡次获取。应用可重入锁能够防止死锁等问题,进步程序的可靠性。

  1. 应用读写锁

读写锁是一种非凡的锁,它能够别离对读操作和写操作进行加锁和解锁。应用读写锁能够进步程序的并发性能,因为多个线程能够同时读取共享资源。

总之,锁的应用技巧和最佳实际是 Java 并发编程中十分重要的一部分。理解如何应用锁来爱护共享资源,避免竞争条件,并确保线程安全性和可见性,以及如何防止死锁和其余常见的多线程问题,并学习一些最佳实际,如防止适度同步、尽可能应用局部变量等,对于编写高效、牢靠的并发程序十分有帮忙。

五、Java 内存模型

volatile,happens-before

Java 内存模型(Java Memory Model,JMM)是 Java 虚拟机标准中形容的一种形象计算机模型,用于定义多线程程序中共享内存区域的拜访规定,确保多线程程序的正确性。

了解 Java 内存模型的原理和规定对于编写正确的并发代码至关重要。以下是一些相干的概念和技术:

5.1、主内存和工作内存

Java 内存模型中定义了两个次要的内存区域:主内存和工作内存。所有的线程都能够拜访主内存,但每个线程都有本人的工作内存。

在 Java 中,主内存(Main Memory)和工作内存(Working Memory)是两个重要的概念,用于形容多线程环境下的内存模型。

  1. 主内存(Main Memory):主内存是 Java 线程共享的内存区域,它是所有线程能够拜访的公共存储区域。在主内存中存储了所有的共享变量,包含实例字段、动态字段和数组元素等。
  2. 工作内存(Working Memory):工作内存是线程独立的内存区域,每个线程都有本人的工作内存。工作内存是主内存的一部分的拷贝,用于存储线程执行的数据和操作。线程对变量的读写操作都是在工作内存中进行的。

线程与主内存之间通过工作内存进行交互,线程从主内存中读取变量的值到工作内存中进行操作,而后再将后果写回主内存。这样的交互是通过 Java 内存模型(Java Memory Model)规定的内存操作实现的。

须要留神的是,每个线程对于共享变量的操作都是在本人的工作内存中进行的,不同线程之间的工作内存是互相独立的,线程之间无奈间接拜访彼此的工作内存。因而,为了保障线程之间的可见性和一致性,须要通过内存屏障、锁、volatile 等机制来进行同步和协调。

总结起来,主内存是线程共享的内存区域,存储了所有的共享变量;工作内存是线程独立的内存区域,用于存储线程执行的数据和操作。通过工作内存与主内存之间的交互,实现了多线程环境下的内存模型。

5.2、内存屏障

内存屏障是一种硬件或软件机制,用于强制处理器或编译器遵循指定的内存拜访程序。在 Java 内存模型中,内存屏障用于保障线程之间的正确同步。

内存屏障(Memory Barrier),也称为内存栅栏或内存栅障,是一种同步原语,用于管制对内存的拜访和操作程序,保障多线程环境下的内存可见性和有序性。

内存屏障分为两种类型:

  1. 读屏障(Read Barrier):读屏障用于确保在读操作之前,所有之前的读写操作都曾经实现,以保障读取到最新的值。它会阻止对读操作的重排序,使得读操作必须在读屏障之后的指令之前执行。
  2. 写屏障(Write Barrier):写屏障用于确保在写操作之前,所有之前的读写操作都曾经实现,以保障写入的值对其余线程可见。它会阻止对写操作的重排序,使得写操作必须在写屏障之前的指令之前执行。

内存屏障的作用是强制刷新处理器缓存或者阻止处理器将缓存写回主内存,以确保内存操作的程序性和可见性。它们能够用于解决因为指令重排序、缓存一致性等因素导致的线程间数据不统一的问题。

在 Java 中,内存屏障的应用被封装在各种同步机制中,例如锁(如 synchronizedReentrantLock)、volatile 关键字、Atomic类等。这些同步机制在适当的时候会插入内存屏障来保障内存操作的有序性和可见性。

须要留神的是,内存屏障的具体实现和成果可能因不同的硬件平台、编译器和 JVM 实现而有所不同。在编写多线程代码时,能够依赖于高级的同步机制,而无需间接应用内存屏障。

内存屏障能够分为以下几种类型:

  1. Load Barrier(读屏障):Load Barrier 用于确保在该屏障之前的读操作实现之后,后续的读操作能力开始执行。它能够避免指令重排序,保障了读操作的程序性和可见性。
  2. Store Barrier(写屏障):Store Barrier 用于确保在该屏障之前的写操作实现之后,后续的写操作能力开始执行。它能够避免指令重排序,保障了写操作的程序性和可见性。
  3. Read/Write Barrier(读写屏障):Read/Write Barrier 是 Load Barrier 和 Store Barrier 的组合。它用于确保在该屏障之前的读写操作实现之后,后续的读写操作能力开始执行。它既保证了读操作的程序性和可见性,也保障了写操作的程序性和可见性。
  4. Full Barrier(全屏障):Full Barrier 是最严格的屏障,它既蕴含了 Read/Write Barrier 的成果,又保障了 Load Barrier 和 Store Barrier 之间的程序性和可见性。Full Barrier 能够避免所有指令重排序,提供了最强的内存一致性。

这些屏障在不同的编程语言、硬件平台和编译器中可能有不同的名称和实现形式,但它们的根本作用是类似的:保障内存操作的程序性和可见性,防止因为指令重排序等起因引发的数据不统一问题。

在 Java 中,内存屏障的应用被封装在各种同步机制中,例如锁、volatile关键字、Atomic类等。这些同步机制会依据须要主动插入适当类型的内存屏障来保障内存操作的有序性和可见性。

上面别离以代码示例的形式阐明几种内存屏障的作用:

  1. Load Barrier(读屏障)

    int x = 0;
    int y = 0;
    volatile int z = 0;
    
    // 线程 1
    x = 1;
    y = z; // 读屏障,确保读操作在屏障之前的写操作实现
    
    // 线程 2
    z = 2; // 写操作

    在上述代码中,线程 1 中的读操作 y = z 之前插入了一个读屏障,确保线程 1 可能读取到最新的值。即便线程 2 对 z 的写操作在 y = z 之后执行,因为读屏障的存在,线程 1 依然能够读取到更新后的值。

  2. Store Barrier(写屏障)

    volatile int x = 0;
    int y = 0;
    
    // 线程 1
    x = 1; // 写操作
    y = 2; // 写操作
    // 写屏障,确保写操作在屏障之前的写操作实现
    
    // 线程 2
    int a = x; // 读操作
    int b = y; // 读操作

    在上述代码中,线程 1 在对变量 xy 进行写操作后,插入了一个写屏障。这个写屏障确保了线程 1 的写操作在屏障之前的写操作都曾经实现。因而,当线程 2 进行读操作时,可能读取到线程 1 实现的最新的值。

  3. Read/Write Barrier(读写屏障)

    volatile int x = 0;
    volatile int y = 0;
    
    // 线程 1
    x = 1; // 写操作
    // 读写屏障,确保写操作在屏障之前的写操作实现
    int a = y; // 读操作
    
    // 线程 2
    y = 2; // 写操作
    // 读写屏障,确保写操作在屏障之前的写操作实现
    int b = x; // 读操作

    在上述代码中,线程 1 在写操作 x = 1 之后插入了一个读写屏障,确保线程 1 的写操作在屏障之前的写操作都曾经实现。同样地,线程 2 在写操作 y = 2 之后也插入了一个读写屏障。这样能够保障线程 1 和线程 2 在读操作时可能读取到对方实现的最新的值。

  4. Full Barrier(全屏障)

    volatile int x = 0;
    volatile int y = 0;
    
    // 线程 1
    x = 1; // 写操作
    // 全屏障,确保写操作在屏障之前的写操作和读操作都曾经实现
    int a = y; // 
    
    读操作
    
    // 线程 2
    y = 2; // 写操作
    // 全屏障,确保写操作在屏障之前的写操作和读操作都曾经实现
    int b = x; // 读操作

    在上述代码中,线程 1 和线程 2 都在写操作后插入了一个全屏障。全屏障确保了写操作在屏障之前的写操作和读操作都曾经实现,从而保障线程 1 和线程 2 在读操作时可能读取到对方实现的最新的值。全屏障提供了最强的内存一致性。

须要留神的是,上述示例只是为了阐明内存屏障的作用,并非 Java 代码中理论应用内存屏障的典型场景。理论利用中,内存屏障的应用由编译器、JVM 和硬件平台来解决,通常被封装在同步机制中,如锁、volatile 关键字和原子操作等。

5.3、happens-before 关系

“happens-before”(产生在之前)是 Java 并发编程中的一个重要概念,它定义了对共享变量的读写操作之间的可见性和程序性保障。

以下是对于 ”happens-before” 原理的一些重要知识点:

  1. 程序程序规定(Program Order Rule):在单个线程内,依照程序的程序,后面的操作在前面的操作之前。即在同一个线程中,后面的操作的后果对后续操作可见。
  2. 监视器锁规定(Monitor Lock Rule):一个解锁操作(unlock)在同一个锁的后续锁定操作(lock)之前。即对于同一个锁对象,解锁操作的批改对于后续锁定操作的读取可见。
  3. volatile 变量规定(Volatile Variable Rule):对于 volatile 变量的写操作在后续对该变量的读操作之前。即通过 volatile 变量的写入,能够保障对该变量的读取是可见的。
  4. 线程启动规定(Thread Start Rule):线程的启动操作在该线程的所有操作之前。即在一个线程启动之前,它的操作对于启动后的线程是可见的。
  5. 线程终止规定(Thread Termination Rule):线程的所有操作在终止操作之前。即一个线程的所有操作对于其余线程来说是可见的,直到该线程终止。
  6. 中断规定(Interruption Rule):对于被中断的线程,中断操作在后续对该线程的操作之前。即中断操作的批改对于后续操作的读取是可见的。
  7. 传递性(Transitivity):如果操作 A 在操作 B 之前,操作 B 在操作 C 之前,那么操作 A 在操作 C 之前。

“happens-before” 准则提供了在多线程环境下对内存操作进行排序和可见性保障的规定。通过遵循 ”happens-before” 准则,能够确保多线程程序中的操作依照预期程序执行,并且线程间的数据共享是牢靠的。在理论编程中,了解和正确利用 ”happens-before” 准则是确保多线程程序正确性的要害。

5.4、volatile 关键字

volatile 是 Java 中的关键字,用于申明变量,具备非凡的内存语义。它的次要原理是确保对被申明为 volatile 的变量的读取和写入操作具备可见性和有序性。

  1. 可见性:当一个线程对一个 volatile 变量进行写操作时,JVM 会立刻将该变量的最新值刷新到主内存中,而不是仅仅存储在线程的工作内存中。其余线程在读取该变量时,会从主内存中获取最新的值,而不是应用本地缓存的旧值。这样能够保障不同线程之间对于 volatile 变量的读写操作是可见的,防止了应用过期的值。
  2. 有序性volatile 关键字还保障了变量的读写操作具备有序性。在一个线程中,所有的操作(包含 volatile 变量的读写)都是依照程序的程序执行的。这意味着在一个线程中,写入一个 volatile 变量的操作产生在后续读取该变量的操作之前。这样能够确保在不同线程中对于 volatile 变量的读写操作也具备肯定的程序性。

须要留神的是,volatile 关键字不能代替锁,它次要实用于以下状况:

  • 对变量的写入操作不依赖于变量的以后值。
  • 变量没有蕴含在具备原子性要求的复合操作中。
  • 不须要保障变量的互斥拜访。

当须要满足上述条件时,应用 volatile 能够提供一种轻量级的线程同步机制,但在其余状况下,依然须要应用锁或其余更弱小的同步机制来确保线程安全性。

5.5、synchronized 关键字

synchronized 是 Java 语言中的另一个关键字,用于实现线程之间的同步。应用 synchronized 关键字能够保障同一时间只有一个线程可能拜访被爱护的代码块。

具体能够参考文章结尾介绍的内容

六、总结

目前就整顿了这几种工作中罕用的锁类型,也简略介绍了 JMM 内存模型,具体的每一个也都给出了示例,须要本人亲手体验下了。

理解以上这些概念和技术能够帮忙您更好地了解 Java 内存模型,从而编写出更平安、更正确的并发程序。

前面会针对每一个小的知识点做一个更全面的剖析。欢送关注。

参考链接

https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

https://www.artima.com/insidejvm/ed2/threadsynchP.html

本文由 mdnice 多平台公布

退出移动版