多线程

锁策略

乐观锁和悲观锁

乐观锁在加锁之前预估当前锁冲突的概率不大,因此在进行加锁的时候就不会做太多的工作,加锁过程中做的事情比较少,加锁的速度可能就更快,但是更容易引入一些其他问题,效率快但是会消耗更多的cpu资源

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

 

轻量级锁和重量级锁

轻量级锁,加锁的开销比较小,加锁的速度更加快,也是乐观锁

重量级锁,加锁的开销比较大,加锁的速度更慢,也是悲观锁

自旋锁和挂起等待锁

自旋锁是轻量级锁的典型实现,进行加锁的时候搭配一个while循环如果加锁成功,循环结束,加锁不成功,不是进入阻塞放弃cpu,而是进行下一次循环,再次尝试获取锁,这个反复快速执行的过程就是自旋锁,使用自旋的前提就是锁冲突的概率不大,其他线程释放锁,就能第一时间拿到,加锁的线程特别多,自旋意义就不大

挂起等待锁是重量级锁的一种典型实现,进行加锁的时候搭配一个while循环如果加锁成功,循环结束,加锁不成功,进入阻塞等待主动放弃cpu,重新参与系统的调度,什么时候能够调度回cpu就不知道了

 

普通互斥锁:

类似于synchronized涉及到加锁和解锁

读写锁:

分为加读锁和加写锁

读锁和读锁之间不会起锁冲突(不会阻塞)

写锁和写锁之间会出现锁冲突(会阻塞)

读锁和写锁之间会出现锁冲突(会阻塞)

一个线程加读锁的时候,另一个线程只能读,不能写

一个线程加写锁的时候,另一个线程不能写也不能读

公平锁和非公平锁

遵循先来后到的规矩

可重入锁和不可重入锁

一个线程针对这把锁,连续加锁两次,不会死锁就是重入锁,synchronized是可重入锁

会死锁就是不可重入锁,系统自带的锁是不可重入锁

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

synchronized的工作原理

synchronized是能够自适应的锁

1.偏向锁

 核心思想就是懒汉模式,能不加锁就不加锁,能晚加锁就晚加锁,所谓的偏向锁,并非真的加锁而是做了一个非常轻量的标记

2.轻量级锁阶段

线程有竞争但是不多,通过自旋锁的方式来实现的,

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

劣势:对于cpu的消耗来说相对较高

synchronized的内部也会统计当前这个锁对象上有多少个线程参与锁竞争,锁的竞争多了就会升级成重量级锁

3.重量级锁阶段

线程多了,锁竞争多了,拿不到锁的线程就会进行阻塞等待,不会继续进行自旋,就会让出cpu,不会是cpu占用率太高

当线程释放锁的时候,系统就会随机唤醒一个线程来获取锁

锁消除

编译器发现代码不需要加锁时,不涉及线程安全的代码就会自动把锁干掉

锁粗化

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

粒度就是synchronized后面的大阔号里的代码越少,粒度越细

对于多线程来说粒度越细有利于多个线程并发执行,并不是说粒度越细越好,有的时候也是希望粒度粗点,当线程需要三次加锁解锁加锁解锁加锁解锁,就可以合并成一个粗粒度的锁,这样可以提高效率,因为每次加锁都可能会涉及阻塞

 

CAS

compare and swap是一个特殊的cpu指令,工作内容是交换和比较,比较和交换的是内存和cpu寄存器,可以原子的完成很多复杂的操作,达成无锁化编程效果

1.基于CAS实现原子类

java标准库提供的原子类基于原子类实现的

这个代码是使用了原子类的方法,在后置++操作就是通过cas的方式实现的,且要修改的内容也没有加锁,但是也保证了线程安全,并且更加高效,不会涉及线程阻塞等待

 

 

2.实现自旋锁

通过cas看当前线程是否有锁,若有锁就自旋等待,没锁就尝试让线程加锁

3.ABA问题

当数值从A变成B,又在途中把数值变回A此时CAS不知到数值到底是变了还是没有变

解决方案:1.约定数据变化只能单向变化,2.对于本身必须双向变化的数据,可以引入一个版本号,且这个版本号只能增加不能减少,让CAS查看到底有没有变

 

posted @ 2025-03-31 15:53  油头男孩  阅读(15)  评论(0)    收藏  举报