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;
}
如上代码使用retryNum设置更新失败后的重试次数,如果代码(3.1)执行后返回0,额说明代码(1.1)获取的记录已经被修改了,则循环一遍,重新通过代码(1.1)获取最新的数据,然后执行代码(3.1)尝试更新,这类似CAS的自旋操作,只是这里没有死循环,而是指定了尝试次数。
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交才锁定,所以不会产生任何死锁。
公平锁与非公平锁
根据线程获取锁的机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程获取到锁,而非公平锁在运行时闯入,也就是先来不一定先得。
ReentrantLock提供了公平与非公平锁的实现。
公平锁:ReentrantLock pairLock=new ReentrantLock(true);
非公平锁:ReentrantLock pairLock=new ReentrantLock(false);
如果构造函数不传入参数,就默认就是非公平锁。
假如线程A已经持有了锁,这时候线程B请求该锁会被挂起。当线程A释放锁后,假如当前也有线程C也需要获取到该锁,如果采用非公平锁方式,则根据线程调度策略,线程B和线程C两者之一可能获得该锁,这时候不需要任何其他干涉。而如果采用公平锁,则需要把C线程挂起,让线程B获取到该锁。在没有公平性需求的情况下尽量使用非公平锁,因为公平锁会带来性能消耗。
独占锁与共享锁
根据锁只能被单个线程持有还是多个线程共享哎,可以将锁分为独占锁和共享锁。
独占锁保证任何时候都只有一个线程得到锁,ReenTrantLock就是以独占的方式实现的,共享锁则可以同时由多个线程持有,,例如ReadWriteLock读写锁,它允许一个资源可以同时被多个线程进行读操作。
独占锁是一种悲观锁,由于每次访问资源都需要先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许同一时间只能允许同一个线程读取数据,其他线程必须等待当前线程释放锁后才能释放。共享锁是一种乐观锁,它放松了加锁的条件,允许多个线程同时进行读操作。
什么是可重入锁
当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不阻塞,那么我们说该锁是可重入的,也就是只要线程获取到了该锁,那么可以无限次数(在以后的文章中我们将知道严格来说是有限次)地进入被该锁锁住的代码。
下面看一个例子:
public class Hello{
public synchronized void helloA(){
System.out.println("helloA");
}
public synchronized void helloB(){
System.out.println("helloB");
}
}
在如上代码中,调用helloB方法前会先获取内置锁,然后打印输出,之后调用helloA方法,在调用前会先获取内置锁,如果内置锁是不可重入的,那么调用线程将会一直被阻塞。实际上,synchronized内部锁是可重入锁,可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁被目前哪个线程占用,然后关联一个计数器,一开始计数器为0,说明该锁没有被任何线程占用,当一个线程获取到该锁时,计数器的值就会变为1,这时其他线程再次来获取到该锁时发现锁的持有者不是自己就会被阻塞挂起。
当时当获取到了该锁的线程再次发现锁的拥有者仍然是自己的时候,就会把计数器+1,当释放锁后计数器-1,当计数器为0的时候,锁里面的线程标示被指为null,之时候被阻塞的线程会被唤醒来竞争获取该锁。
自旋锁
由于Java中的线程与操作系统中的线程一一对应,所以当一个线程获取锁失败后,会被切换到内核状态而挂起。当该线程获取到锁的时候有需要将其切换到内核状态而唤醒该线程,而从用户状态切换到内核状态开销是比较大的,在一定程度上会影响并发性能。自旋锁的原则是:当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认为10次,可以使用XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了该锁。如果尝试了指定次数后仍然没有获取到该锁则当前线程才会被挂起,由此看来自旋锁就是利用CPU时间来获取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了。
14-Java锁的概述的更多相关文章
- Android(java)学习笔记69:JDK5之后的Lock锁的概述和使用
1. Lock锁的概述: java.util.concurrent.locks,接口Lock 首先Lock是一个接口,Lock实现提供了比使用synchronized方法 和 同步代码块更为广泛的锁定 ...
- Android(java)学习笔记9:JDK5之后的Lock锁的概述和使用
1. Lock锁的概述: java.util.concurrent.locks,接口Lock 首先Lock是一个接口,Lock实现提供了比使用synchronized方法 和 同步代码块更为广泛的锁定 ...
- Java锁的种类
转载自:---->http://ifeve.com/java_lock_see/ Java锁的种类以及辨析锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchroniz ...
- JAVA 锁之 Synchronied
■ Java 锁 1. 锁的内存语义 锁可以让临界区互斥执行,还可以让释放锁的线程向同一个锁的线程发送消息 锁的释放要遵循 Happens-before 原则(锁规则:解锁必然发生在随后的加锁之前) ...
- java锁的种类以及辨析(转载)
java锁的种类以及辨析(一):自旋锁 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我 ...
- Java锁的种类以及辨析
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类 ...
- java 锁 简介(转)
转自 https://www.cnblogs.com/hustzzl/p/9343797.html 1. Java锁的种类 在笔者面试过程时,经常会被问到各种各样的锁,如乐观锁.读写锁等等,非常繁多, ...
- Java Reference简要概述
@(Java)[Reference] Java Reference简要概述 Reference对象封装了其它对象的引用,可以和普通的对象一样操作. Java提供了四种不同类型的引用,引用级别从高到低分 ...
- java 锁!
问题:如何实现死锁. 关键: 1 两个线程ta.tb 2 两个对象a.b 3 ta拥有a的锁,同时在这个锁定的过程中,需要b的锁:tb拥有b的锁,同时在这个锁定的过程中,需要a的锁: 关键的实现难点是 ...
- Java锁(一)之内存模型
想要了解Java锁机制.引发的线程安全问题以及数据一致性问题,有必要了解内存模型,机理机制了解清楚了,这些问题也就应声而解了. 一.主内存和工作内存 Java内存模型分为主内存和工作内存,所有的变量都 ...
随机推荐
- 【C语言】第5章 循环结构程序设计
第5章 循环结构程序设计 三种基本循环控制结构 使用while语句实现循环 先判断条件表达式,后执行循环体语句 while (循环条件表达式) { 循环体 } 用do-while语句实现循环 先无条件 ...
- Git入门配置
1.账户注册: 无论是GitHub还是码云(下称Gitee),要使用他们,我们都需要先注册账户,已有账户的可以跳过此步骤. Gitee GitHub 2.创建仓库: a.创建远程仓库 登入Gitee后 ...
- Excel 列名转int索引(C#版)
/// <summary> /// 获取Excel实际列索引 /// </summary> /// <param name="columnName"& ...
- Python - 面向对象编程 - self 参数
为什么要讲 self 参数 class PoloBlog: def __init__(self): ... def say(self): ... 在类里面,所有实例方法都需要加 self 参数,且排在 ...
- 基本ServletWEB项目
项目搭建 项目链接https://gitee.com/zhangjzm/smbms.git 前置知识,Servlet JSP 结构图 搭建maven web项目 1.搭建一个maven web项目 2 ...
- 第08课:GDB 实用调试技巧( 上)
本节课的核心内容: 将 print 打印结果显示完整 让被 GDB 调试的程序接收信号 函数明明存在,添加断点时却无效 将 print 打印结果显示完整 当使用 print 命令打印一个字符串或者字符 ...
- JS边角料: NodeJS+AutoJS+WebSocket+TamperMonkey实现局域网多端文字互传
---阅读时间约 7 分钟,复现时间约 15 分钟--- 由于之前一直在用的扩展 QPush 停止服务了,苦于一人凑齐了 Window, Android, Mac, ios 四种系统的设备,Apple ...
- 测试平台系列(55) 引入AceEditor(代码编辑器)
大家好,我是米洛,求三连!求关注测试开发坑货! 回顾 我们上一节已经写好了左侧数据表目录,今天继续完成sql编辑器的部分. 调研组件 monaco 因为我们的项目用的是React,市面上很多编辑器都是 ...
- vue-router路由钩子
路由跳转前后,需要做某些操作,这时就可以使用路由钩子来监听路由的变化. 接收三个参数: to: Route: 即将要进入的目标路由对象 from: Route: 当前导航正要离开的路由 next: F ...
- 【多线程】Android多线程学习笔记——线程池
Java线程池采用了享元设计模式,在系统中维持一定数量的线程,用于处理异步或并发需求,在平时处理异步或并发任务时被广泛使用.这里基于JDK1.8和Android28来整理一些关于线程池的知识点. 一. ...