首页 > 编程语言 >精通Java并发锁机制:24种锁技巧+业务锁匹配方案(第一部分)

精通Java并发锁机制:24种锁技巧+业务锁匹配方案(第一部分)

时间:2024-09-25 22:26:54浏览次数:7  
标签:24 种锁 synchronized lock ReentrantLock 线程 Java public

image

在 Java 并发编程中,锁是确保线程安全、协调多线程访问共享资源的关键机制。从基本的 synchronized 同步关键字到高级的 ReentrantLock、读写锁 ReadWriteLock、无锁设计如 AtomicInteger,再到复杂的同步辅助工具如 CountDownLatchCyclicBarrierSemaphore,每种锁都针对特定的并发场景设计,以解决多线程环境下的同步问题。StampedLock 提供了乐观读锁和悲观写锁的选项,而 ConcurrentHashMapConcurrentLinkedQueue 等并发集合则通过内部机制优化了并发访问。了解不同锁的特点和适用场景,对于构建高效、稳定的并发应用程序至关重要。

肖哥弹架构 跟大家“弹弹” 高并发锁, 关注公号回复 'mvcc' 获得手写数据库事务代码

欢迎 点赞,关注,评论。

关注公号Solomon肖哥弹架构获取更多精彩内容

历史热点文章

1、java锁领域范围介绍

image

1.1 基本锁

  • synchronized:Java的内置锁机制,用于同步方法或代码块。
  • ReentrantLock:可重入的互斥锁,提供了与synchronized相比更灵活的锁定机制。

1.2 读写锁

  • ReadWriteLock:允许多个读操作并行执行,但写操作会阻塞所有其他读写操作。
  • StampedLock:一种新的读写锁,支持乐观读和悲观写,以及避免ABA问题的版本号。

1.3 乐观/悲观锁

  • 乐观锁:基于无锁的算法,通常使用版本号或时间戳来实现。
  • 悲观锁:假设会发生冲突并采取预防措施,通常通过锁定机制实现。

1.4 原子变量和无锁结构

  • AtomicIntegerAtomicLongAtomicReference:提供无锁的线程安全操作。
  • ConcurrentHashMap:一个线程安全的哈希表,使用CAS操作来保证线程安全。

1.5 条件变量和屏障

  • Lock.newCondition:与ReentrantLock配合使用,允许线程在某些条件满足之前挂起。
  • CountDownLatch:允许一个或多个线程等待一组操作完成。除了作为倒计时门闩使用,也可以作为一次性的屏障点。
  • CyclicBarrier:允许一组线程相互等待,直到所有线程都到达某个公共屏障点。除了作为循环屏障使用,也可以作为可重用的屏障点。

1.6 信号量

  • Semaphore:用于控制对有限资源的访问,允许一定数量的线程并发访问资源。

1.7 高级并发集合

  • ConcurrentHashMap:线程安全的哈希表。
  • ConcurrentSkipListMap:线程安全的有序映射。
  • ConcurrentLinkedQueue:线程安全的无界队列。
  • BlockingQueue 实现:如ArrayBlockingQueueLinkedBlockingQueue等,提供阻塞操作的队列。

1.8 锁策略和优化

  • 锁粗化:将多个细粒度锁合并为一个粗粒度锁。
  • 锁消除:通过编译器优化,消除一些不必要的锁。
  • 锁分割:将一个大锁分解成多个小锁,以提高并发性能。

2、锁详细分析

2.1. synchronized

synchronized 是 Java 中用于实现线程同步的关键字,它通过内置锁(也称为监视器锁 Monitor)来控制多个线程对共享资源的访问。
image

图解说明:
  • Java 线程:表示运行中的线程,它们可能需要访问共享资源。
  • Java 内存:表示Java程序使用的内存空间,包括堆和栈等。
  • 共享资源:表示被多个线程共享的数据,需要通过同步机制来保护。
  • 对象头:Java对象头包含多个信息,其中之一是 Mark Word,用于存储对象的同步信息。
  • Mark Word:存储对象的锁状态信息,如是否被锁定、锁的持有者等。
  • Monitor:内置锁的实现机制,也称为监视器锁,通过对象的 Mark Word 指向一个监视器对象。
综合说明:
  • 作用synchronized 是Java语言中用于实现线程同步的一种关键字,它可以用于修饰方法或代码块,确保在同一时刻最多只有一个线程可以执行该段代码。
  • 背景:在早期的Java版本中,为了简化线程同步的操作,避免程序员直接使用底层的同步原语(如互斥锁),Java提供了synchronized关键字,它基于JVM的内置锁(Monitor)实现。
  • 优点
    • 使用简单:只需通过关键字即可实现同步,无需手动获取和释放锁。
    • 保证内存可见性:自动处理内存的可见性问题,确保线程间的正确通信。
  • 缺点
    • 锁粒度较粗:只能锁定整个方法或代码块,缺乏灵活性。
    • 性能问题:在高并发环境下,可能导致线程频繁阻塞和唤醒,影响性能。
  • 场景:适用于锁竞争不激烈,且同步代码块较小的场景。
  • 业务举例:在银行账户系统中,当一个客户通过ATM机进行存款或取款操作时,需要确保账户余额的更新操作在同一时刻只能由一个线程执行,以防止资金数据的不一致。
使用方式:
// 同步方法案例
class Counter {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

// 同步代码块案例
class Account {
    private double balance;
    private final Object lock = new Object();

    public void deposit(double amount) {
        synchronized (lock) {
            balance += amount;
        }
    }

    public double getBalance() {
        synchronized (lock) {
            return balance;
        }
    }
}

// 同步静态方法案例
class Utility {
    private static int sharedResource = 0;

    // 同步静态方法
    public static synchronized void modifyResource() {
        sharedResource++;
    }

    public static synchronized int getSharedResource() {
        return sharedResource;
    }
}

// 同步实例方法(锁定调用实例)
class BankAccount {
    private double balance;

    public synchronized void deposit(double amount) {
        balance += amount;
    }

    public synchronized double getBalance() {
        return balance;
    }
}

// 同步实例方法(自定义锁对象)
class BankAccountWithCustomLock {
    private double balance;
    private final Object lock = new Object();

    public void deposit(double amount) {
        synchronized (lock) {
            balance += amount;
        }
    }

    public double getBalance() {
        synchronized (lock) {
            return balance;
        }
    }
}

// 测试类
public class SynchronizedDemo {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.increment();
        System.out.println(counter.getCount());

        Account account = new Account();
        account.deposit(100.0);
        System.out.println(account.getBalance());

        Utility.modifyResource();
        System.out.println(Utility.getSharedResource());

        BankAccount bankAccount = new BankAccount();
        bankAccount.deposit(200.0);
        System.out.println(bankAccount.getBalance());

        BankAccountWithCustomLock bankAccountWithLock = new BankAccountWithCustomLock();
        bankAccountWithLock.deposit(300.0);
        System.out.println(bankAccountWithLock.getBalance());
    }
}
业务代码案例:

业务说明: 在一个在线银行系统中,客户可以通过各种电子渠道(如网上银行、移动应用等)进行资金操作,包括查看账户余额、存款、取款和转账等。系统需要确保所有这些操作在多用户并发访问时的数据一致性和准确性。

为什么需要 synchronized 技术: 在多线程环境中,多个用户可能同时对同一个账户进行操作。如果没有适当的同步机制,如 synchronized,那么两个并发操作可能会同时修改账户余额,导致数据不一致。例如,两个线程同时从同一个账户中扣款,如果没有同步控制,可能会导致账户余额被错误地减少两次。

没有 synchronized 技术会带来什么后果

  • 数据不一致:最直接和严重的后果是账户余额可能会不正确。多个线程同时修改同一个变量可能会导致最终值与预期不符。
  • 金融风险:在金融系统中,数据不一致可能导致严重的金融风险和损失,比如用户资金被错误地扣除或增加。
  • 信任危机:如果用户发现账户余额不准确,可能会对银行系统的信任度下降,导致客户流失。
  • 法律和合规问题:金融行业的数据准确性受到严格的法律和规章监管。数据不一致可能违反相关法规,导致法律责任和罚款。

代码实现:

public class BankAccount {
    private double balance;  // 账户余额

    // 构造函数
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // 存款方法
    public synchronized void deposit(double amount) {
        // 增加余额
        balance += amount;
        System.out.println("Deposited " + amount + ". New balance is " + balance);
    }

    // 取款方法
    public synchronized void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            System.out.println("Withdrew " + amount + ". Remaining balance is " + balance);
        } else {
            System.out.println("Insufficient funds for withdrawal!");
        }
    }

    // 获取余额
    public synchronized double getBalance() {
        return balance;
    }
}

// 测试类
public class BankDemo {
    public static void main(String[] args) {
        final BankAccount account = new BankAccount(1000); // 开始余额为1000

        // 创建线程进行存款
        Thread depositThread = new Thread(() -> {
            account.deposit(500);
        });

        // 创建线程进行取款
        Thread withdrawThread = new Thread(() -> {
            account.withdraw(200);
        });

        depositThread.start();
        withdrawThread.start();
    }
}

2.2. ReentrantLock

ReentrantLock 是 Java 中一个高级同步工具,它提供了比 synchronized 更多的灵活性和控制能力。这个锁基于 Java 的 java.util.concurrent.locks.Lock 接口实现,通常内部使用 AQS(AbstractQueuedSynchronizer)来管理锁的获取和释放。
image

图解说明:
  • Java 线程:表示运行中的线程,它们可能需要访问共享资源。
  • ReentrantLock 实例:是 ReentrantLock 类的实例,用于控制对共享资源的访问。
  • AQS(同步队列)AbstractQueuedSynchronizer 的实例,用于管理锁的请求和释放。它维护一个同步队列,该队列保存所有请求锁的线程。
  • 共享资源:表示被多个线程共享的数据,需要通过锁来保护以确保线程安全。
  • 线程等待队列:当锁不可用时,请求锁的线程可能会被放入等待队列。
  • 锁状态:表示当前锁的状态,如锁定、未锁定、重入次数等。
  • 条件变量ReentrantLock 可以与一个或多个条件变量关联,用于更复杂的线程间协调。
  • 公平锁标志:指示锁是否以公平模式运行,公平模式下,锁会按照线程请求的顺序分配。
  • 独占模式ReentrantLock 默认以独占模式运行,即一次只允许一个线程持有锁。
  • 重入次数:记录当前线程获取锁的次数,允许同一线程多次获取锁。
  • 队列头节点:指向同步队列中的第一个节点,通常是获取锁的线程节点。
综合说明:
  • 作用ReentrantLock 是Java并发包中的一个类,提供了与synchronized关键字类似的同步功能,但它提供了更丰富的API,如尝试非阻塞获取锁、可中断获取锁、超时获取锁等。
  • 背景:随着多核处理器的普及和Java应用对并发性能要求的提高,synchronized的局限性逐渐显现,因此Java并发包提供了ReentrantLock,以提供更灵活的线程同步机制。
  • 优点
    • 功能丰富:支持多种锁获取方式,包括可中断、超时、尝试非阻塞获取等。
    • 公平锁:可以设置为公平锁,以减少线程饥饿问题。
  • 缺点
    • 使用复杂:需要手动获取和释放锁,增加了编码复杂度。
    • 必须手动释放:如果异常处理不当,可能导致锁无法释放。
  • 场景:适用于需要精细控制锁或需要高级功能的场景,如可中断锁获取、尝试非阻塞获取等。
  • 业务举例:在高并发的电商平台中,ReentrantLock 可以用于实现一个线程安全的购物车系统,其中锁的灵活性和高级功能有助于提高系统的响应速度和用户体验。例如,当多个用户同时尝试购买同一件库存有限的商品时,ReentrantLock可以确保商品库存的更新操作是原子性的,避免超卖现象。
使用方式:
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class Counter {
    // 定义一个 ReentrantLock 对象作为锁
    private final ReentrantLock lock = new ReentrantLock();
    // 用于计数的共享资源
    private int count = 0;

    // 锁的获取与释放
    public void increment() {
        // 获取锁
        lock.lock();
        try {
            // 对共享资源进行操作,此处为增加计数
            count++;
            // 打印更新后的计数
            System.out.println("Count updated to " + count);
        } finally {
            // 确保在操作完成后释放锁
            lock.unlock();
        }
    }

    public int getCount() {
        // 获取锁以确保线程安全的读取
        lock.lock();
        try {
            // 返回当前计数
            return count;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    // 尝试非阻塞地获取锁
    public void tryIncrement() {
        // 尝试获取锁,如果锁不可用则立即返回false
        if (lock.tryLock()) {
            try {
                // 如果成功获取锁,则增加计数
                count++;
                // 打印更新后的计数
                System.out.println("Count updated to " + count);
            } finally {
                // 释放锁
                lock.unlock();
            }
        } else {
            // 如果未能获取锁,打印提示信息
            System.out.println("Unable to acquire lock");
        }
    }

    // 带超时的锁获取
    public void incrementWithTimeout(long timeout, TimeUnit unit) {
        // 尝试在指定的时间内获取锁
        if (lock.tryLock(timeout, unit)) {
            try {
                // 如果成功获取锁,则增加计数
                count++;
                // 打印更新后的计数
                System.out.println("Count updated to " + count);
            } finally {
                // 释放锁
                lock.unlock();
            }
        } else {
            // 如果在指定时间内未能获取锁,打印提示信息
            System.out.println("Unable to acquire lock within timeout");
        }
    }

    // 可中断地获取锁
    public void incrementInterruptibly() throws InterruptedException {
        // 可中断地获取锁
        lock.lockInterruptibly();
        try {
            // 如果成功获取锁,则增加计数
            count++;
            // 打印更新后的计数
            System.out.println("Count updated to " + count);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    // 定义两个条件变量,一个用于缓冲区不满,一个用于缓冲区不空
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Object[] items = new Object[100]; // 存储数据的数组
    private int putPtr, takePtr, count; // 指向待放入位置的指针、指向待取出位置的指针和计数器

    public void put(Object x) throws InterruptedException {
        // 获取锁
        lock.lock();
        try {
            // 使用循环等待缓冲区不满的条件
            while (count == items.length) {
                notFull.await();
            }
            // 放入元素
            items[putPtr] = x;
            // 更新放入位置的指针
            putPtr = (putPtr + 1) % items.length;
            // 更新计数器
            count++;
            // 通知可以取元素的条件变量
            notEmpty.signal();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        // 获取锁
        lock.lock();
        try {
            // 使用循环等待缓冲区不空的条件
            while (count == 0) {
                notEmpty.await();
            }
            // 取出元素
            Object x = items[takePtr];
            // 更新取出位置的指针
            takePtr = (takePtr + 1) % items.length;
            // 更新计数器
            count--;
            // 通知可以放元素的条件变量
            notFull.signal();
            // 返回取出的元素
            return x;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

// 测试类
public class ReentrantLockDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        counter.increment(); // 同步方法,用于增加计数
        System.out.println("Current count: " + counter.getCount()); // 同步方法,用于获取当前计数

        counter.tryIncrement(); // 尝试非阻塞地获取锁并增加计数
        counter.incrementWithTimeout(2, TimeUnit.SECONDS); // 尝试在指定时间内获取锁并增加计数

        BoundedBuffer buffer = new BoundedBuffer(); // 创建有界缓冲区
        buffer.put("Item 1"); // 生产者放入元素
        Object item = buffer.take(); // 消费者取出元素
        System.out.println("Taken from buffer: " + item);
    }
}
业务代码案例:

业务说明: 在线交易平台允许多个用户同时提交订单。每个订单都需要检查库存、扣减库存数量、生成订单记录并更新用户余额。这些操作需要作为一个原子操作来处理,以确保库存数据的一致性和避免超卖现象。

为什么需要 ReentrantLock 技术: 在高并发的环境下,多个用户可能同时对同一商品提交订单。使用 ReentrantLock 可以确保在检查和扣减库存的过程中,只有一个请求能够操作共享资源,避免了并发导致的数据不一致问题。

没有 ReentrantLock 技术会带来什么后果: 如果没有适当的同步机制,如 ReentrantLock,可能会导致库存数据被多个请求同时修改,从而出现超卖现象。这不仅会损害消费者体验,还可能导致财务损失和信誉下降。

代码实现:

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class OrderService {
    private final ReentrantLock lock = new ReentrantLock();
    private final InventoryService inventoryService;
    private final AccountService accountService;

    public OrderService(InventoryService inventoryService, AccountService accountService) {
        this.inventoryService = inventoryService;
        this.accountService = accountService;
    }

    public void processOrder(int productId, int quantity, double price) throws InterruptedException {
        lock.lock();
        try {
            // 检查库存是否充足
            if (inventoryService.checkInventory(productId, quantity)) {
                // 扣减库存
                inventoryService.decreaseInventory(productId, quantity);
                // 更新用户余额
                accountService.updateBalance(userId, price * quantity);
                // 创建订单记录
                createOrderRecord(productId, quantity, price);
                System.out.println("Order placed successfully for product ID: " + productId);
            } else {
                System.out.println("Not enough inventory for product ID: " + productId);
            }
        } finally {
            lock.unlock();
        }
    }

    private void createOrderRecord(int productId, int quantity, double price) {
        // 实现订单记录的创建逻辑
    }

    // 模拟库存服务
    static class InventoryService {
        public boolean checkInventory(int productId, int quantity) {
            // 实现检查库存逻辑
            return true;
        }

        public void decreaseInventory(int productId, int quantity) {
            // 实现扣减库存逻辑
        }
    }

    // 模拟账户服务
    static class AccountService {
        public void updateBalance(int userId, double amount) {
            // 实现更新账户余额逻辑
        }
    }
}

// 测试类
public class交易平台Demo {
    public static void main(String[] args) throws InterruptedException {
        InventoryService inventoryService = new OrderService.InventoryService();
        AccountService accountService = new OrderService.AccountService();
        OrderService orderService = new OrderService(inventoryService, accountService);

        // 模拟多个用户同时下单
        Thread t1 = new Thread(() -> orderService.processOrder(1, 2, 10.0));
        Thread t2 = new Thread(() -> orderService.processOrder(1, 3, 10.0));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

其他内容在第二篇文章《精通Java并发锁机制:24种锁技巧+业务锁匹配方案(第二部分)》中。。。

标签:24,种锁,synchronized,lock,ReentrantLock,线程,Java,public
From: https://www.cnblogs.com/xiaoge-it/p/18432384

相关文章

  • Java BigDecimal 详解
     目录一、BigDecimal简介二、常用方法A、BigDecimal常用构造方法B、BigDecimal常用方法二、代码实现A、加减乘除1.创建两个BigDecimal对象2.BigDecimal相加3.BigDecimal相减4.BigDecimal相乘5.BigDecimal相除B、转换1.定义一个数值2.转换3.java.math.BigDeci......
  • Java中的序列化和反序列化
    Java中序列化和反序列化的区别序列化和反序列化的定义序列化(Serialization)与反序列化(Deserialization)是编程中常见的两个概念,他们主要涉及到将数据结构或对象状态转换为可以存储或传输的格式,以及将存储或传输的格式转换回原始的数据结构或对象状态的过程。这两个过程在数据持久......
  • 2024主流前端框架对比和选择
    Hello,大家好,我是Feri,一枚十多年的程序员,同时也是一名在读研究生,关注我,且看一个平凡的程序员如何在自我成长,CodingSir是我想打造一个编程社区,只为各位小伙伴提供编程相关干货知识,希望在自我蜕变的路上,我们一起努力,努力什么时候开始都不晚,我,从现在开始做起!一、前言     ......
  • 0924-25,QT的数据类型,实现一个井字棋和计算器(只输入)
    day-01#include"mainwindow.h"#include<stdio.h>#include<iostream>#include<QApplication>#include<QDebug>#include<QPoint>#include<QLine>intmain(intargc,char*argv[]){QApplicationa(argc,......
  • javaScript 值的比较
    值的比较值的比较是指判断两个数的大小,返回一个布尔值。  比较运算符列表:   大于>  小于<  大于等于>= 小于等于<= 等于== 严格等于===不进行类型转换不等于!= 严格不等于!==不进行类型转换 字符串比较大小字符串间的比较大小遵循以下规则:1比较字符串首字母的大小。......
  • JavaScript中if嵌套 assert
    在JavaScript中,通常我们不会直接使用assert这个词,因为JavaScript标准库中并没有直接提供assert函数(尽管在一些测试框架如Jest、Mocha中经常看到)。但是,我们可以模拟一个assert函数的行为,即当某个条件不满足时抛出一个错误。结合if语句进行嵌套判断时,可以在每个需要断言的地方调用这......
  • 2024.9.24 Python与C++面试八股文
    1.externextern关键字用于在多个文件中引用同一个全局变量的声明在一个头文件中,如果这个变量声明了,但是在cpp文件中没找到他的定义,那么编译就会报错,但是如果加了extern,编译器就不会给头文件报错,而是给cpp文件报错,如果函数没定义的话。或者定义出错的话。2.关于反复调用简......
  • 2024.9 做题笔记
    CF1575IIllusionsoftheDesert看这个边权这么复杂,猜测其必然有一些性质。对\(a_u,a_v\)的正负分讨易得\(\max(|a_u+a_v|,|a_u-a_v|)=|a_u|+|a_v|\),树剖树状数组单点修改链求和即可。ABC177FIhateShortestPathProblem考虑dp,设\(f_{i,j}\)表示到达第\(i\)行第\(......
  • 2024年一款非常好用的视频剪辑软件会声会影Corel VideoStudio2024,非常适合新手
    随着数字媒体的飞速发展,视频剪辑已成为表达创意、传播信息的重要工具。2024年,视频剪辑软件市场迎来了新一轮的革新与竞争。今天,我们就来盘点一下这一年里备受瞩目的十大视频剪辑软件,无论你是初学者还是专业团队,都能在其中找到适合你的那一款。会声会影CorelVideoStudio2024一......
  • 【Java】JVM垃圾收集器深入解析:原理与实践
    目录一、判断对象是否存活1.引用计数算法2.可达性计数算法3.Java中的四种引用 3.1强引用(StrongReference)3.2软引用(SoftReference)3.3弱引用(WeakReference)3.4虚引用(PhantomReference)3.5小结二、垃圾收集算法1.分代收集理论1.1分代存储1.2分......