自学内容网 自学内容网

Java 多线程与锁策略的深入探讨

在 Java 的多线程编程中,锁策略、CAS(Compare and Swap)机制以及 synchronized 的优化过程是非常重要的概念。本文将对这些知识点进行总结和讲解,并加入一些代码示例以帮助理解。

一、锁策略


1. 悲观锁与乐观锁

  • 悲观锁:总是假设最坏的情况,每次访问数据时都加锁,确保数据安全。

    • 示例
public class PessimisticLockExample {  
    private final Object lock = new Object();  
    private int data;  

    public void updateData(int newData) {  
        synchronized (lock) {  
            data = newData; // 加锁,确保数据安全  
        }  
    }  
}

乐观锁:假设不会发生冲突,只有在提交更新时才会检查是否有其他线程修改了数据。

  • 示例
public class OptimisticLockExample {  
    private int data;  

    public boolean updateData(int expectedValue, int newValue) {  
        if (data == expectedValue) {  
            data = newValue; // 直接更新数据  
            return true;  
        }  
        return false; // 更新失败  
    }  
}  

 Synchronized 初始使⽤乐观锁策略. 当发现锁竞争⽐较频繁的时候, 就会⾃动切换成悲观锁策略.

2. 重量级锁与轻量级锁

锁的核⼼特性 "原⼦性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

  1. CPU 提供了 "原⼦操作指令".
  2.  操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁.
  3.  JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
  • 重量级锁:依赖于操作系统提供的互斥锁,涉及到用户态和内核态的切换,成本较高。
  • 轻量级锁:尽量在用户态完成加锁操作,只有在必要时才使用互斥锁。

synchronized 开始是⼀个轻量级锁. 如果锁冲突⽐较严重, 就会变成重量级锁.

3. 自旋锁

自旋锁是一种轻量级锁,线程在获取锁失败后不会进入阻塞状态,而是持续尝试获取锁,直到成功

  • 示例
public class SpinLock {  
    private volatile Thread owner = null;  

    public void lock() {  
        while (!compareAndSetOwner(null, Thread.currentThread())) {  
            // 自旋等待  
        }  
    }  

    public void unlock() {  
        owner = null;  
    }  

    private boolean compareAndSetOwner(Thread expected, Thread newOwner) {  
        if (owner == expected) {  
            owner = newOwner;  
            return true;  
        }  
        return false;  
    }  
}

 如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会 在极短的时间内到来.

⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.

⾃旋锁是⼀种典型的 轻量级锁 的实现⽅式.

优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁.

缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不 消耗 CPU 的).

synchronized 中的轻量级锁策略⼤概率就是通过⾃旋锁的⽅式实现的.

 4. 公平锁与非公平锁

  • 公平锁:遵循“先来后到”的原则,确保按照请求顺序获取锁。
  • 非公平锁:不保证顺序,可能导致后来的线程优先获取锁。

操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序。

synchronized 是⾮公平锁.

5. 可重入锁

可重入锁指的是同一线程可以多次获取同一把锁而不会导致死锁。

Java⾥只要以Reentrant开头命名的锁都是可重⼊锁,⽽且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重⼊的。

⽽ Linux 系统提供的 mutex 是不可重⼊锁.

  • 示例
import java.util.concurrent.locks.ReentrantLock;  

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

    public void method() {  
        lock.lock();  
        try {  
            // 业务逻辑  
            method(); // 递归调用  
        } finally {  
            lock.unlock();  
        }  
    }  
}  

synchronized 是可重⼊锁

6. 读写锁

读写锁允许多个线程同时读取数据,但写操作是互斥的。

  • 示例:
import java.util.concurrent.locks.ReentrantReadWriteLock;  

public class ReadWriteLockExample {  
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();  
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();  
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();  
    private int sharedData = 0;  

    public void readData() {  
        readLock.lock();  
        try {  
            // 读取共享数据  
            System.out.println("Reading data: " + sharedData);  
        } finally {  
            readLock.unlock();  
        }  
    }  

    public void writeData(int data) {  
        writeLock.lock();  
        try {  
            // 写入共享数据  
            sharedData = data;  
            System.out.println("Writing data: " + sharedData);  
        } finally {  
            writeLock.unlock();  
        }  
    }  
}

二、CAS(Compare and Swap)机制


CAS: 全称Compare and swap,字⾯意思:”⽐较并交换“,⼀个 CAS 涉及到以下操作:

  1. ⽐较 A 与 V 是否相等。(⽐较)
  2. 如果⽐较相等,将 B 写⼊ V。(交换)
  3. 返回操作是否成功。

CAS 是一种乐观锁的实现方式,涉及三个操作:

  1. 比较内存中的值与预期值是否相等。
  2. 如果相等,则将新值写入内存。
  3. 返回操作是否成功。
  • 示例:
public class CASExample {  
    private volatile int value;  

    public boolean compareAndSet(int expectedValue, int newValue) {  
        if (value == expectedValue) {  
            value = newValue; // 交换  
            return true;  
        }  
        return false; // 操作失败  
    }  
}

针对不同的操作系统,JVM ⽤到了不同的 CAS 实现原理,简单来讲:

  • ava 的 CAS 利⽤的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使⽤了汇编的 CAS 操作,并使⽤ cpu 硬件提供的 lock 机制保证其原⼦ 性。

1. 原子变量操作

Java提供了多种原子类(如AtomicInteger、AtomicLong、AtomicBoolean等),它们都使用CAS来实现原子更新操作。这些类在不使用锁的情况下提供了一种无锁的线程安全机制。

  • 示例
AtomicInteger atomicInteger = new AtomicInteger(0);  

public void increment() {  
    atomicInteger.incrementAndGet(); // 使用CAS实现原子性  
}

2. 自旋锁

自旋锁通过CAS循环不断尝试获取锁。与传统锁不同,自旋锁不会使线程进入阻塞状态。

  • 示例
public class SpinLock {  
    private AtomicReference<Thread> owner = new AtomicReference<>();  
    
    public void lock() {  
        Thread current = Thread.currentThread();  
        while (!owner.compareAndSet(null, current)) {  
            // 自旋等待  
        }  
    }  
    
    public void unlock() {  
        owner.set(null);  
    }  
}

3. ABA问题解决方案

CAS操作中的ABA问题是指:在一次CAS操作期间,一个变量可能被其它线程改变为另一个值,然后又恢复为原来的值。

为了解决这个问题,Java提供了AtomicStampedReference和 AtomicMarkableReference它们通过版本号(或标记)来避免ABA问题。

  • 示例:
import java.util.concurrent.atomic.AtomicStampedReference;  

public class AtomicStampedReferenceExample {  
    // 创建一个AtomicStampedReference对象,初始值为100,初始版本号为0  
    private AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 0);  

    // 更新操作  
    public void update() {  
        // 用于存储当前版本号的数组  
        int[] stampHolder = new int[1];  

        // 获取当前值,并通过stampHolder获取当前的版本号  
        Integer value = stampedRef.get(stampHolder); // value为当前值,stampHolder[0]为当前版本号  

        // 计算新的版本号  
        int newStamp = stampHolder[0] + 1; // 新版本号为当前版本号加1  

        // 尝试使用CAS更新值和版本号  
        // 如果当前值和版本号分别等于value和stampHolder[0],则将值更新为value + 1,版本号更新为newStamp  
        boolean updated = stampedRef.compareAndSet(value, value + 1, stampHolder[0], newStamp);  

        // 结果:更新成功返回true,更新失败返回false  
        if (updated) {  
            System.out.println("Update successful: Value is " + stampedRef.getReference() + " with stamp " + stampedRef.getStamp());  
        } else {  
            System.out.println("Update failed");  
        }  
    }  

    public static void main(String[] args) {  
        AtomicStampedReferenceExample example = new AtomicStampedReferenceExample();  
        example.update(); // 对stampedRef进行更新操作  
    }  
}

CAS为多线程编程提供了高效无锁的同步机制,广泛应用于需要多线程安全且高性能的场合。它通过乐观并发控制,允许多个线程同时操作数据,大大提高了系统的可伸缩性和响应速度。

3. synchronized的优化过程


synchronized关键字是Java中用于实现同步的基本机制之一。在JDK 1.8中,synchronized经历了一系列的优化,以提高其性能和效率。

1. 乐观锁与悲观锁

  • 乐观锁synchronized在初始阶段采用乐观锁的策略,假设不会发生锁竞争,因此不立即加锁。
  • 悲观锁:如果锁竞争频繁,synchronized会转换为悲观锁,确保线程安全。

2. 轻量级锁与重量级锁

  • 轻量级锁:在锁竞争不激烈的情况下,synchronized使用轻量级锁。轻量级锁通过CAS操作实现,避免了线程阻塞。
  • 重量级锁:如果锁被持有的时间较长或竞争激烈,轻量级锁会膨胀为重量级锁,使用操作系统的互斥量来实现线程阻塞和唤醒。

3. 自旋锁

  • 自旋锁:在实现轻量级锁时,synchronized大概率会使用自旋锁策略。自旋锁让线程在短时间内反复尝试获取锁,而不是立即进入阻塞状态,从而减少线程上下文切换的开销。

4. 不公平锁

  • 不公平锁synchronized是一种不公平锁,意味着线程获取锁的顺序不一定按照请求的顺序进行。这种策略可以提高吞吐量,但可能导致某些线程长期得不到锁。

5. 可重入锁

  • 可重入锁synchronized是可重入锁,同一线程可以多次获取同一把锁而不会导致死锁。这是通过在锁中记录持有锁的线程和计数器来实现的。

6. 锁消除

  • 锁消除:编译器和JVM会判断某些锁是否可以消除。如果在单线程环境中使用了synchronized,编译器可能会优化掉这些不必要的锁。

7. 锁粗化

  • 锁粗化:如果在一段逻辑中多次加锁和解锁,编译器和JVM会自动进行锁粗化,将多个锁操作合并为一个较大的锁操作,以减少频繁的加锁和解锁开销。

通过这些优化,synchronized在JDK 1.8中变得更加高效,能够在保证线程安全的同时,尽量减少锁带来的性能损耗。这些优化使得synchronized在许多情况下成为一个性能良好的同步机制。


原文地址:https://blog.csdn.net/singreen37/article/details/142702131

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!