14-Java锁的概述

14-锁的概述 乐观锁与悲观锁

​ 乐观锁与悲观锁是数据库中引入的名词,但是在并发包里也引入了类似的思想,在这里我们还是有必要需要了解一下。

​ 悲观锁指数据被外界修改持保守态度,认为数据会很容易被其他线程修改,所以在数据处理前先要对数据进行加锁,在整个数据处理中,使得数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即在数据库中,在对数据记录操作前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。

​ 下面我们看一个例子,看它如何使用悲观锁避免多线程同时对一个记录进行修改。

public int updateEntry(long id){ //使用悲观锁获取指定记录(1) EntryObject entry=query("select * from table1 where id=#{id} for update",id); //修改记录内容(2) String name=generatorName(entry); entry.setName(name); //update操作(3) int count=udpate("update table1 set name=#{name},age=#{age} where id=#{id}",entry); return count; }

​ 在如上代码中,假设updateEntry,query,update使用了事务切面的方法,并且事务传播性被设置为required。执行updateEntry方法时如果上层调用方法里面没有开启事务,则会即时开启一个事务,然后执行代码(1),代码(1)调用query()方法,其根据Id查询出一条记录来,由于事务传播是erquired所以执行query时没有开启新的事务,而是加入了updateEntry开启的事务,也就是在updateEntry方法执行完毕提交事务时,query才会被提交,也就是说记录的锁会持续到updateEntry执行结束。

​ 代码(2)则对获取到的记录进行修改,代码(3)把修改的新内容写入数据库,同样update方法也没有开启新的事务,而是加入了updateEntry的事务。也就是说updateEntry,query,update公用一个事务。

​ 当多个线程同时调用updateEntry方法,并且传递的是同一个id,只有一个线程执行代码(1)会成功,其他线程则会被阻塞挂起,这是因为在同一时间只能有一个线程可以获取到对应的锁,在获得锁的线程释放锁之前(updateEntry执行完毕,提交事务前),其他线程必须等待,也就是同一时间只有一个线程可以对该记录进行修改。

​ 乐观锁是相对悲观锁的,他认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在数据进行更新或者提交的时候才会对数据冲突与否进行检测。具体来说,根据update返回的行数让用户决定如何去做,将上面的例子改为使用乐观锁的代码如下。

public int updateEntry(long id){ //使用乐观锁获取指定记录(1) EntryObject entry=query("select * from table1 where id=#{id}",id); //修改字段内容(2) String name=generatorName(entry); entry.setName(name); //update操作(3) int count=update("update table1 set name=#{name},age=#{age},version=#{version}+1 where id=#{id} and version=#{version}",entry); }

​ 在如上代码中,当多个线程调用updateEntry方法并且传入相同的id时,多个线程可以同时执行代码(1)获取id对应的记录并放入到本地栈里面,然后可以同时执行代码(2)对自己栈上的记录进行修改,多个线程修改后各自的entry里面的属性都应该不一样了。然后多个线程可以同时执行代码(3),代码(3)中的update语句的where条件加入了version=#{version}条件,并且set语句多了verson=#{vesion}+1表达式,该表达式的意思是,如果数据库里id=#{id} and version=#{version}的记录存在,则更新version的值为原来的+1,这有点CAS操作的意思。

​ 加入多个线程同时执行updateEntry并传递相同的id,那么它们执行代码(1)时获取的entry是同一个,获取的entry的version值是相同的(这里假设version=0),当多个线程执行代码(3)时,由于update语句是原子性的,假如线程A执行update成功了,那么这时候id对应的记录的version就应该为1了,其他线程执行代码(3)更新时发现数据库里面已经没有了vesion=0的语句,所以会返回影响行号为0。在业务上根据返回为0就可以知道当先更新就没有更新成功,那么接下来就有两个办法,如果业务发现更新失败了,下面可以什么都不做,也可以选择重试,如果选择重试,则下面的updateEntry修改如下:

public boolean updateEntry(long id){ boolean result=false; int retryNum=5; while(retryNum>0){ //使用乐观锁获取指定记录(1.1) EntryObject entry=query("select * from table1 where id=#{id}",id); //修改字段内容(2.1) String name=generatorName(entry); entry.setName(name); //update操作(3.1) int count=update("update table1 set name=#{name},age=#{age},version=#{version}+1 where id=#{id} and version=#{version}",entry); if(count==1){ result=true; break; } retryNum--; } return result; }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzxgdx.html