Параллельное программирование на Java (4) Синхронизация потоков [AQS|Lock]

Обзор

В Java блокировку можно использовать для обеспечения безопасности доступа к ресурсам, когда несколько потоков обращаются к определенному общедоступному ресурсу. Java предлагает два способа блокировки

  • Первый из них — это блокировка с помощью ключевого слова Synchronized, как мы упоминали выше. Базовый уровень синхронизации размещается на JVM для выполнения, и после Java 1.6 было сделано множество оптимизаций (блокировка смещения, вращение, облегченная блокировка), что очень удобен в использовании.И производительность тоже очень хорошая, поэтому рекомендуется использовать синхронизированные операции синхронизации, когда в этом нет необходимости;
  • Второй — блокировка через Lock в пакете java.util.concurrent, который будет представлен в этой статье ( lock широко использует CAS+spin. Поэтому в соответствии с характеристиками CAS рекомендуется использовать lock в случае низкий уровень конфликтов блокировок )

АКС

Обзор

  • Полное имя AQS — AbstractQueuedSynchronizer, что переводится как синхронизатор абстрактной очереди.
  • Базовая структура данных AQS — это изменчивое измененное состояние и двунаправленная очередь узла.
  • Классы реализации в разделе Lock включают ReentrantLock, ReadLock и WriteLock, все из которых основаны на AQS для получения или освобождения ресурсов блокировки.

внутренняя структура

Согласно исходному коду, мы можем знать, что AQS поддерживает нестабильное состояние и двустороннюю очередь CLH (FIFO).

state — это переменная мьютекса типа int, измененная с помощью Летучего.State = 0 означает, что ни один поток задачи не использует ресурс, а state>= 1 означает, что поток уже удерживает ресурс блокировки. Очередь CLH — это очередь FIFO, поддерживаемая внутренним классом Node.

Принцип реализации

Когда поток получает ресурс блокировки, он сначала определяет, равно ли состояние 0 (состояние без блокировки). Если оно равно 0, состояние обновляется до 1. В это время ресурс блокировки занят. Если в этом процессе несколько потоков одновременно выполняют операции обновления состояния, это вызовет проблемы с безопасностью потоков. Таким образом, нижний уровень AQS использует механизм CAS для обеспечения атомарности взаимоисключающих обновлений состояния переменных . Потоки, не получившие блокировку, блокируются с помощью метода park в классе Unsafe. Заблокированные потоки помещаются в двусвязный список CLH по принципу «первым вошел — первым вышел». Когда поток, получивший блокировку, снимается. блокировку, он начнется с головы этого двусвязного списка. Пробудите следующий ожидающий поток и поборитесь за блокировку.

Справедливая блокировка и несправедливая блокировка

При конкуренции за ресурсы блокировки справедливым блокировкам необходимо определить, есть ли заблокированные потоки в двусвязном списке. Если они есть, им нужно встать в очередь и ждать. Способ обработки несправедливых блокировок заключается в том, что независимо от того, есть ли заблокированные потоки, ожидающие в очереди в двусвязном списке, он попытается изменить переменную состояния, чтобы конкурировать за блокировку, что несправедливо по отношению к потокам, стоящим в очереди в связанном списке. .

Блокировка интерфейса

Класс реализации блокировки

  • В JDK8, за исключением StampedLock, который является нереентерабельной блокировкой, все другие ключевые слова, включая ReentrantLock, ReentrantReadWriteLock и Synchronized, являются реентерабельными блокировками.
  • Повторная блокировка означает, что поток вытеснил ресурс блокировки мьютекса и может повторно получить ресурс блокировки, прежде чем блокировка будет снята. Ему нужно только записать количество повторных входов, и состояние увеличивается на 1.
  • Lock фактически контролирует ситуацию удержания блокировки, обновляя состояние в AQS.

Метод блокировки

// 尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
// 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
// 尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

Реентерантлокок

  • Согласно исходному коду, вы можете видеть, что ключевая переменная-член типа Sync, реализующая функцию блокировки, наследует AQS.
  • Sync имеет два класса реализации в ReentrantLock: тип справедливой блокировки NonfairSync и тип нечестной блокировки FairSync.
  • ReentrantLock по умолчанию является реализацией нечестной блокировки. При создании экземпляра можно указать справедливую или нечестную блокировку.

Процесс блокировки получения ReentrantLock

//.lock()调用的是AQS的acquire()
public void lock() {
    sync.acquire(1);
}

public final void acquire(int arg) {
    //tryAcquire:会尝试通过CAS获取一次锁。
    //addWaiter:将当前线程加入双向链表(等待队列)中
    //acquireQueued:通过自旋,判断当前队列节点是否可以获取锁
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//---------------------非公平锁尝试获取锁的过程---------------------
protected final boolean tryAcquire(int acquires) {
	// AQS的nonfairTryAcquire()方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state
    int c = getState();
    if (c == 0) {
    	// 目前没有线程获取锁,通过CAS(乐观锁)去修改state的值
        if (compareAndSetState(0, acquires)) {
        	// 设置持有锁的线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁的持有者是当前线程(重入锁)
    else if (current == getExclusiveOwnerThread()) {
    	// state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

//---------------------当前线程加入双向链表的过程---------------------
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
    	// 获取末位节点
        Node oldTail = tail;
        if (oldTail != null) {
        	// 当前节点的prev设置为原末位节点
            node.setPrevRelaxed(oldTail);
            // CAS确保在线程安全的情况下,将当前线程加入到链表的尾部
            if (compareAndSetTail(oldTail, node)) {
            	// 原末位节点的next设置为当前节点
                oldTail.next = node;
                return node;
            }
        } else {
        	// 链表为空则初始化
            initializeSyncQueue();
        }
    }
}

//---------------------首节点自旋过程---------------------
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 首节点线程去尝试竞争锁
            if (p == head && tryAcquire(arg)) {
            	// 成功获取到锁,从首节点移出(FIFO)
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

Процесс снятия блокировки ReentrantLock

Суть снятия блокировки заключается в постепенном уменьшении значения состояния State в AQS.

//.unlock()调用AQS的release()方法释放锁资源
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
	// Sync的tryRelease()方法
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
	// 获取状态
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 修改锁的持有者为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

Повторный входЧтениеЗаписьБлокировка

  • Блокировка чтения-записи ReentrantReadWriteLock может получить блокировку чтения или блокировку записи соответственно, то есть разделить операции чтения и записи данных ;
  • writeLock(): получить блокировку записи.
    • writeLock().lock(): заблокировать блокировку записи
    • writeLock().unlock(): снять блокировку записи.
  • readLock(): получить блокировку чтения.
    • readLock().lock(): заблокировать блокировку чтения
    • readLock().unlock(): снять блокировку чтения.
  • Блокировки чтения используют общий режим, а блокировки записи — монопольный режим . То есть, когда нет блокировки записи, блокировка чтения может удерживаться несколькими потоками одновременно; когда есть блокировка записи, за исключением потока, получившего блокировку записи, другие потоки не могут получить блокировку чтения; и при наличии блокировки чтения невозможно получить блокировку записи
  • Подходит для большего чтения и написания меньшего количества сценариев приложений, таких как кэширование.

Состояние

Обзор 

  • Условие также является механизмом взаимодействия потоков, который реализует блокировку потоков и пробуждение с помощью await и SingalAll().
  • Базовая структура данных представляет собой очередь, которая повторно использует класс узла AQS и реализуется в виде связанного списка без ведущего узла.
  • Принцип реализации Await: перевести текущий поток в состояние блокировки ожидания через LockSupport.park до тех пор, пока другие потоки не вызовут signal или signalAll, чтобы переместить головной узел очереди ожидания в очередь синхронизации, давая ему возможность получить блокировку посредством вращения.
  • signal/signalAll: переместите головной узел очереди ожидания в очередь синхронизации и разбудите поток через LockSupport.unpark.
  • Сравнение с механизмом ожидания/уведомления Объекта
    • Условие поддерживает отсутствие реакции на прерывания, но объект не может
    • Блокировка может поддерживать несколько очередей ожидания условий, а объект может поддерживать только одну.
    • Условие может установить тайм-аут для ожидания, но объект не может
  • Проблема производитель-потребитель может быть реализована с помощью Lock+Condition (соответствующие примеры будут приведены позже в главе о практике параллелизма).

Практика условий

package com.bierce;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Lock + Condition: 实现线程按序打印
 * 案例:开启3个线程,id分别为A、B、C,并打印10次,而且按顺序交替打印如:ABCABCABC...
 */
public class TestCondition {
    public static void main(String[] args) {
        PrintByOrderDemo print = new PrintByOrderDemo();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {// 打印10次
                print.loopA(i);
            }
        },"A").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                print.loopB(i);
            }
        },"B").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                print.loopC(i);
            }
        },"C").start();
    }
}
class PrintByOrderDemo{
    private int number = 1; //当前正在执行线程的标记
    private Lock lock = new ReentrantLock();
    private Condition ConditionA = lock.newCondition();
    private Condition ConditionB = lock.newCondition();
    private Condition ConditionC = lock.newCondition();

    public void loopA(int totalLoop){
        lock.lock();
        try {
            if ( number != 1){ //判断当前是否打印A
                ConditionA.await();
            }
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName()); //打印A
            }
            //唤醒其他线程
            number = 2;
            ConditionB.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void loopB(int totalLoop){
        lock.lock();
        try {
            if ( number != 2){ //判断当前是否打印B
                ConditionB.await();
            }
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName()); //打印B
            }
            //唤醒其他线程
            number = 3;
            ConditionC.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void loopC(int totalLoop){
        lock.lock();
        try {
            if ( number != 3){ //判断当前是否打印C
                ConditionC.await();
            }
            for (int i = 1; i <= 1; i++) {
                System.out.print(Thread.currentThread().getName()); //打印C
            }
            //唤醒其他线程
            number = 1;
            ConditionA.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Supongo que te gusta

Origin blog.csdn.net/qq_34020761/article/details/132234785
Recomendado
Clasificación