目录
锁策略(locking strategy)
乐观锁/悲观锁
锁的实现,预测接下来锁冲突概率是大,还是不大,根据这个冲突的概率,来决定接下来该怎么做
锁冲突:就是锁竞争,两个线程针对一个对象加锁,产生了阻塞等待
悲观锁:
预测接下来冲突概率比较大
所以每次在拿数据的时候就会上锁,这样别人想拿这个数据的时候就会阻塞直到它释放锁,适用于写操作比较频繁的场景
乐观锁:
预测接下来冲突概率比较小
发生冲突之前什么都不管就是干,如果发生了并发冲突了,则让返回用户的错误信息,让用户决定如何去做
读写锁(readers-write lock)
多线程之间,数据的读取方之间不会产生 线程安全,但数据写入方互相之间以及和读者之间都需要进行互斥,如果两种场景夏都用同一个锁,就会产生极大地性能损耗,所以读写锁因此而产生.
。读写锁可以提高程序的并发度,减少锁的竞争,从而提高程序的性能,但是比较容易引起死锁,要注意避免死锁的发生
读者之间并不互斥,而写者则要求与任何人互斥.
一个线程对于数据的访问,主要存在两种操作:读数据 和 写数据
-
两个线程都只是读一个数据,此时并没有线程安全问题,之间并发的读取即可
-
两个线程都要写一个数据,有线程安全问题
-
一个线程读另一个线程写,也有线程安全问题
读写锁就是把读操作和写操作区分对待,java标准库提供了ReentrantReadWriteLock类,实现了读写锁
-
ReentrantReadwriteLock.ReadLock类表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁
-
ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象也提供了lock/unlock方法进行加锁解锁
其中
-
读加锁和读解锁之间,不互斥
-
写加锁和写解锁之间,互斥
-
读加锁和读解锁之间,互斥
注意只要是涉及到了"互斥",就会产生线程的挂起等待,一旦线程挂起,再次被唤醒就不知道隔了多久了
因此尽可能的减"互斥"的机会,就是提高效率的重要途径
读写锁特别适合于"频繁读,不频繁读"的场景(这样的场景也是非常广泛存在的)
比如教务系统
每节课老师都要用教务系统点名,点名就需要查看班级的同学列表(读操作),这个操作可能要每周执行好几次
而修改的情况不多,每次来了新同学或者有同学退学的时才修改一下
Synchronized不是读写锁
重量级锁 VS 轻量级锁
锁的核心特性"原子性",这样的机制追更溯源是CPU这样的硬件设备提供的
-
CPU提供了"原子操作指令"
-
操作系统基于CPU的原子指令,实现了mutex互斥锁
-
JVM基于操作系统提供的互斥锁,实现了syschronized 和 ReentrantLock 等关键字和类
注意,synchronized并不仅仅是对mutex进行封装,在synchonized内部还做了其他的工作
重量级锁:加锁机制重度的依赖了OS提供的mutex
-
大量的内核态用户态切换
-
很容易引发线程的调度
这两个操作,成本比较高,一旦涉及到用户态代码完成,就意味着"沧海桑田"
轻量级锁:加锁机制尽量不适用mutex,而是尽量在用户态