Java中锁的类型

Java中锁的类型

      • 思想上的锁:乐观锁/悲观锁
        • 悲观锁
        • 乐观锁
        • 应用场景
      • 独占锁(排他锁)/ 共享锁
      • 可重入锁
      • 公平锁/非公平锁
      • 分段锁
      • 自旋锁
      • 偏向锁/轻量级锁/重量级锁
        • 偏向锁
        • 轻量级锁
        • 重量级锁

思想上的锁:乐观锁/悲观锁

悲观锁

总是假设资源的竞争十分激烈,每次访问数据的时候当前线程都会先加上一把锁,那么在该线程执行过程中,其他线程就不能访问数据并且处于阻塞状态,当该线程执行结束或者异常退出了,其他线程才可以去竞争锁,那么悲观锁就是采取一种保守的思想,提前上锁
Java中的 synchronized 和 ReentrantLock 锁都是一种悲观锁

乐观锁

总是只有某一个线程来访问数据,也就是资源竞争不激烈,那么乐观锁会先让所有线程对数据进行操作,只在最终提交对于数据的修改时才会进行判断,判断在本线程更新数据期间有没有其他线程对这个数据更新过,如果没有的话,就能成功的更新数据,如果有其他线程更新了这个数据,那么本线程更新数据失败
通过 CAS 机制和 版本号来实现,像 juc 包中的 atomic 类中几种原子性更新就是乐观锁的一种实现

应用场景

乐观锁适用于读操作大于写操作的场景中,因为写操作少,那么对于资源的竞争激烈程度低,如果使用悲观锁的话,反而因为锁的开销和内核态与用户态之间来回切换的开销,影响整体的吞吐量
悲观锁适用于写操作大于读操作的情况,当竞争情况激烈的时候,如果采取 CAS 这种乐观锁,那么会有大量的线程一直处于自旋的状态,不停的在读取内存中数据值和预期比较,那么就会白白浪费 CPU 资源

独占锁(排他锁)/ 共享锁

共享锁:指的是一把锁可以被多个线程获取
独占锁:一把锁只可以被一个线程获取
AQS(抽象同步队列管理器)中就提供了独占锁和共享锁的模板方法,其中 ReentrantLock 就是继承了 AQS 中独占锁的模板方法,synchronized 也是独占锁
CountDownLacth 就是共享锁,ReenReadWriteLock (读写锁)即是共享锁也是独占锁

可重入锁

ReentrantLock 和 synchronized 都是可重入锁,在进行锁获取的时候如果发现锁已经被线程获取,那么就会进行判断:当前线程是不是和锁持有的线程相同(其中 synchronized 可以通过锁对象头中的 Mark Word 里面保存的锁线程 ID 进行判断,ReentrantLock 通过指定的方法返回锁持有的线程,然后和当前线程进行一个比较),如果相同,那就就可以进行一个锁重入(synchronized 底层通过 monitorenter 这样一个锁计数器 + 1表示锁重入,ReentrantLock 通过表示锁状态变量 state + 1 来表示锁重入)

公平锁/非公平锁

公平锁:指的是按照锁请求的顺序来获取锁,也就是锁请求的顺序和锁获取的顺序相同
非公平锁:指的是不按照锁请求的顺序获取锁,而是随机的获取锁,当锁释放的时候,等待队列中的线程都有可能会获取到锁
非公平锁的优点:减少公平锁中因为要保证公平性所带来的线程上下文切换的开销,增加了吞吐量
缺点:容易产生饥饿的情况,先到的线程可能比后来的线程晚获取锁

分段锁

ConcurrentHashMap 在 JDK1.7版本中底层就采用的是 segment 数组 + HashEntry 数组 + 链表的形式组织数据,其中 segment 又继承 ReentrantLock ,将整体数据划分成不同的 segment 段,只会对每一段数据进行加锁,也就是锁住了一个 segment 元素,那么其他线程操作其他 segment 元素就不需要等待,大大提高了多线程下对于数据操作的并发度

自旋锁

如果多个线程同时竞争同一把锁,让后面请求获取锁的线程等待一会,也就是继续运行运行一会,看一下持有锁的线程是否很快的就能释放锁,如果锁很快就释放,那么后面的线程就能继续竞争锁,也就不涉及到唤醒操作,避免了线程从用户态切换到内核态操作的开销,但是如果持有锁的线程执行任务时间很长,那么自旋的线程就会一直的运行,这样就白白的浪费处理器的资源,造成性能浪费,所以自旋锁的自旋是有时间限制的,当自旋锁超过了一定的次数仍然没有获取到锁,那么就会将该线程阻塞,自旋的此时默认为十次,可以通过参数修改

偏向锁/轻量级锁/重量级锁

偏向锁

偏向锁可以通过虚拟机参数设置启动与否
适用场景:只有一个线程竞争同一把锁的情况,那么就通过偏向锁来消除同步
过程:当线程第一次请求获取锁对象时,虚拟机会将对象头中的偏向锁标志位设置为 1,表示进入偏向模式,同时通过 CAS 机制将获取到这个锁的线程 ID 记录到对象的 Mark Word 里面,成功之后,以后每次该线程要进入这个锁的同步块时,直接通过线程 ID 校验就可以进入代码块,无序进行锁的获取和释放
如果有第二个线程竞争获取该锁时,偏向锁就马上结束,紧接着升级为轻量级锁

轻量级锁

适用场景:只适用于两个线程竞争同一把锁的情况,也就是资源竞争不是很激烈,只要有超过两个以上的线程竞争,轻量级锁就会膨胀为重量级锁
过程:线程竞争锁资源,虚拟机会在自己的栈上创建一个 Lock Record 空间,用来储存锁对象 Mark Word 的拷贝,然后虚拟机尝试适用 CAS 机制将对象的 Mark Word 指向 Lock Record 指针,更新成功则表示该线程获取到锁,并且对象 Mark Word 中会表示为轻量级锁
如果 CAS 更新失败,则说明至少有一个线程和当前线程竞争锁,那么首先会检查对象的 Mark Word 是否指向当前线程栈中的 Lock Record 指针,如果是的话说明当前线程已经拥有这个对象锁,直接进入同步操作即可,否则说明这个锁对象已经被其他线程抢占了。当出现两个以上的线程竞争同一个锁对象,则轻量级模式就会膨胀为重量级锁,这个线程栈中的 Lock Record 指针就像是一个标签,哪个线程将自己的标签贴到锁对象上,就说明哪个线程获取到锁

重量级锁

存在阻塞和唤醒的情况,多个线程竞争同一把锁,只有一个线程能竞争到,其他线程要进入阻塞队列,等待该线程任务完成释放锁之后,阻塞的线程重新被唤醒去竞争锁,以此类推,此时就采取自旋来避免在一块时间很少的 synchronized 同步代码块内进行阻塞和唤醒操作,如果遇到同步时间较长这种情况,那就不得不使用 synchronized 的方法

你可能感兴趣的:(Java)