美食网站开发方案,王野天的照片,关于seo网站优化公司,石嘴山网站定制开发建设锁从不同的角度有不同的分类#xff0c;从线程是否需要锁住同步资源角度来分#xff0c;可以分为#xff1a;悲观锁和乐观锁。
一、悲观锁、乐观锁的定义 悲观锁就是我们常说到的锁。对于悲观锁来说#xff0c;他总是认为每次访问共享资源时会发生冲突#xff08;认为别的… 锁从不同的角度有不同的分类从线程是否需要锁住同步资源角度来分可以分为悲观锁和乐观锁。
一、悲观锁、乐观锁的定义 悲观锁就是我们常说到的锁。对于悲观锁来说他总是认为每次访问共享资源时会发生冲突认为别的线程会修改所以必须每次数据操作会上锁以保证临界区的程序同一时间只能有一个线程在执行共享资源同一时间只给一个线程使用其它线程阻塞用完后再把资源转让给其它线程。传统的关系型数据库里边就用到了很多这种锁机制比如行锁表锁等读锁写锁等都是在做操作之前先上锁。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。 由于悲观锁的频繁加锁因此导致了一些问题的出现比如在多线程竞争下频繁加锁、释放锁导致频繁的上下文切换和调度延时一个线程持有锁会导致其他线程进入阻塞状态从而引起性能问题。 乐观锁又称为“无锁”顾名思义它是乐观派。乐观锁总是假设对共享资源的访问不会产生冲突认为别的线程不会修改线程可以不停地执行无需加锁也无需等待。而一旦多个线程发生冲突乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。乐观锁适用于多读的应用类型这样可以提高吞吐量像数据库提供的类似于write_condition机制其实都是提供的乐观锁。在Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用乐观锁的一种实现方式CAS实现的。 由于无锁操作中没有锁的存在因此不可能出现死锁情况也就是说乐观锁天生免疫死锁。 乐观锁多用于“读多写少”的环境避免频繁加锁影响性能而悲观锁锁用于“写多读少”的环境避免频繁失败和重试影响性能。
二、实现方式 悲观锁的实现方式是加锁加锁既可以是对代码块加锁如Java的synchronized关键字也可以是对数据加锁。synchronized关键字和Lock的实现类都是悲观锁。 乐观锁的实现方式主要有两种CAS机制和版本号机制。乐观锁在Java中是通过使用无锁编程来实现最常采用的是CAS算法。 以Java中的自增操作 i 为例看一下悲观锁和CAS分别是如何保证线程安全的。在Java中自增操作不是原子操作它实际上包含三个独立的操作
从内存中取出 i 的当前值将 i 的值加 1将计算好的值放入到内存当中 因此如果并发执行自增操作可能导致计算结果的不准确。在下面的代码示例中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 i0; 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 。
操作员 A 此时将其读出 version1 并从其帐户余额中扣除 $50 $100-$50 。在操作员 A 操作的过程中操作员B 也读入此用户信息 version1 并从其帐户余额中扣除 $20 $100-$20 。操作员 A 完成了修改工作将数据版本号加一 version2 连同帐户扣除后余额 balance$50 提交至数据库更新此时由于提交数据版本大于数据库记录当前版本数据被更新数据库记录 version 更新为 2 。操作员 B 完成了操作也将版本号加一 version2 试图向数据库提交数据 balance$80 但此时比对数据库记录版本时发现操作员 B 提交的数据版本号为 2 数据库记录当前版本也为 2 不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略因此操作员 B 的提交被驳回。
这样就避免了操作员 B 用基于 version1 的旧数据修改的结果覆盖操作员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{accountzhangsan, amount80} 数据更新成功当前版本号2数据内容 debitCard DebitCard{accountzhangsan, amount80} 数据更新失败版本号已变化 最终版本号2 最终数据debitCard DebitCard{accountzhangsan, amount80} 在上面的示例代码中我们使用了AtomicInteger类来实现版本号的自增操作并通过比较版本号来判断数据是否被其他线程修改过。如果版本号未变化则可以进行更新操作如果版本号已变化则更新操作失败。
2CAS机制 CAS机制即 compare and swap比较与交换是一种有名的无锁算法。无锁编程即不使用锁的情况下实现多线程之间的变量同步也就是在没有线程被阻塞的情况下实现变量的同步所以也叫非阻塞同步Non-blocking Synchronization。CAS算法涉及到三个操作数
需要读写的内存值 V进行比较的值 A拟写入的新值 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 AtomicReferenceDebitCard debitCardAtomicReference new AtomicReference(new DebitCard(zhangsan, new BigDecimal(0)));public static void main(String[] args){for (int i 0; i10; 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-0DebitCard{accountzhangsan, amount10} T-6DebitCard{accountzhangsan, amount50} T-2DebitCard{accountzhangsan, amount40} T-3DebitCard{accountzhangsan, amount30} T-1DebitCard{accountzhangsan, amount20} T-4DebitCard{accountzhangsan, amount70} T-8DebitCard{accountzhangsan, amount60} T-9DebitCard{accountzhangsan, amount100} T-7DebitCard{accountzhangsan, amount90} T-5DebitCard{accountzhangsan, amount80} CAS虽然很高效但是它也存在三大问题 1ABA问题 CAS需要在操作值的时候检查内存值是否发生变化没有发生变化才会更新内存值。但是如果内存值原来是A后来变成了B然后又变成了A那么CAS进行检查时会发现值没有发生变化但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号每次变量更新的时候都把版本号加一这样变化过程就从“ABA”变成了“1A2B3A”。 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资源。 悲观锁适合写操作多的场景先加锁可以保证写操作时数据正确。 乐观锁适合读操作多的场景不加锁的特点能够使其读操作的性能大幅提升。