《Java-SE-第二十七章》之常见的锁策略

简介: 《Java-SE-第二十七章》之常见的锁策略

文章目录

常见的锁策略

乐观锁vs悲观锁

读写锁

重量级锁vs轻量级锁

自旋锁vs挂起等待锁

公平锁vs非公平锁

**可重入锁** **vs** **不可重入锁**

常见的锁策略

乐观锁vs悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。


乐观锁:假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并

发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

举个栗子:合租房里面有2个人一起合租,厕所只有一个。A在上厕所的时候,就比较乐观,他上厕所的时候,他就觉得B想上厕所的概率比较小,就不锁门了。这就是乐观锁。所谓的悲观锁就是,B上厕所的时候,就比较悲观,就觉得A懒人屎尿多,老想上厕所,就每次上厕所的时候,即使A不在家,也会把门锁上。

读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.

两个线程都要写一个数据, 有线程安全问题.

一个线程读另外一个线程写, 也有线程安全问题.

Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读锁写锁,ReentrantReadWriteLock.ReadLock 类表示一个读锁.,这个对象提供了 lock / unlock 方法进行加锁解锁。 ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁。其中读加锁和读加锁不互相互斥,写加锁和写加锁之前互斥,读加锁和写加锁之间互斥。

使用演示

我们创建了一个ReadWriteLockExample类,其中包含了一个sharedData变量,表示共享数据。ReadWriteLock用于控制对sharedData的读写访问。

读取操作使用读锁(readLock)进行保护,允许多个线程同时读取共享数据。写入操作使用写锁(writeLock)进行保护,确保在写入过程中只有一个线程能够修改共享数据。

在main方法中,我们创建了多个读取线程和一个写入线程。读取线程会不断读取共享数据并输出,而写入线程会每隔一段时间写入一个新的数据。由于使用了读写锁,读取线程可以并发执行读操作,而写入线程则可以互斥地执行写操作,从而实现了对共享数据的安全读写。

实现代码

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
    private int sharedData = 0;
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private Lock readLock = rwLock.readLock();
    private Lock writeLock = rwLock.writeLock();
    public void readData() {
        readLock.lock();
        try {
            System.out.println("Read Thread: Reading data: " + sharedData);
        } finally {
            readLock.unlock();
        }
    }
    public void writeData(int data) {
        writeLock.lock();
        try {
            System.out.println("Write Thread: Writing data: " + data);
            sharedData = data;
        } finally {
            writeLock.unlock();
        }
    }
    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();
        // 创建多个读取线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                while (true) {
                    example.readData();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        // 创建一个写入线程
        new Thread(() -> {
            int data = 1;
            while (true) {
                example.writeData(data);
                data++;
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

运行结果:

重量级锁vs轻量级锁

锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的,CPU 提供了 "原子操作指令,然后操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.,JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类。

重量级锁

 重量级锁 加锁机制重度依赖了 OS 提供了 mutex,使用此锁容易引起线程的调度以及大量的内核态用户态的切换。这两个操作成本都比较高,一旦涉及到用户态和内核态的切换,就意味着"沧海桑田",典型的进入内核态的加锁逻辑,开销比较大。

轻量级锁

 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex,轻量级锁和重量级锁相反,不太容易引起线程调度以及只有少量的内核态用户态的切换,典型的纯用户态的加锁逻辑,开销比较小。

 举个栗子,去银行办理业务,在窗口找机器办理,自己操作,这里是用户态,用户态的时间成本相对是可控的。有时候得去排队找工作人员办理,这就是内核态.内核态的时间成本不太可控,因为办理业务的时候存在和工作人员大量的沟通,还需要排队,这时的效率就很低。

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

自旋锁vs挂起等待锁

自旋锁

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度,但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU。 这个时候就可以使用自旋锁来处理这样的问题.。

自旋锁伪代码

while (抢锁(lock) == 失败) {}

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.。一旦锁被其他线程释放, 就能第一时间获取到锁。

举个栗子,这就还比张三和妹子出去约会,当张三已经到了目的地,张三就给妹子打电话,问到了没,妹子就说"马上",挂了之后,你又不停的打电话询问到了没。这样你就第一时间知道她到了。

自旋锁是一种典型的 轻量级锁 的实现方式.

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

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

挂起等待锁

当获取锁失败后,就会挂起等待。

举个栗子,依旧是张三和妹子约会的,张三不再反复的打电话询问妹子到了没,而是站在那里看小说等待着妹子过来。

公平锁vs非公平锁

假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.。当线程 A 释放锁的时候, 会发生啥呢?

公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.。

非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.

举个栗子,当被一群男生心心念念的女神失恋后,这些男生对女生都展开了猛烈的追求,先来的男生先上位,这就是公平锁。如果是女神看那个顺眼就和那个在一起,这就是非公平锁。

使用ReentrantLock演示公平锁和非公平锁,ReentrantLock默认是非公平锁

代码演示

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockFairnessDemo {
    private static Lock fairLock = new ReentrantLock(true); // 公平锁
    private static Lock nonFairLock = new ReentrantLock(); // 非公平锁
    public static void main(String[] args) {
        Runnable fairTask = () -> {
            fairLock.lock();
            try {
                System.out.println("Fair lock: Thread " + Thread.currentThread().getId() + " acquired the lock.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                fairLock.unlock();
            }
        };
        Runnable nonFairTask = () -> {
            nonFairLock.lock();
            try {
                System.out.println("Non-fair lock: Thread " + Thread.currentThread().getId() + " acquired the lock.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                nonFairLock.unlock();
            }
        };
        // 使用公平锁执行任务
        for (int i = 0; i < 5; i++) {
            new Thread(fairTask).start();
        }
        try {
            Thread.sleep(10000); // 等待2秒,以确保公平锁的线程先运行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------------------");
        // 使用非公平锁执行任务
        for (int i = 0; i < 5; i++) {
            new Thread(nonFairTask).start();
        }
    }
}

运行结果:

 当运行程序时,你会注意到公平锁的输出中,线程获得锁的顺序与线程启动的顺序一致。这是公平锁保证的特性。而在非公平锁的输出中,线程的获得锁顺序与线程启动顺序不一致,这是因为非公平锁在某些情况下允许新线程抢占锁,以提高并发性能。

可重入锁vs不可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。不可重入锁就是同一个线程多次获取同一把锁,把自己锁死。

各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。


相关文章
|
2月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
44 2
|
10天前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
21天前
|
消息中间件 监控 算法
Java性能优化:策略与实践
【10月更文挑战第21】Java性能优化:策略与实践
|
1月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
41 1
|
1月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
算法 Java 关系型数据库
Java中到底有哪些锁
【9月更文挑战第24天】在Java中,锁主要分为乐观锁与悲观锁、自旋锁与自适应自旋锁、公平锁与非公平锁、可重入锁以及独享锁与共享锁。乐观锁适用于读多写少场景,通过版本号或CAS算法实现;悲观锁适用于写多读少场景,通过加锁保证数据一致性。自旋锁与自适应自旋锁通过循环等待减少线程挂起和恢复的开销,适用于锁持有时间短的场景。公平锁按请求顺序获取锁,适合等待敏感场景;非公平锁性能更高,适合频繁加解锁场景。可重入锁支持同一线程多次获取,避免死锁;独享锁与共享锁分别用于独占和并发读场景。
|
1月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
16 0
|
1月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
32 0