最近有很多小伙伴给我留言。在分布式系统、线程并发、资源抢占的时代,“锁”逐渐变得非常重要。那么常见的锁有哪些呢?汤姆弟兄今天就和大家简单聊聊这个话题。1、悲观锁顾名思义,指的是对数据修改持保守态度,认为其他人也会修改数据。因此,在操作数据时,数据会被锁定,直到操作完成。大多数情况下,悲观锁是通过数据库的锁机制来实现的,以保证操作的最大程度的排他性。如果加锁时间过长,其他用户将长时间无法访问,影响程序的并发访问。同时也会对数据库的性能开销产生很大的影响,尤其是对于长事务来说,这样的开销往往是难以承受的。如果是单机系统,我们可以使用JAVA自带的synchronized关键字,通过在方法或者同步块中添加来锁定资源。如果是分布式系统,我们可以利用数据库本身的锁机制来实现。select*fromtablenamewhereid=#{id}forupdate在使用悲观锁的时候,一定要注意锁的级别。MySQLinnodb加锁时,只有明确指定主键或(索引字段)才会使用行锁;否则会执行表锁,锁住整张表,此时性能会很差。在使用悲观锁的时候,我们一定要关闭MySQL数据库的autocommit属性,因为mysql默认使用的是autocommit模式。悲观锁适用于写很多的场景,对并发性能要求不高。2.乐观锁。从字面意思就可以猜到乐观锁。操作数据的时候是很乐观的,认为其他人不会同时修改数据,所以不会加锁乐观锁。只有提交了更新,数据才会正式更新。检查冲突。如果发现冲突,将返回一条错误消息让用户决定如何去做,fail-fast机制。否则,执行此操作。分为数据读取、写入验证、数据写入三个阶段。如果是单机系统,我们可以基于JAVA的CAS来实现。CAS是一种原子操作,是借助于硬件的比较和交换来实现的。如果是分布式系统,我们可以在数据库表中增加一个版本号字段,比如:version。updatetableset...,version=version+1whereid=#{id}andversion=#{version}操作前先读取记录的版本号。更新的时候通过SQL语句比较版本号是否一致。如果一致,则更新数据。否则,将再次读取版本并重试上述操作。3、分布式锁JAVA中的synchronized和ReentrantLock都解决了单应用单机部署的资源互斥问题。随着业务的快速发展,当单体应用演变成分布式集群时,多线程、多进程分布在不同的机器上,原有的单机并发控制锁策略失效。这时候就需要引入分布式锁来解决跨机问题。互斥机制来控制对共享资源的访问。分布式锁需要具备哪些条件:与单机系统相同的资源互斥功能,这是锁的基础高性能的获取和释放锁的高可用和可重入有锁失效机制来防止deadlocks非阻塞,无论是否获取锁可以通过多种方式快速返回,基于数据库、Redis、Zookeeper等,下面是基于Redis的主流实现:Locking:SETkeyunique_value[EXseconds][PX毫秒][NX|XX]通过原子命令,如果执行成功返回1,表示加锁成功。注意:unique_value是客户端生成的唯一标识。需要特别注意区分来自不同客户端的锁操作。解锁首先要判断unique_value是否为锁定客户端,然后允许解锁删除。毕竟,我们不能删除其他客户端添加的锁。解锁:解锁有两个命令操作,需要Lua脚本保证原子性。//先比较unique_value是否相等,避免意外释放锁ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end依赖于Redis的高性能,Redis对分布式锁的实现也是目前主流的实现方式。但凡事都有优点和缺点。如果加锁的服务器宕机了,当slave节点还没有来得及备份数据的时候,并不是说其他??client也能拿到锁。为了解决这个问题,Redis官方设计了分布式锁Redlock。基本思路:让客户端和多个独立的Redis节点请求并行申请锁。如果超过半数的节点能够成功完成加锁操作,那么我们认为客户端已经成功获取到分布式锁,否则加锁失败。4、可重入锁可重入锁,也称为递归锁,是指当同一个线程正在调用外部方法获取锁时,再进入内部方法会自动获取锁。对象锁或类锁里面有一个计数器。线程每获得一次锁,计数器就+1;解锁时,计数器为-1。加锁次数对应解锁次数,加锁和解锁成对出现。Java中的ReentrantLock和synchronized都是可重入锁。可重入锁的一个优点是可以在一定程度上避免死锁。5、自旋锁自旋锁用于让当前线程在循环体中不断执行。当循环的条件被其他线程改变时,它可以进入临界区。自旋锁只是保持当前线程执行循环体,不会改变线程状态,所以响应速度更快。但是当线程数不断增加时,性能会明显下降,因为每个线程都需要执行,会占用CPU时间片。如果线程竞争不激烈,保持锁时间段。适合与自旋锁一起使用。自旋锁的缺点:可能造成死锁。可能使用CPU时间过长。我们可以设置循环时间或循环次数。当超过阈值时,线程会进入阻塞状态,防止线程长时间占用CPU资源。JUC并发包中的CAS使用自旋锁,compareAndSet是CAS操作的核心,底层使用Unsafe对象实现。publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;做{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));returnvar5;}如果var1对象在内存中的var2字段值等于预期的var5,则更新position为新值(var5+var4),否则什么也不做,不断重试,直到操作成功。CAS包含Compare和Swap操作,如何保证原子性?CAS是CPU支持的原子操作,其原子性是在硬件层面进行控制的。特别是CAS会导致ABA问题,我们可以通过引入递增的版本号来解决。6.排他锁排他锁,也有人叫排他锁。无论读还是写操作,只有一个线程可以获得锁,其他线程被阻塞。缺点:读操作不修改数据,大多数系统读多写少。如果读与读互斥,系统的性能会大大降低。下面的共享锁会解决这个问题。Java中的ReentrantLock和synchronized都是独占锁。7、共享锁共享锁允许多个线程同时持有锁,一般用于读锁。读锁的共享锁可以保证并发读非常高效。读与写、写与读、写与写是互斥的。独占锁和共享锁也是通过AQS实现的,通过实现不同的方法来实现独占或者共享。ReentrantReadWriteLock,它的读锁是共享锁,它的写锁是排它锁。8.读锁/写锁如果一个资源是读操作,多个线程不会互相影响,可以通过加读锁实现共享。如果有修改动作,为了保证数据的并发安全,此时只有一个线程可以获得锁,我们称之为写锁。读读共享;而读写、写读和写写是互斥的。像Java中的ReentrantReadWriteLock就是一种读写锁。9、公平锁/非公平锁公平锁:多个线程按照申请锁的顺序获取锁,所有线程在队列中排队,公平先来先获取的原则。优点:所有线程都能拿到资源,不会饿死在队列里。缺点:吞吐量会下降很多。除了队列中的第一个线程,其他线程都会被阻塞,CPU唤醒下一个被阻塞的线程会有系统开销。非公平锁:多个线程不按照申请锁的先后顺序获取锁,而是直接通过同时跳入队列的方式尝试获取锁。到了(跳队成功),直接获取锁。优点:可以减少CPU唤醒线程的开销,整体吞吐效率会更高。缺点:可能导致队列中排队的线程长时间获取不到锁或者长时间获取不到锁,饿死。对于Java多线程并发操作,我们的大部分操作锁都是基于Sync本身实现的,但是Sync本身是ReentrantLock的一个内部类,Sync继承了AbstractQueuedSynchronizer。像ReentrantLock默认是非公平锁,我们可以在构造函数中传入true来创建公平锁。公共ReentrantLock(布尔公平){同步=公平?newFairSync():newNonfairSync();}10.Interruptiblelock/non-interruptiblelockInterruptiblelock:指线程在阻塞等待自阻塞状态时,由于没有获取到锁而可以被中断。不可中断锁:反之,如果锁被其他线程获取到,当前线程只能阻塞等待。如果持有锁的线程从不释放锁,其他想要获取锁的线程将一直被阻塞。内置锁synchronized是不可中断锁,而ReentrantLock是可中断锁。ReentrantLock获取锁的方式有3种:lock(),如果获取到锁,会立即返回,如果有其他线程持有锁,则当前线程会一直阻塞,直到该线程获取到锁。如果获得锁,tryLock()立即返回true,如果另一个线程正在持有锁,则立即返回false。tryLock(longtimeout,TimeUnitunit),如果获取到锁,则立即返回true。如果其他线程持有锁,它将等待参数给定的时间。在等待过程中,如果获取到锁,则返回true。如果等待超时,则返回false。lockInterruptibly(),获取到锁后立即返回;如果未获取到锁,则线程将被阻塞,直到获取到锁或线程被另一个线程中断。更多:https://github.com/aalansehaiyang/p-java-proof/blob/master/resource/17.md。11.分段锁分段锁其实是一种锁设计,目的是细化锁的粒度,而不是特定的锁。对于ConcurrentHashMap,它的并发是通过分段锁的形式实现的高效并发操作。ConcurrentHashMap中的段锁称为Segment,类似于HashMap(JDK7中HashMap的实现)的结构,即内部有一个Entry数组,数组中的每个元素是一个链表;它也是一个ReentrantLock(SegmentInheritedfromReentrantLock)。当需要放元素的时候,不是锁定整个HashMap,而是先通过hashcode知道放哪个段,然后锁定段,所以多线程放的时候,只要不放在同一个中段,支持并行插入。12、锁升级(无锁|偏向锁|轻量级锁|重量级锁)在JDK1.6之前,synchronized还是重量级锁,效率比较低。但是在JDK1.6之后,JVM为了提高锁获取和释放的效率,对synchronized进行了优化,引入了偏向锁和轻量级锁。从那以后,出现了四种锁:无锁、偏向锁、轻量级锁、重量级锁。这四种状态会随着比赛形势逐渐升级,无法降级。Lock-free和lock-free不锁资源。所有线程都可以访问和修改同一个资源,但同时只能有一个线程修改成功。也就是我们常说的乐观锁。偏向锁偏向于第一个访问锁的线程。synchronized代码块第一次执行时,通过CAS修改对象头中的锁标志,锁对象变成偏向锁。当线程访问同步代码块并获得锁时,它会将偏向锁的线程ID存储在MarkWord中。当线程进入和退出同步块时,不再通过CAS操作加锁和解锁,而是检测MarkWord中是否存在指向当前线程的偏向锁。轻量级锁的获取和释放依赖多条CAS原子指令,而偏向锁在替换ThreadID时只需要依赖一条CAS原子指令。执行完同步代码块后,线程不会主动释放偏向锁。当线程第二次执行同步代码块时,线程会判断持有锁的线程是否是自己(持有锁的线程ID也在对象头中),如果是则继续正常执行.由于之前没有释放过锁,所以这里不需要重新加锁,几乎没有偏向锁的额外开销,性能极高。只有当其他线程试图竞争偏向锁时,持有偏向锁的线程才会释放锁,该线程不会主动释放偏向锁。关于偏向锁的取消,需要等待全局安全点,即当某个时间点没有字节码正在执行时,会先挂起拥有偏向锁的线程,然后判断锁定对象是否被锁定。如果线程不活跃,设置对象头为无锁状态,取消偏向锁,返回无锁(标志位为01)或轻量级锁(标志位为00)状态。偏向锁是指当一段同步代码始终被同一个线程访问时,即多个线程之间不存在竞争时,则该线程在后续访问时会自动获取锁,从而降低获取锁的成本.轻量级锁当前锁是偏向锁。这时同时有多个线程在竞争锁,偏向锁就会升级为轻量级锁。轻量级锁认为,虽然存在竞争,但理想情况下竞争程度很低,通过自旋获取锁。轻量级锁的获取有两种情况:关闭偏向锁功能时。多个线程竞争偏向锁导致偏向锁升级为轻量级锁。一旦第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。在轻量级锁状态下,锁的竞争还在继续,没有抢到锁的线程会自旋,不断循环判断能否成功获取到锁。获取锁的操作其实就是通过CAS修改对象头中的锁标志。首先比较当前的锁标志是否为“已释放”,如果是则设置为“已锁定”。这个过程是原子的。如果锁被抢了,那么线程将当前的锁持有者信息修改为自己。重量级锁如果线程竞争很激烈,线程自旋超过一定次数(默认循环10次,可以通过虚拟机参数更改),轻量级锁升级为重量级锁(还是CAS修改锁标志位,但不修改持有锁的线程ID),当后续线程尝试获取锁,发现占用的锁是重量级锁时,直接挂起自己(而不是忙等待),等待未来的唤醒。重量级锁是指当一个线程获取到锁时,所有其他等待获取锁的线程都会被阻塞。总之,所有的控制权都交给了操作系统,操作系统负责线程间的调度,改变线程的状态。而这样会频繁的切换线程运行状态,挂起和唤醒线程,从而消耗大量的系统资源。13、锁优化技术(锁粗化、锁淘汰)锁粗化告诉我们,任何事物都有一个极限。在某些情况下,我们希望将多个锁请求合并为一个请求,以在短时间内减少大量的锁。请求、同步、释放带来的性能损失。例如:有一个循环体,内部。for(inti=0;i
