当前位置: 首页 > 科技观察

Java并发编程之悲观锁和乐观锁机制

时间:2023-03-22 01:30:08 科技观察

一、资源与锁1、场景描述多个线程并发访问同一个资源。如果线程A在获取变量后修改了变量值,此时线程C也获取了变量值修改,两个线程同时并发处理一个变量,会造成并发问题。这种数据库的并行处理在实际业务开发中是很常见的。两个线程先后修改数据库的值,导致数据出现问题。很容易定位问题。2.演示案例privateCountAddcountAdd;publicAddThread01(CountAddcountAdd){this.countAdd=countAdd;}@Overridepublicvoidrun(){countAdd.countAdd(30);}}classAddThread02extendsThread{privateCountAddcountAdd;publicAddThread02(CountAddcountAdd){this.countAdd=countAdd;}@Overridepublicvoidrun(){countAdd.countAdd(10);}}classCountAdd{privateIntegercount=0;publicvoidcountAdd(Integernum){try{if(num==30){count=count+50;Thread.sleep(3000);}else{count=count+num;}System.out.println("num="+num+";count="+count);}catch(Exceptione){e.printStackTrace();}}}本例演示多线程并发修改count值,导致和Expect结果不一致,这是多线程并发下最常见的问题,尤其是y同时更新数据时。当并发发生时,需要使用一定的方法或策略来控制并发下数据读写的准确性。这叫做并发控制,实现并发控制的方法有很多种。最常见的方式是资源锁定。还有一个简单的实现策略:修改数据前先读取数据,修改时加上限制,保证修改的内容在此期间没有被修改。二、锁的概念介绍1.锁机制介绍并发编程中最关键的问题之一,多线程并发处理同一个资源,防止资源使用上的冲突一个关键的解决方案就是对资源加锁:多线程线程序列化访问。锁用于控制多个线程访问共享资源的方式。锁机制在任何给定时间只允许一个线程任务访问共享资源,实现了线程任务的同步和互斥。这是最理想但也是最差的表现方式。共享读锁机制允许多个任务并发访问资源。2.悲观锁悲观锁总是假设每次读取的数据都会被修改,所以需要对读取的数据进行加锁,具有很强的资源独占和独占特性。在整个数据处理过程中,数据是被锁住的,比如synchronized关键字的实现就是一种悲观机制。悲观锁的实现往往依赖于数据库提供的锁机制。只有数据库层提供的锁机制才能真正保证数据访问的独占性。否则,即使本系统实现了锁定机制,也不能保证外部系统不会修改数据,悲观锁主要分为共享读锁和排它写锁。独占锁的基本机制:也称为写锁,允许获取独占锁的事务更新数据,同时防止其他事务获取同一资源的共享读锁和独占锁。如果事务T给数据对象A加了写锁,事务T就可以读取A或者修改A,其他事务不能给A加任何锁,直到T释放对A的写锁。3.乐观锁相对于悲观锁,乐观锁采用更加宽松的锁定机制。大多数情况下,悲观锁是通过数据库的锁机制来实现的,以保证操作的最大程度的排他性。但是随之而来的是大量的数据库性能开销,尤其是长事务的开销是非常耗费资源的,而乐观锁机制在一定程度上解决了这个问题。乐观锁大多是基于数据版本记录机制实现的,为数据添加版本标识。在基于数据库表的版本解决方案中,一般是通过在数据库表中增加一个版本字段来实现的。读出数据的时候,一起读这个版本号,以后更新的时候,这个版本号加一。此时将提交数据的版本数据与数据库表中相应记录的当前版本信息进行比较。如果提交数据的版本号等于数据库表的当前版本号,则更新,否则认为是过期数据。乐观锁机制在高并发场景下可能会导致大量更新操作失败。乐观锁的实现是在策略层面:CAS(Compare-And-Swap)。当多个线程同时尝试使用CAS更新同一个变量时,只有一个线程可以成功更新变量的值,而其他线程则失败。失败的线程不会被挂起,但会被告知本次比赛失败。,可以再试一次。4、机制对比悲观锁的实现机制本身就是以性能损失为代价的。多线程争用、加锁和释放锁会造成更多的上下文切换和调度延迟,加锁机制会产生额外的开销。死锁的可能性增加,导致性能问题。虽然乐观锁会根据比较检测的手段来判断更新的数据是否发生了变化,但是并不能确定数据是否发生了变化。比如线程1读到的数据是A1,但是线程2操作A1的值变成了A2,然后又变成了A1,所以线程1的任务是不知道的。悲观锁每次修改数据都需要加锁,效率低,写入数据失败的概率比较低。比较适合写多读少的场景。乐观锁不是真的加锁,效率高。写入数据失败的概率比较高,容易出现业务异常。比较适合读多写少的场景。选择牺牲性能还是追求效率,要根据业务场景来判断。这种选择需要依靠经验判断。但是随着技术的迭代,数据库效率的提升,以及集群模式的出现,性能和效率还是可以兼得的。三、锁基本情况1、锁方法说明lock:执行一次锁获取,获取后立即返回;lockInterruptibly:获取锁的过程可以中断;tryLock:尝试以非阻塞方式获取锁,可以设置超时时间,获取成功则返回true,有利于线程状态监控;unlock:释放锁,清理线程状态;newCondition:获取等待通知组件,绑定当前锁;2.应用案例importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassLockThread02{publicstaticvoidmain(String[]args){LockNumlockNum=newLockNum();LockThreadlockThread1=newLockThread(lockNum,"TH1");LockThreadlockThread2=newLockThread(lockNum,"TH2");LockThreadlockThread3=newLockThread(lockNum,"TH3");lockThread1.start();lockThread2.start();lockThread3.start();}}classLockNum{privateLocklock=newReentrantLock();publicvoidgetNum(){lock.lock();try{for(inti=0;i<3;i++){System.out.println("ThreadName:"+Thread.currentThread().getName()+";i="+i);}}finally{lock.unlock();}}}classLockThreadextendsThread{privateLockNumlockNum;publicLockThread(LockNumlockNum,Stringname){this.lockNum=lockNum;super.setName(name);}@Overridepublicvoidrun(){lockNum.getNum();}}这里多线程是基于Lock机制,顺序执行任务。这是Lock的基本用法。各种API的详细解说下次再说。3、与基于synchronized的synchronized实现相比,锁机制是非常安全的,但是一旦线程失败,直接抛出异常,没有机会清理线程状态。显式使用Lock语法,最终可以在finally语句中释放锁,保持一个比较正常的线程状态。在获取锁的过程中,可以尝试获取锁,或者尝试获取锁一段时间。

猜你喜欢