JAVAEE-9-锁的策略

锁的策略

 在加锁过程中,处理冲突的过程中,设计到有一些不同的处理方式,这些处理方式,并非是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.锁粗化,把多个细粒度的锁合并成一个粗粒度的锁,减少锁竞争的开销 

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值