详解Java中的悲观锁与乐观锁

详解Java中的悲观锁与乐观锁

什么是锁?

在多线程编程中,为了保证线程安全和数据一致性,我们常常采用锁机制。锁顾名思义就是在一段代码区域加上一个锁,使得同一时刻只有一个线程可以访问该代码区域。Java中的锁机制主要有两种:悲观锁和乐观锁。

悲观锁

悲观锁的思想就是认为并发情况下不同线程之间会发生冲突,因此在整个处理过程中,都加上了同步锁,让线程独占资源,其他线程等待。

Java中常见的悲观锁实现方式是synchronized关键字,例如:

public synchronized void method(){
    // 该方法的代码块
}

synchronized关键字会自动加锁,当线程执行synchronized代码块时,其他线程无法访问该代码块,只能等待。

乐观锁

乐观锁的思想则是相反的,它认为并发情况下不同线程之间不会发生冲突,因此不需要加锁。在更新数据时,判断该数据有没有被其他线程修改,若该数据被修改,则暂停该次操作,重新读取数据并重试。

Java中的乐观锁实现方式一般是通过CAS(Compare And Swap)来实现,例如:

public class Counter {
    private AtomicInteger num = new AtomicInteger(0);

    public void increment() {
        int newVal;
        do {
            newVal = num.get() + 1;
        } while(!num.compareAndSet(num.get(), newVal));
    }
}

CAS先获取内存中的值,然后基于获取的值来计算新值,最后通过CAS操作来尝试更新内存中的值。如果这个值不是期望的,那么修改失败,就需要重试。

悲观锁与乐观锁的选择

在选择使用悲观锁还是乐观锁时,需要根据实际情况来进行考虑。

悲观锁适合于写操作比较多的情况,因为写操作需要独占资源。悲观锁在实现简单的同时也存在着性能问题,因为当访问量大时,悲观锁会降低系统的吞吐量。

乐观锁适合于读操作比较多的情况,因为读操作不需要独占资源,多个线程可以共同读取一个数据。并且乐观锁的实现方式可以避免了加锁引起的性能问题。

示例

下面两个示例演示了悲观锁和乐观锁的实现方式。其中线程安全的计数器Counter类用于演示两种锁的实现方式。

悲观锁示例

public class PessimisticLockExample {
    private static int value = 0;

    public static synchronized void increment() {
        value++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("PessimisticLockExample value: " + value);
    }
}

乐观锁示例

public class OptimisticLockExample {
    private static AtomicInteger value = new AtomicInteger(0);

    public static void increment() {
        int oldValue;
        int newValue;
        do {
            oldValue = value.get();
            newValue = oldValue + 1;
        } while(!value.compareAndSet(oldValue, newValue));
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("OptimisticLockExample value: " + value);
    }
}

总结

悲观锁的实现方式简单直接,但存在性能问题,适合于写操作比较多的情况。乐观锁的实现方式可以避免加锁带来的性能问题,适合于读操作比较多的情况。在实际情况中,需要根据具体情况选择合适的锁实现方式来保证线程安全和数据一致性。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:详解Java中的悲观锁与乐观锁 - Python技术站

(0)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • 使用maven-assembly-plugin如何打包多模块项目

    使用maven-assembly-plugin打包多模块项目需要分为以下几个步骤: 在父项目中添加maven-assembly-plugin插件,并对子模块的打包进行配置; 子模块中添加打包所需相关资源,并配置打包信息。 下面提供两个示例来更清晰地讲解上述步骤。 示例1: 假设我们有一个项目,该项目包含一个父项目和两个子模块:模块A和模块B。我们希望使用ma…

    Java 2023年5月19日
    00
  • java8 stream 如何打印数据元素

    Java8 Stream 如何打印数据元素? Java8 中引入了 Stream API,它是用于描述对基于元素序列的一个或多个操作的流式计算的 API。使用 Stream API,可以以声明性的方式处理数据元素,而不是命令式的方式,这样可以极大地提高代码的可读性和表达性。 在 Java8 Stream 中,打印流中的所有元素通常是我们进行调试的一项重要操作…

    Java 2023年5月26日
    00
  • java使用计算md5校验码方式比较两个文件是否相同

    计算MD5校验码是一种常用的文件完整性校验方式。在Java中,使用Java加密扩展(JCE)提供的MessageDigest类来计算MD5校验码,比较两个文件是否相同可以参考以下步骤: 1.引入JCE 首先需要在Java代码中引入JCE扩展包,可以在官网下载或在Maven中添加依赖: pom.xml依赖: <dependency> <gro…

    Java 2023年6月15日
    00
  • 浅谈Spring与SpringMVC父子容器的关系与初始化

    浅谈Spring与SpringMVC父子容器的关系与初始化 在SpringMVC中,Spring框架和SpringMVC框架是通过父子容器的方式进行协作的。本文将介绍Spring和SpringMVC父子容器的关系和初始化过程。 Spring和SpringMVC父子容器的关系 在SpringMVC中,Spring框架和SpringMVC框架是通过父子容器的方式…

    Java 2023年5月17日
    00
  • Java Web 简单的分页显示实例代码

    下面是详细讲解“Java Web 简单的分页显示实例代码”的完整攻略,包括两条示例说明: 1. 分页显示实现原理 在实现分页显示之前,我们需要先了解分页的原理。当我们在页面中点击“下一页”或者“上一页”等翻页按钮时,客户端会向服务器发送请求,请求需要显示的数据的页数及每页显示的数据数量。服务器收到请求后,根据请求参数查询指定页数的数据,返回给客户端,客户端再…

    Java 2023年6月15日
    00
  • Nginx服务器中强制使用缓存的配置及缓存优先级的讲解

    针对“Nginx服务器中强制使用缓存的配置及缓存优先级的讲解”的问题,我可以提供以下的完整攻略: 强制使用缓存的配置 proxy_cache_bypass 在Nginx服务器中,可以通过设置proxy_cache_bypass配置来强制使用缓存。当需要绕过缓存向后端请求或满足某些条件时,可以通过在请求中设置特定的头部来绕过缓存,格式如下: proxy_cac…

    Java 2023年6月16日
    00
  • 浅析jQuery Ajax通用js封装

    我将为您详细讲解“浅析jQuery Ajax通用js封装”的完整攻略: 1. 什么是jQuery Ajax通用js封装 jQuery Ajax通用js封装是一种在Javascript中使用jQuery库对Ajax进行封装的方法。通过对Ajax通用操作的封装,可以实现代码的重用,减少代码冗余,提高代码的可维护性。 2. jQuery Ajax通用js封装要点 …

    Java 2023年6月15日
    00
  • mybatis查询语句揭秘之参数解析

    下面是关于”mybatis查询语句揭秘之参数解析”的完整攻略。 什么是参数解析? 在Mybatis框架中,#{}和${}是两种常用的参数占位表达式。它们在执行sql语句时,代表不同的参数解析方式。 #{}表示的是预编译的SQL语句参数占位符,会将传入的参数使用JDBC的预编译功能进行替换,可以有效地防止SQL注入攻击。 ${}表示的是占位符,会将参数直接拼接…

    Java 2023年5月20日
    00
合作推广
合作推广
分享本页
返回顶部