锁的策略
在加锁过程中,处理冲突的过程中,设计到有一些不同的处理方式,这些处理方式,并非是JAVA独有的.
第一组:乐观锁和悲观锁
乐观锁:在加锁之前,预估当前出现锁冲突的概率不大,因此在加锁的时候就不太会做太多的工作.
加锁过程做的事情比较少,加锁的速度可能就越快,但是更容易引入一些其他问题(但是可能会消耗更多的cpu资源)
悲观锁:在加锁之前,预估当前锁冲突出现的概率比较大,因此在加锁的时候,就会做更多的工作.
做的事情更多,加锁的速度可能就会更慢,但是整个过程中不容易出现其他问题.
第二组:轻量级锁和重量级锁
轻量级锁,加锁的开销小,加锁的速度更快=>轻量级锁,一般就是乐观锁
重量级锁:加锁的开销更大,加锁速度更慢=>重量级锁,一般就是悲观锁
轻量重量,是加锁之后,对结果的评价.
悲观乐观,是加锁之前,对未发生的事情进行的预估.
整体来说,这两种角度,描述的是同一个事情.
第三组:自旋锁和挂起等待锁
自旋锁就是轻量级锁的一种典型实现,进行加锁的时候,搭配一个while循坏,如果加锁成功,自然循环结束.
如果加锁不成功,不是阻塞放弃cpu,而是进行下一次循环,再次尝试获取锁.
这个反复快速执行的过程,就称为"自旋",一旦其他线程释放了锁,就能第一时间拿到锁,同时,这样的锁,也是乐观锁,使用自选锁的前提,就是预期锁冲突的概率不大,其他线程释放了锁,就能第一时间拿到锁.万一,当前加锁的线程特别多,就会白白浪费CPU资源
挂起等待锁就是重量级锁(同时也是一种悲观锁)的一种典型实现,进行挂起等待的时候,就需要内核调度器介入了,这一块需要完成的操作就更多了,真正获取到锁要花的时间也就更多了.这个锁可以适用于锁冲突激烈的情况.
挂起等待锁的线程一旦进入到阻塞中去,就需要重新参与系统的调度,什么时候能够调度上cpu就是未知数了,但是好处就是这个阻塞过程中把cpu资源让出来了,可以干点别的事情.
自旋锁加锁消耗时间比较短,这边一释放,我就立即加上去,但是缺点就是比较消耗cpu,自旋锁在预估了锁竞争不激烈的时候才能使用
synchronized锁具有自适应能力
synch在某些情况下就是乐观锁/轻量级锁/自旋锁,某些情况下就是悲观锁/重量锁/挂起等待锁.
内部会自动评估当前锁冲突的激烈程度.
如果当前锁冲突的激烈程度不大,就处于乐观/轻量级/自旋锁.
如果当前锁冲突的激烈程度很大,就处于悲观锁/重量级锁/挂起等待锁.
第四组:普通互斥锁和读写锁
普通互斥锁就如同synchronized之类的锁
读写锁,把加锁分成两种情况
1.加读锁
2.加写锁
读锁和读锁之间,不会出现锁冲突(不会阻塞)
读锁和写锁之间,会出现锁冲突(会阻塞)
写锁和写锁之间,会出现锁冲突(会阻塞)
一个线程加速读锁的时候,另一个线程只能读,不能写
一个线程加写锁的时候,另一个线程,不能写,也不能读
引入读写锁的原因:
如果两个线程在读,本身就是线程安全的,不需要进行互斥.
如果使用synchronized这种方式进行加锁,两个线程读,也会产生互斥,产生阻塞(对于性能有一定的损失)
完全给读操作不加锁,也不行,一个线程读,一个线程写,--可能就会读到写了一半的数据```
读写锁就可以很好的解决上述问题.
实际开发中,读操作本身就是非常频繁的,非常常见的,读写锁就能把这些并发读之间的锁冲突的开销就给省下了,就对于性能提升很明显了.
第五公平锁和非公平锁
有点类似于线程饿死
我们站在系统原生的锁的角度,就是非公平锁,系统的调度本身就是无序的随机的~~
上一个线程释放了锁之后,接下来唤醒哪个线程?就不好说了.
java中的synchronized锁也是非公平得,
要想实现公平锁,就需要引入额外的数据结构(引入队列,记录每个线程的先后顺序),才能实现公平锁
使用公平锁,天然就可以避免线程饿死的问题
第六可重入锁和不可重入锁
一个线程针对一把锁,连续加锁两次,不会死锁,就是可重入锁,会死锁,就是不可重入锁.
synchronized锁就是可重入锁,系统自带的锁就是不可重入锁,
可重入锁中需要记录持有锁的线程是谁,加锁的次数的计数.
synchronized锁
1.乐观/悲观自适应锁
2.轻量级/重量级自适应
3.自旋锁/挂起等待锁自适应
4.不是读写锁
5.非公平锁
6.可重入锁
对于系统原生的锁(Linux提供的mutex锁)
1.悲观锁
2.重量级锁
3.挂起等待锁
4.不是读写锁
5.非公平锁
6.不可重入锁
synchronized锁的"自适应性"
1.偏向阶段
核心思想,"懒汉模式",能不加锁,就不加锁,能晚加锁,就晚加锁.所谓的偏向锁,并非真的加锁了,而只是做了一个非常轻量的标记~~
搞暧昧,就是所谓的偏向锁~~只是做一个标记,没有真加锁(也不会有互斥)
一旦有其他线程,来和我竞争这个锁,就在另一个线程之前,先把锁获取到~~从偏向锁升级到轻量级锁(真加锁,就有互斥了)
如果在这个阶段,要是没有人来竞争,整个过程就把加锁这样的操作就完全省略了.
(假设没有线程来竞争)
非必要不加锁,在遇到竞争的情况下,偏向锁没有提高效率,但是如果在没有竞争的情况下,偏向锁就大幅度的提高了效率,偏向锁的意义还是很大的.
/**偏向锁->轻量级锁 这个过程 不涉及解锁,能够确保持有偏向锁状态的线程肯定能先拿到锁
/**偏向锁标记,是对象头里的一个属性,每个锁对象都有自己的这个标记.当这个锁对象首次被加锁的时候,先进入偏向锁.如果这个过程中,没有涉及到锁竞争,下次加锁还是先进入偏向锁.一旦这个过程中升级到轻量级锁,后续再对这个对象加锁,那都是轻量级锁(跳过了偏向锁了)
2.轻量级锁阶段
(假设有竞争,但是不多)
此处就是通过自旋锁的方式实现的
优势:另外的线程把锁释放了,就会第一时间拿到锁
劣势:消耗cpu
于此同时,synchronized内部也会统计当前这个锁对象,有多少个线程在参与竞争,这里当发现参与竞争的线程比较多了,就会进一步升级到重量级锁
/****对于自旋锁来说,如果同一个锁竞争者很多时,大量的线程都在自旋,整体cpu的消耗就很大了.
3.重量级锁阶段
此时拿不到锁的线程就不会继续自旋了,而是进入"阻塞等待"
就会让出"cpu".(不会使cpu占用率太高)
当当前线程释放锁的时候,就由系统随机唤醒一个线程来获取锁了.
/***到底多少个线程算多,这里具体要看jvm源码,有一个阈值,这里,我们应重点关注锁策略
/*****此处的锁只能进行升级,不能进行降级,这里的自适应性不是很准确
锁消除
也是synchronized中内置的优化策略
编译器优化的一种方式,编译器在编辑代码的时候,如果发现这个代码,不需要加锁,就会自动把锁给干掉.这里的优化是比较保守的,比如,就只有一个线程,在这一个线程里加锁了,或者加锁代码中,没有涉及到:成员变量的修改,就只是一些局部变量.都不需要加锁,(但是对于模棱两可的加锁,编译器也不确定,这里都不会消除)
锁消除,针对一眼看上去就完全不涉及线程安全问题的代码,能够把锁消除掉
偏向锁,运行起来才知道有没有锁冲突
锁粗化
会把多个细粒度的锁,合并成一个粗粒度的锁~~
synchronized{}大括号里包括的代码越少,救认为锁的粒度越细,包含的代码越多,就认为锁的粒度越粗.~~
通常情况下,是更偏向于让锁的粒度更细一点,更有利于多个线程并发执行的~~
但有的时候,是希望所得粒度粗点更好~~
第一个图每次加锁都可能涉及到阻塞,第二个图就是把三次细粒度的锁合并成一个粗粒度的锁了~~
粗化是为了提高效率.
小结:
synchronized背后的一些优化手段
1.锁升级,偏向锁到轻量级锁到重量级锁
2.锁消除策略,自动干掉不必要的锁
3.锁粗化,把多个细粒度的锁合并成一个粗粒度的锁,减少锁竞争的开销