一、CAS、乐观锁与悲观锁、数据库相关锁机制、分布式锁、偏向锁、轻量级锁、重量级锁、Monitor
CAS:在java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。
简单来说就是一种无锁算法,有三个操作数(内存值V,旧的预期值A,要修改的新值B。当且仅当A=V,将V修改为B否则什么都不做。)
CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新值,保证新的值总是基于最新的信息计算,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。优点:竞争不大时系统开销小。缺点:循环时间长开销大、只能保证一个共享变量的原子操作、ABA的问题
乐观锁与悲观锁:
1.乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
2.悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据时都会上锁,这样别人想拿这个数据时就会阻塞,直到他拿到锁(共享资源每次只给一个线程使用,用完后再把资源转让给其他线程。)
数据库相关锁机制:
数据库锁一般分为两类,一个是悲观锁,一个是乐观锁。
乐观锁一般是指用户自己实现的一种锁机制,悲观锁一般就是我们通常说的数据库锁机制,悲观锁主要表锁、行锁、页锁。
分布式锁:
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
分布式锁应该具备哪些条件:
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的三种实现方式:
基于数据库实现分布式锁;
2.基于缓存(Redis等)实现分布式锁;
3.基于Zookeeper实现分布式锁;
偏向锁:
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
缺点:
同样的,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。
轻量级锁:
自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。
当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。
缺点:
同自旋锁相似,如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。
重量级锁:
内置锁(内置锁是JVM提供的最便捷的线程同步工具,在代码块或方法声明上添加synchronized关键字即可使用内置锁)在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。
偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。
另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。
如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。
二、锁优化、锁消除、锁粗化、自旋锁、可重入锁、阻塞锁、死锁:
1.锁优化:减少锁的持有时间(将同步方法改成同步代码块)、减少锁的粒度(ConcurrentHashMap)、读写分离锁代替独占锁(ReadWriterLock)、锁分离(LinkedBlockingQueue)、锁粗化。推荐/xdecode/p/9137804.html
2.锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。然后带来一定的性能提升。
3.锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
4.自旋锁:又称非可重入锁。简单来说,若一个类中有两个方法A、B,则AB都有获得同一把锁,||当A调用时获得锁,在A方法锁还没有被释放时,调用B时,B无法获得锁。必须等A释放锁。首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。因为锁阻塞造成线程切换的时间与锁持有的时间相当,所以减少线程阻塞造成的线程切换,就能得到较大的性能提升。
推荐:/p/36eedeb3f912
5.可重入锁:简单来说,若一个类中有两个方法A、B,则AB都有获得同一把锁,||当A调用时获得锁,在A方法锁还没有被释放时,调用B时,B也获得锁。专业说法:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
6.阻塞锁:阻塞锁指改变了线程的运行状态,在java中,线程Thread有如下几种状态:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
7.死锁:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
三、死锁的原因:
竞争资源引起进程死锁竞争不可剥夺资源(可剥夺资源和不可剥夺资源)竞争临时资源进程推进顺序不当引起死锁
四、死锁的解决办法:
死锁预防。去破坏产生死锁的四个必要条件(互斥、占有且等待、不可抢占、循环等待)中的一个或者几个,来预防发生死锁。死锁避免。(在使用前进行判断,只允许不会产生死锁的进程申请资源)死锁检测和解除。(在检测到运行系统进入死锁,进行恢复。)CountDownLatch、 CyclicBarrier 和Semaphore三个类的使用和原理:
JAVA并发包中有三个类用于同步一批线程的行为,分别是CountDownLatch、Semaphore和CyclicBarrier。
1.CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。
2.Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。有点类似于锁的lock与 unlock过程。相对来说他也有两个主要的方法:
用于获取权限的acquire(),其底层实现与CountDownLatch.countdown()类似;
用于释放权限的release(),其底层实现与acquire()是一个互逆的过程。
推荐:/p/bb5105303d85
3.CyclicBarrier也是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。类似于CountDownLatch,它也是通过计数器来实现的。那么CyclicBarrier和CountDownLatch之间的区别在于:
CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作,描述的是1个线程或N个线程等待其他线程的关系。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作,描述的多个线程内部相互等待的关系。
CountDownLatch是一次性的,而CyclicBarrier则可以被重置而重复使用。
如果觉得《无招胜有招之锁》对你有帮助,请点赞、收藏,并留下你的观点哦!