java的锁
一、锁的类型
数据库的锁相关:读锁、写锁、表锁、行锁
1.1存储引擎:
InnoDB :支持主外键,行锁,只锁住某一行;不仅缓存索引还缓存真实数据,对内存要求高,内存大小对性能有影响;关注的是事务。
MyISAM:不支持主外键;表锁,即使操作一条记录会锁住整个表;只缓存索引,不缓存真实数;关注的是性能。
1.2锁分类
锁分类:分为读锁(共享锁、Share Locks 简称S锁)、写锁(独占锁、Exclusive Locks简称X锁)
读锁:事务A对对象T加S锁,其他事务也只能对T加S锁。多个事务可以同花读,但不能有写操作。知道A释放S锁
写锁:事务A对对象T加X锁后,其他事务不能对T加任何锁。
手动加表上的表锁
lock table 表名 read(/write),表名 read (/write);
查看表上加过的锁
show open tables;
释放表锁
unlock tables
如何分析表锁定———表锁的查看
show status like 'table%';Table_locks_immediate;
表示产生表级锁定的次数,表示可以立即获取锁的查询次数,每次加1; Table_locks_waited
表示出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁加1),此值很高说明存在着较严重的表级锁争用情况。
1.2.1 行锁
定义:行锁是建立在索引上的锁。如果没有索引,那么行锁就会自动锁全表,那就是表锁了。
注意:两个事务不能锁同一个索引;行锁会发生死锁,发生锁冲突几率低,并发高。
行锁分为读锁和写锁
行锁就是对一行或者多行进行加锁,比如
#对某一行进行加锁
begin;
select * from tableA where id="9" for update;
commit;
1.2.1.1间隙锁
当我们用范围条件而不是相等条件检索数据并请求共享或排他锁时,InnoDB会给复合条件的已有数据记录的索引项添加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙”;InnoDB会对这个“间隙”也加锁。这种锁叫间隙锁。
#行锁的查看
select status like 'innodb_row_lock%';
#会出现的结果
Innodb_row_lock_current_waits:当前正在等到锁定的数量
Innodb_row_lock_time:从系统启动到现在锁定总时间的长度
Innodb_row_lock_time_avg:每次等待所花平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数
重要的参数:等待总次数、等待总时长、等待平均时长。
1.2.1.2优化建议
- 尽量让所有的数据检索都通过索引来完成,避免无索引导师行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能的减少索引条件,避免间隙锁
- 尽量控制事务的大小,减少锁定资源量和时间长度
- 尽可能低级别的事务隔离
1.2.1.3死锁
- 不同表 相同记录 行锁冲突
- 相同表记录行锁冲突:事务A处理[1,2,3,4]事务B处理[3,5,6,8]
- 不同索引冲突:这种情况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就造成了死锁的可能性。
1.2.1.4行锁之手动加锁
- 共享锁(读锁):select * from table_name where … lock in share mode
- 排它锁(写锁): select * from table_name where … for update
- 注:serviceImple层:使用for update 一定要在方法上加上@Transactional(isolation = Isolation.READ_COMMITTED),当事务处理完后,for update 才会将行级锁解除。
1.2.2 表锁
定义:对整个表进行加锁。
#手动加表锁
lock table 表名 read(/write),表名 read (/write);
#查询表锁
show open tables;查询表上加过的锁
show status like 'table%';Table_locks_immediate;
#释放表锁
unlock tables;
二、悲观锁
总是假设最坏的情况,每次去拿数据的时候,都认为别人会修改。
所以>>> 每次拿数据的时候都会加锁,这样别人想拿这个数据的时候都会上锁。传统的关系型数据库中就用到了很多这种锁机制;比如行锁,表锁,读写锁。都是在操作之前先上锁。
java中的 ___synchronized___和___ReentrantLock___等独占锁就是悲观锁思想的实现
- synchronized:同步锁。保证在同一时刻,被修饰的代码块或方法只能有一个线程执行。保证安全。java解决并发问题的一种方法。
- ReentrantLock:和synchronized差不多
三、乐观锁
总是假设最好的情况,每次拿数据的时候,都认为别人不会修改,所以不会上锁 。但是在更新数据的时候会查看一下别人有没有更新这个数据。可以使用版本号机制和CAS算法实现。
适用于多读的类型。这样可以提高吞吐量。
像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
四、两种锁额常用场景
- 乐观锁:乐观锁适用于写比较少的情况下**(多读场景),**即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量
- 悲观锁:**多写的情况,**一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
五、两种锁的实现机制
### 5.1乐观锁:(版本号机制和CAS机制)
#### 5.1.1版本号机制
一般在数据表中加一个数据版本号version字段,表示数据被修改的次数。当数据被修改后,version加一。在事务A要更新数据的时候,读取数据的同时,也会读取version,在提交更新时,若事务A刚才读取到的值与之前读取到的version相同才更新。否则重试更新操作。
5.1.2CAS 算法(compare and swap)比较与交换
注:没有锁。
目的:在不使用锁的情况下实现进程的同步。也就是在没有线程被阻塞的情况下实现变量的同步。也叫做非阻塞同步(No-blocked-synchronization)
CAS设计三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
5.1.3乐观锁的缺点
- ABA问题 :一开始是A,更新成了B,后来又更新成了A。JDK 1.5 以后的
AtomicStampedReference 类
就提供了此种能力,其中的compareAndSet 方法
就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 循环时间开销大:自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作:CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。 JDK 1.5开始,提供了
AtomicReference类
来保证引用对象之间的原子性,你可以**把多个变量放在一个对象里来进行 CAS 操作.**所以我们可以使用锁或者利用AtomicReference类
把多个共享变量合并成一个共享变量来操作。
六、CAS与synchronized的使用场景
6.1 资源竞争少的(线程冲突较轻)
使用CAS方法较好。CAS基于硬件实现,不需要进入内核,不需要切换线程。性能更高
6.2资源竞争严重的(线程冲突严重)
使用synchronized较好。CAS的自旋概率较大。时间浪费。
自旋锁:https://blog.csdn.net/qq_34337272/article/details/81252853