Java中的锁(二)

        锁从不同的角度有不同的分类,从线程是否需要锁住同步资源角度来分,可以分为:悲观锁和乐观锁。

一、悲观锁、乐观锁的定义

        悲观锁就是我们常说到的锁。对于悲观锁来说,他总是认为每次访问共享资源时会发生冲突(认为别的线程会修改),所以必须每次数据操作会上锁,以保证临界区的程序同一时间只能有一个线程在执行(共享资源同一时间只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

        由于悲观锁的频繁加锁,因此导致了一些问题的出现:比如在多线程竞争下,频繁加锁、释放锁导致频繁的上下文切换和调度延时,一个线程持有锁会导致其他线程进入阻塞状态,从而引起性能问题。

        乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问不会产生冲突(认为别的线程不会修改),线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用乐观锁的一种实现方式CAS实现的。

        由于无锁操作中没有锁的存在,因此不可能出现死锁情况,也就是说乐观锁天生免疫死锁。 

        乐观锁多用于“读多写少”的环境,避免频繁加锁影响性能;而悲观锁锁用于“写多读少”的环境,避免频繁失败和重试影响性能。

二、实现方式

        悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁。synchronized关键字和Lock的实现类都是悲观锁。

        乐观锁的实现方式主要有两种:CAS机制和版本号机制。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法。

        以Java中的自增操作( i++ )为例,看一下悲观锁和CAS分别是如何保证线程安全的。在Java中自增操作不是原子操作,它实际上包含三个独立的操作:

  1. 从内存中取出 i 的当前值;
  2. 将 i 的值加 1;
  3. 将计算好的值放入到内存当中;

        因此,如果并发执行自增操作,可能导致计算结果的不准确。在下面的代码示例中:value1没有进行任何线程安全方面的保护,value2使用了乐观锁(CAS),value3使用了悲观锁(synchronized)。运行程序,使用5000个线程同时对value1、value2和value3进行自增操作,可以发现:value2和value3的值总是等于5000,而value1的值常常小于5000。

package com.example.demo;import java.util.concurrent.atomic.AtomicInteger;public class Test {//value1: 线程不安全private static int value1 = 0;//value2: 使用乐观锁private static AtomicInteger value2 = new AtomicInteger(0);//value2: 使用悲观锁private static int value3 = 0;private static synchronized void increaseValue3(){value3++;}public static void main(String[] args) throws InterruptedException {for (int i=0; i< 5000; i++){new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}value1++;value2.getAndIncrement();increaseValue3();}}).start();}// 打印结果Thread.sleep(10000);System.out.println("value1 线程不安全:"+ value1);System.out.println("value2 乐观锁:"+ value2);System.out.println("value3 悲观锁:"+ value3);}
}

输出结果:

value1 线程不安全:4760
value2 乐观锁:5000
value3 悲观锁:5000 

三、乐观锁两种实现方式

(1)版本号机制

        一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

        举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

package com.example.demo;import java.math.BigDecimal;public class DebitCard {// 账户名称private String account;//账户余额private BigDecimal amount;public DebitCard(String account, BigDecimal amount) {this.account = account;this.amount = amount;}public String getAccount() {return account;}public void setAccount(String account) {this.account = account;}public BigDecimal getAmount() {return amount;}public void setAmount(BigDecimal amount) {this.amount = amount;}@Overridepublic String toString() {return "DebitCard{" +"account='" + account + '\'' +", amount='" + amount + '\'' +'}';}
}
package com.example.demo;import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockDemo {private AtomicInteger version = new AtomicInteger(0);private DebitCard debitCard = new DebitCard("zhangsan", new BigDecimal(100));public AtomicInteger getVersion() {return version;}public DebitCard getDebitCard() {return debitCard;}public void updateDebitCard(BigDecimal amount){int currentVersion = version.get();// 模拟读取数据的过程DebitCard currentDebitCard = debitCard;// 模拟其他线程修改数据try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 检查版本号是发生变化if (currentVersion == version.get()){// 版本号为变化,可以进行更新操作currentDebitCard.setAmount(currentDebitCard.getAmount().add(amount));debitCard = currentDebitCard;version.incrementAndGet();System.out.println("数据更新成功,当前版本号:"+ version.get()+",数据内容 debitCard= "+ debitCard.toString());} else {// 版本号已经变化,更新操作失败System.out.println("数据更新失败,版本号已变化");}}public static void main(String[] args) {OptimisticLockDemo demo = new OptimisticLockDemo();// 创建两个线程同时更新数据Thread thread1 = new Thread(()->{demo.updateDebitCard(new BigDecimal(-50));});Thread thread2 = new Thread(()->{demo.updateDebitCard(new BigDecimal(-20));});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e){e.printStackTrace();}System.out.println("最终版本号:"+ demo.getVersion());System.out.println("最终数据:debitCard= "+ demo.getDebitCard().toString());}
}

输出结果:

数据更新成功,当前版本号:2,数据内容 debitCard= DebitCard{account='zhangsan', amount='80'}
数据更新成功,当前版本号:2,数据内容 debitCard= DebitCard{account='zhangsan', amount='80'}
数据更新失败,版本号已变化
最终版本号:2
最终数据:debitCard= DebitCard{account='zhangsan', amount='80'} 

        在上面的示例代码中,我们使用了AtomicInteger类来实现版本号的自增操作,并通过比较版本号来判断数据是否被其他线程修改过。如果版本号未变化,则可以进行更新操作;如果版本号已变化,则更新操作失败。

(2)CAS机制

        CAS机制即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  1. 需要读写的内存值 V
  2. 进行比较的值 A
  3. 拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值 B 来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

package com.example.demo;import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;import static java.util.concurrent.ThreadLocalRandom.current;public class AtomicReferenceExample {private static AtomicReference<DebitCard> debitCardAtomicReference = new AtomicReference<>(new DebitCard("zhangsan", new BigDecimal(0)));public static void main(String[] args){for (int i =0; i<10; i++){new Thread("T-"+i){@Overridepublic void run(){DebitCard dc;DebitCard newDebitCard;do {dc = debitCardAtomicReference.get();newDebitCard = new DebitCard(dc.getAccount(), dc.getAmount().add(new BigDecimal(10)));// 循环检测尝试获取锁} while (!debitCardAtomicReference.compareAndSet(dc, newDebitCard));System.out.println(Thread.currentThread().getName()+"=" + newDebitCard);try {TimeUnit.MILLISECONDS.sleep(current().nextInt(20));} catch (InterruptedException e) {e.printStackTrace();}}}.start();}}}

输出结果:

T-0=DebitCard{account='zhangsan', amount='10'}
T-6=DebitCard{account='zhangsan', amount='50'}
T-2=DebitCard{account='zhangsan', amount='40'}
T-3=DebitCard{account='zhangsan', amount='30'}
T-1=DebitCard{account='zhangsan', amount='20'}
T-4=DebitCard{account='zhangsan', amount='70'}
T-8=DebitCard{account='zhangsan', amount='60'}
T-9=DebitCard{account='zhangsan', amount='100'}
T-7=DebitCard{account='zhangsan', amount='90'}
T-5=DebitCard{account='zhangsan', amount='80'} 

CAS虽然很高效,但是它也存在三大问题:
        1)ABA问题

        CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

        JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。

        2)循环时间长开销大

        CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

        3)只能保证一个共享变量的原子操作

        对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

        Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

        需要注意的是,乐观锁并不能保证绝对的并发安全,因为在更新数据的过程中,可能会有其他线程修改了数据。因此,在实际应用中,还需要结合其他的并发控制手段来确保数据的一致性和安全性。 

四、优缺点和适用场景

1、功能限制

        与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

        例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized 则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

2、竞争激烈程度

        如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

        当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。

        当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

        悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

        乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/582270.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CNN实现对手写字体的迭代

导入库 import torchvision import torch from torchvision.transforms import ToTensor from torch import nn import matplotlib.pyplot as plt 导入手写字体数据 train_dstorchvision.datasets.MNIST(data/,trainTrue,transformToTensor(),downloadTrue) test_dstorchvis…

Windows实现MySQL5.7主从复制(详细版)

使用免安装版本&#xff08;官网下载地址&#xff09; 在Windows上安装两种MySQL服务并同时开启服务 1.下载配置 打开解压文件所在位置&#xff0c;就新建一个配置文件my.ini。 2.主库安装 主库的my.ini配置文件如下&#xff1a; [mysqld] #设置主库端口&#xff0c;注意须是…

uniapp开发移动端遇到的问题记录

1. 键盘弹起时页面整体上移问题 很常见但我解决过程中遇到了很多问题 我的键盘没有遮盖到输入框&#xff0c;但手机键盘弹起后&#xff0c;form部分会整体上移一点&#xff0c;并且底部的操作也会弹到键盘上方 网上写得很复杂&#xff0c;什么动态赋值高度balabala。看到有一…

英文vos安装,vos3000 web3.0

英文vos安装&#xff0c;vos3000英文软件工具安装&#xff0c;Web V3.0是一个安全且强大的批发 VOIP 计费解决方案&#xff0c;具有新的 Web 界面和无与伦比的令人惊叹的新功能。现在使用移动应用程序或网络浏览器控制您的 VOS3000 VOIP 计费服务器 yumsed -i "s|enabled…

使用pytorch搭建ResNeXt并基于迁移学习训练

冻结除最后全连接层以外的所有权重&#xff0c;只去单独训练它最后一层的的权重&#xff0c;这个方法&#xff0c;冻结了所有网络的权重。 for param in net.parameters():param.requires_grad False

测试熟悉新技术

一&#xff1a;redis 1.定义 简单来说 redis 就是key-value 类型的数据库&#xff0c;不过与传统数据库不同的是 redis 的数据是存在内存中的&#xff0c;所以读写速度非常快&#xff0c;因此 redis 被广泛应用于缓存方向,它有5种基本数据类型&#xff1a; String&#xff08;…

C语言之指针和函数

目录 作为函数参数的指针 二值互换 scanf函数和指针 指针的类型 空指针 标量型 在C语言程序中&#xff0c;指针的一个重要作用就是作为函数参数使用&#xff0c;下面我们就来学习作为函数参数的指针的相关内容。 作为函数参数的指针 假如我有一个神奇的能力&#xff0c;能…

win32 WM_MENUSELECT消息学习

之前写了一些win32的程序&#xff0c;处理菜单单击都是处理WM_COMMAND消息&#xff0c;通过 LOWORD(wParam) 获取菜单ID&#xff0c;判断单击的是哪个菜单项&#xff1b; 还有一些其他菜单消息&#xff1b; 当在菜单项中移动光标或鼠标&#xff0c;程序会收到许多WM_MENUSELEC…

stm32f103系统滴答定时器

简介 SysTick定时器, 是一个24位的定时器, 只能向下计数(n->0). 从RELOAD寄存器中自动重装载定时初值. 24位可以粗略约等于16M个数字. 寄存器 库函数配置系统定时器中断 选择8分频还是系统时钟源. 系统滴答次数, 也就是重装载值, 每次结束都会产生一个中断. 使用系统时钟…

12.28

1 有道云笔记 2 #include <iostream>using namespace std;class person { private:int *age;string &name; public:person(int age,string name):age(new int(age)),name(name){}~person(){delete age;}person(const person &other):age(new int(*(other.age)…

软件测试/测试开发丨学习笔记之Python运算符

运算符的作用 Python基础语法的内容通常表示不同数据或变量之间的关系 算数运算符 运算符描述加-减*乘/除%取模**幂//取整除 取模与取余区别 概念上&#xff1a;取模是计算机术语&#xff0c;取余属于数学概念&#xff1b; 结果上&#xff1a;当同号的两个数相除&#xff…

程序员如何高效学习技术?

我们相信努力学习一定会有收获&#xff0c;但是方法不当&#xff0c;既让人身心疲惫&#xff0c;也没有切实的回报。 不少朋友每天都阅读技术文章&#xff0c;但是第二天就忘干净了。工作中领导和同事都认可你的沟通和技术能力&#xff0c;但是跳槽面试却屡屡碰壁。面试官问技术…

系统学习新技术的心得体会

在当今快速发展的技术时代&#xff0c;持续学习和掌握新技术已经成为保持竞争力和创新能力的关键。系统学习新技术不仅仅是获取知识和技能&#xff0c;更是一种挑战自我、拓展视野和实现个人成长的过程。在这个过程中&#xff0c;我积累了一系列宝贵的心得体会。 首先&#xf…

EasyExcel多线程批量导出数据,动态表头,静态资源访问

1.导入依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version></dependency>2.建立实体 Data public class ActResultLogVO implements Serializable {private static…

关于MySql字段类型的实践总结

当字段为数值类型时应使用无符号UNSIGNED修饰 ALTER TABLE infoMODIFY COLUMN user_id int UNSIGNED NOT NULL; 当字段为varchar类型时应注意是否选择合适的字符集 例如存储一些范围值&#xff0c;数字英文字符时&#xff08;IP、生日、客户端标识等或以“,”分隔的数据&…

Google Chrome 现在会在后台扫描泄露的密码

谷歌表示&#xff0c;Chrome 安全检查功能将在后台运行&#xff0c;检查网络浏览器中保存的密码是否已被泄露。 如果桌面用户正在使用标记为危险的扩展程序&#xff08;从 Chrome Web Store 中删除&#xff09;、最新的 Chrome 版本&#xff0c;或者如果启用安全浏览来阻止 Go…

Dolphinscheduler-3.2.0 离线部署 伪集群模式

Dolphinscheduler-3.2.0(离线)伪集群模式 一、依赖(前置准备工作) 1.JDK&#xff1a;版本要求 JDK(1.8),安装并配置 JAVA_HOME 环境变量,并将其下的 bin 目录追加到PATH 环境变量中; 2.数据库&#xff1a;PostgreSQL(8.2.15) 或者MySQL(5.7),两者任选其一即可,如 MySQL 则需要…

我的NPI项目之行业黑话 -- 电子/机构/软件/认证

因为最近的NPI项目&#xff0c;参加了很多项目的会议&#xff0c;有电子/机构/软件/认证相关的各方面的专业词汇就出现了。这里我将之称为黑话&#xff0c;就是对我&#xff08;纯软件) 来说是黑盒的话。这里简单记录并用于理解。 EE有关&#xff1a; Layout&#xff0c;一直…

基于Qt之QChart 图表(优美的曲线图案例)

## 项目演示 平台:ubuntu18.04 Qt版本:QT5.14.2 源码位置GitCode:https://gitcode.com/m0_45463480/QCharts/tree/main ## QChart 图表 自从 Qt 发布以来,给跨平台的用户带来很多便利。在 Qt5.7 之前,Qt 在开源社区版本里没有 Qt Charts(自带的绘图组件库)。这使得像…

安卓开发学习笔记

Android发展历史 开发工具 Android Studio 安装工具 下载页面&#xff1a;下载 Android Studio 和应用工具 - Android 开发者 | Android Developers 安装步骤 要求代理&#xff0c;关闭就可以 下载SDK 完成后创建新项目 努力学习中~