这是两种不同的锁的实现方式。
乐观锁:在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候就不会做太多的工作。
悲观锁:在加锁之前,预估到当前锁冲突出现的概率比较大,因此加锁的时候就会做更多的工作,做的事情更多,加锁的速度可能会更慢,但整个过程中不容易出现其他问题。
轻量级锁:加锁的开销小,加锁的速度更快 => 一般为乐观锁
重量级锁:加锁的开销更大,加锁的速度更慢 => 一般为悲观锁
轻量重量:加锁之后,对结果的评价
乐观悲观:加锁之前,对未发生的事情进行的评估
但这两种角度,描述的是同一个事情。
自旋锁:轻量级锁的一种典型实现。
进行加锁的时候,搭配一个while循环,如果加锁成功,自然循环结束。如果加锁不成功,不是阻塞放弃cpu,而是进入下一次循环,再次尝试获取到锁 => 这个反复快速执行的过程,就称为“自旋”,一旦其他线程放弃了锁,就能第一时间拿到锁。
同时,这样的自旋锁也是乐观锁。
使用自旋锁的前提,就是预期锁冲突的概率不大,其他线程释放了锁,就能第一时间拿到。但万一当前加锁的线程特别多,自旋的意义就不大,白白浪费了cpu。
挂起等待锁:是重量级锁的一种典型实现 => 进行挂起等待的时候,就需要内核调度器介入,这一块要完成的操作很多,真正获取到锁要花的时间就更多了
同时也是悲观锁 => 适用于锁冲突激烈的情况
普通互斥锁:类似与synchronized操作涉及到的加锁和解锁。
读写锁:把加锁分成两种情况 1)加读锁 2)加写锁
读锁和读锁之间,不会出现锁冲突(不会阻塞)=> 一个线程加读锁的时候,另外的线程只能读,不能写。
写锁和写锁,写锁和读锁之间会出现锁冲突(会阻塞)=> 一个线程加写锁的时候,其他线程不能读,也不能写。
引入读写锁,为了解决:如果两个线程读,本身就是线程安全的,不需要进行互斥。如果使用synchronized这种方式加锁,两个线程读,也会产生互斥,阻塞。(对性能有一定损失)
但完全给读操作不加锁,也不行,如果一个线程读,一个线程写 => 可能会读到写了一半的数据。
但读写锁可以解决上述问题:实际开发中,读操作本身就是非常频繁的,非常常见的。
读写锁就能把这些并发读之间的锁冲突的开锁给省略,对性能提升明显。
标准库中,也提供了专门的类来实现读写锁(synchronized不是读写锁)
系统原生的锁,属于“非公平锁” => 系统本身的调度就是无序的,随机的。
Java中的synchronized也是非公平锁。
要想实现公平锁,就要引入额外的数据结构(队列,记录每个线程的先后顺序)
(“公平”遵守先来后到,使用公平锁,天然的可以避免线程饿死的问题)
一个线程针对一把锁,连续的加锁两次,不会死锁 => 可重入锁
一个线程针对一把锁,连续的加锁两次,会死锁 => 不可重入锁
可重入锁中需要记录持有锁的线程是谁,加锁的次数的计数。
1.乐观锁/悲观锁自适应
2.轻量级锁/重量级锁自适应
3.自旋锁/挂起等待锁自适应
4.不是读写锁
5.非公平锁
6.可重入锁
1.悲观锁
2.重量级锁
3.挂起等待锁
4.不是读写锁
5.非公平锁
6.不可重入锁
当线程执行到synchronized的时候,如果这个对象当前未加锁的状态。
核心思想:“懒汉模式”能不加锁就不加锁,能晚加锁,就晚加锁。
偏向锁并非真的加锁,而是做了一个非常轻量的标记。一旦有其他的线程来竞争这个锁,就在另一个线程之前,先把锁获取到 => 从偏向锁就会升级到轻量级锁(真的加锁了,就有了互斥)。
非必要不加锁:在没有竞争的情况下,偏向锁就大幅度的提高了效率。
偏向锁标记,是对象头里的一个属性。
每个锁对象里都有自己的这个标识,在这个锁对象首次被加锁时,先进入偏向锁,一旦过程中没有设计到锁竞争,下次加锁还是先进入偏向锁。
只要这个过程中升级成了轻量级锁了,后续再针对这个对象加锁都为轻量级锁了。
此处通过自旋锁的方式来实现(有竞争,但不多)
优势:另外的线程把锁释放了,就会第一时间拿到锁
劣势:比较消耗cpu
同时,synchronized内部也会统计当前这个锁对象上有多少个线程再竞争,当发现参与竞争的线程比较多时 => 重量级锁
对于自旋锁,如果同一个锁竞争者多,大量线程都在自旋,整体cpu消耗很大
此时拿不到锁的线程就不会继续自旋了,而是进入“阻塞等待”,让出cpu。当当前线程释放锁的时候,就由系统随机唤醒一个线程获取锁。
jvm源码中类似于定义一个配置项,作为“阈值” => 超过这个值 => 重量级锁
也是synchronized中内置的优化策略
编译器优化的一种方式,编译器编译代码的时候,如果发现这个代码不选哟加锁,就会自动把锁消除。
但这里的优化是比较保守的。锁消除 => 针对一眼看上去就完全不涉及线程安全问题的代码
偏向锁运行起来才知道有没有锁冲突。
会把多个细粒度的锁,合并成一个粗粒度的锁。
synchronized{},大括号中的代码越少,锁的粒度越细。
通常情况下,更偏向与让锁的粒度细一些,更有利于多个线程并发执行的,但有时希望锁粒度粗点更好。
1.锁升级 偏向锁 => 轻量级锁 => 重量级锁
2.锁消除 自动消除不必要的锁
3.锁粗化 把多个细粒度锁合并成一个粗粒度锁,减少锁竞争开销