Java中锁有多重分类方式,根据粒度可分为:重量锁、轻量锁、偏向锁、分段锁;根据锁获取公平性又分为:公平锁、非公平锁。根据策略又分为:乐观锁、悲观锁、自旋锁;根据不一样的分类还有:共享锁、独占锁、可重入锁、互斥锁等概念。java
Synchronized 是经过对象内部的一个叫作监视器锁(monitor)来实现的。可是监视器锁本质又是依赖于底层的操做系统的 Mutex Lock 来实现的。而操做系统实现线程之间的切换这就须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是为何Synchronized 效率低的缘由。程序员
所以,这种依赖于操做系统 Mutex Lock 所实现的锁咱们称之为
“重量级锁”。JDK 中对 Synchronized 作的种种优化,其核心都是为了减小这种重量级锁的使用。JDK1.6 之后,为了减小得到锁和释放锁所带来的性能消耗,提升性能,引入了“轻量级锁”和“偏向锁”。安全
“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的。可是,首先须要强调一点的是,轻量级锁并非用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁。多线程
随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁(可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。并发
Hotspot 的做者通过以往的研究发现大多数状况下锁不只不存在多线程竞争,并且老是由同一线程屡次得到。偏向锁的目的是在某个线程得到锁以后,消除这个线程锁重入(CAS)的开销,看起来让这个线程获得了偏护。引入偏向锁是为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径,由于轻量级锁的获取及释放依赖屡次 CAS 原子指令,而偏向锁只须要在置换ThreadID 的时候依赖一次 CAS 原子指令(因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能。框架
这并不是一中实际的锁,而是一中思想、或者说是一种策略;ConcurrentHashMap 是学习分段锁的最好实践。经过segment加锁,增长并发度、下降锁粒度。jvm
1.加锁是须要耗费性能和资源的,因此只在有并发安全要求的地方加锁,减小性能消耗,减小锁持有时间。
2.下降锁粒度。大对象会有不少对象进行访问,因此将其细分为多个小对象,这样能够减低粒度,增长并发度,ConcurrentHashMap就是个很好的例子。
3.锁分离,最明显的作法就是ReadWriteLock.根据操做功能进行分离,分红读锁与写锁。那么读写、写写都是互斥,而读读则是共享的,由于不会影响彼此结果。
4.一般状况下,为了保证多线程间的有效并发,会要求每一个线程持有锁的时间尽可能短,即在使用完公共资源后,应该当即释放锁。可是,凡事都有一个度,若是对同一个锁不停的进行请求、同步和释放,其自己也会消耗系统宝贵的资源,反而不利于性能的优化 。
5.锁消除是在编译器级别的事情。在即时编译器时,若是发现不可能被共享的对象,则能够消除这些对象的锁操做,多数是由于程序员编码不规范引发。ide
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,若是某个只读线程获取锁,则其余读线程都只能等待,这种状况下就限制了没必要要的并发性,由于读操做并不会影响数据的一致性。因此前面又说道ReadWriteLock进行锁分离,解决不要的并发问题。函数
共享锁则容许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,容许多个执行读操做的线程同时访问共享资源。读读是共享的,也是安全的。性能
所谓的公平是指在锁等待队列中获取到锁前后关系,先到先得的思想。优先队列中的线程获取锁。
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
为了提升性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,若是没有写锁的状况下,读是无阻塞的,在必定程度上提升了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 本身控制的,你只要上好相应的锁便可。
若是你的代码只读数据,能够不少人同时读,但不能同时写,那就上读锁。
若是你的代码修改数据,只能有一我的在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现
ReentrantReadWriteLock。
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。
java 中的乐观锁基本都是经过 CAS 操做实现的,CAS 是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁原理很是简单,若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁
的线程就不须要作内核态和用户态之间的切换进入阻塞挂起状态,它们只须要等一等(自旋),等持有锁的线程释放锁后便可当即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是须要消耗 cup 的,说白了就是让 cup 在作无用功,若是一直获取不到锁,那线程也不能一直占用 cup 自旋作无用功,因此须要设定一个自旋等待的最大时间。若是持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会致使其它争用锁的线程在最大等待时间内仍是获取不到锁,这时争用线程会中止自旋进入阻塞状态。
自旋锁尽量的减小线程的阻塞,这对于锁的竞争不激烈,且占用锁时间很是短的代码块来
说性能能大幅度的提高,由于自旋的消耗会小于线程阻塞挂起再唤醒的操做的消耗,这些操做会致使线程发生两次上下文切换!
可是若是锁的竞争激烈,或者持有锁的线程须要长时间占用锁执行同步块,这时候就不适合
使用自旋锁了,由于自旋锁在获取锁前一直都是占用 cpu 作无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会致使获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操做的消耗,其它须要 cup 的线程又不能获取到 cpu,形成 cpu 的浪费。因此这种状况下咱们要关闭自旋锁;
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁当即进行处理。可是如何去选择自旋的执行时间呢?若是自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响总体系统的性能。所以自旋的周期选的额外重要!
JVM 对于自旋周期的选择,jdk1.5 这个限度是必定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷状况作了较多的优化,若是平均负载小于 CPUs 则一直自旋,若是有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,若是正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,若是 CPU 处于节电模式则中止自旋,自旋时间的最坏状况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差别。
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;
本文里面讲的是广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。可重入锁,也叫作递归锁,指的是同一线程 外层函数得到锁以后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。
synchronized 它能够把任意一个非 NULL 的对象看成锁。他属于独占式的悲观锁,同时属于可重
入锁。
Synchronized 核心组件
1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
2) Contention List:竞争队列,全部请求锁的线程首先被放在这个竞争队列中;
3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
5) Owner:当前已经获取到所资源的线程被称为 Owner;
6) !Owner:当前释放锁的线程。