锁的策略)

一、锁的分类

第一组:乐观锁和悲观锁

这是两种不同的锁的实现方式。

乐观锁:在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候就不会做太多的工作。

悲观锁:在加锁之前,预估到当前锁冲突出现的概率比较大,因此加锁的时候就会做更多的工作,做的事情更多,加锁的速度可能会更慢,但整个过程中不容易出现其他问题。

第二组:轻量级锁和重量级锁

轻量级锁:加锁的开销小,加锁的速度更快 => 一般为乐观锁

重量级锁:加锁的开销更大,加锁的速度更慢 => 一般为悲观锁

        轻量重量:加锁之后,对结果的评价

        乐观悲观:加锁之前,对未发生的事情进行的评估

        但这两种角度,描述的是同一个事情。

第三组:自旋锁和挂起等待锁

自旋锁:轻量级锁的一种典型实现。

        进行加锁的时候,搭配一个while循环,如果加锁成功,自然循环结束。如果加锁不成功,不是阻塞放弃cpu,而是进入下一次循环,再次尝试获取到锁 => 这个反复快速执行的过程,就称为“自旋”,一旦其他线程放弃了锁,就能第一时间拿到锁。

        同时,这样的自旋锁也是乐观锁

        使用自旋锁的前提,就是预期锁冲突的概率不大,其他线程释放了锁,就能第一时间拿到。但万一当前加锁的线程特别多,自旋的意义就不大,白白浪费了cpu。

挂起等待锁:是重量级锁的一种典型实现 => 进行挂起等待的时候,就需要内核调度器介入,这一块要完成的操作很多,真正获取到锁要花的时间就更多了

同时也是悲观锁 => 适用于锁冲突激烈的情况

第四组:普通互斥锁和读写锁

普通互斥锁:类似与synchronized操作涉及到的加锁和解锁。

读写锁:把加锁分成两种情况 1)加读锁        2)加写锁

        读锁和读锁之间,不会出现锁冲突(不会阻塞)=> 一个线程加读锁的时候,另外的线程只能读,不能写。

        写锁和写锁,写锁和读锁之间会出现锁冲突(会阻塞)=> 一个线程加写锁的时候,其他线程不能读,也不能写。

        引入读写锁,为了解决:如果两个线程读,本身就是线程安全的,不需要进行互斥。如果使用synchronized这种方式加锁,两个线程读,也会产生互斥,阻塞。(对性能有一定损失

        但完全给读操作不加锁,也不行,如果一个线程读,一个线程写 => 可能会读到写了一半的数据。

但读写锁可以解决上述问题:实际开发中,读操作本身就是非常频繁的,非常常见的。

读写锁就能把这些并发读之间的锁冲突的开锁给省略,对性能提升明显。

       标准库中,也提供了专门的类来实现读写锁synchronized不是读写锁

第五组:公平锁和非公平锁

系统原生的锁,属于“非公平锁” => 系统本身的调度就是无序的,随机的。

Java中的synchronized也是非公平锁

要想实现公平锁,就要引入额外的数据结构(队列,记录每个线程的先后顺序)

(“公平”遵守先来后到,使用公平锁,天然的可以避免线程饿死的问题

 第六组:可重入锁和不可重入锁           

一个线程针对一把锁,连续的加锁两次,不会死锁 => 可重入锁

 一个线程针对一把锁,连续的加锁两次,会死锁 => 不可重入锁

可重入锁中需要记录持有锁的线程是谁,加锁的次数的计数。

对于 synchronized来说

1.乐观锁/悲观锁自适应

2.轻量级锁/重量级锁自适应

3.自旋锁/挂起等待锁自适应

4.不是读写锁

5.非公平锁

6.可重入锁

对于系统原生的锁(Linux提供的mutex锁)

1.悲观锁

2.重量级锁

3.挂起等待锁

4.不是读写锁

5.非公平锁

6.不可重入锁

二、锁升级

当线程执行到synchronized的时候,如果这个对象当前未加锁的状态。

1.偏向锁阶段

核心思想:“懒汉模式”能不加锁就不加锁,能晚加锁,就晚加锁。

        偏向锁并非真的加锁,而是做了一个非常轻量的标记。一旦有其他的线程来竞争这个锁,就在另一个线程之前,先把锁获取到 => 从偏向锁就会升级到轻量级锁(真的加锁了,就有了互斥)。

非必要不加锁:在没有竞争的情况下,偏向锁就大幅度的提高了效率。

偏向锁标记,是对象头里的一个属性。

        每个锁对象里都有自己的这个标识,在这个锁对象首次被加锁时,先进入偏向锁,一旦过程中没有设计到锁竞争,下次加锁还是先进入偏向锁。

        只要这个过程中升级成了轻量级锁了,后续再针对这个对象加锁都为轻量级锁了。

2.轻量级锁阶段

此处通过自旋锁的方式来实现(有竞争,但不多)

优势:另外的线程把锁释放了,就会第一时间拿到锁

劣势:比较消耗cpu

同时,synchronized内部也会统计当前这个锁对象上有多少个线程再竞争,当发现参与竞争的线程比较多时 => 重量级锁

对于自旋锁,如果同一个锁竞争者多,大量线程都在自旋,整体cpu消耗很大

3.重量级锁阶段

此时拿不到锁的线程就不会继续自旋了,而是进入“阻塞等待”,让出cpu。当当前线程释放锁的时候,就由系统随机唤醒一个线程获取锁。

jvm源码中类似于定义一个配置项,作为“阈值” => 超过这个值 => 重量级锁

三、锁消除

也是synchronized中内置的优化策略

        编译器优化的一种方式,编译器编译代码的时候,如果发现这个代码不选哟加锁,就会自动把锁消除。

但这里的优化是比较保守的。锁消除 => 针对一眼看上去就完全不涉及线程安全问题的代码

偏向锁运行起来才知道有没有锁冲突。

四、锁粗化

会把多个细粒度的锁,合并成一个粗粒度的锁。

synchronized{},大括号中的代码越少,锁的粒度越细

        通常情况下,更偏向与让锁的粒度细一些,更有利于多个线程并发执行的,但有时希望锁粒度粗点更好。

synchronized背后涉及到很多“优化手段”

1.锁升级         偏向锁 => 轻量级锁 => 重量级锁

2.锁消除         自动消除不必要的锁

3.锁粗化         把多个细粒度锁合并成一个粗粒度锁,减少锁竞争开销

你可能感兴趣的:(synchronized,锁的分类)