目录
一、乐观锁与悲观锁
二、重量级锁与轻量级锁
三、自旋锁
四、公平锁与不公平锁
五、可重入锁与不可重入锁
六、读写锁
一、乐观锁与悲观锁
乐观锁:乐观锁认为在大多数情况下,数据一般不会并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
悲观锁:悲观锁认为在并发环境下,数据冲突是常态。因此,它会在数据被读取时就加锁,就每次拿数据的时候都会进行一次上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
例如在生活职场上,有一个比较重要的项目需要小组去做,而在流程进行中,有一些东西需要请示领导,而在这个时候有俩个员工,分别是员工A和员工B都需要去请示一次领导,员工A认为领导是比较忙的,所以在员工需要请示领导的时候首先在微信跟领导说一下(相当于加锁操作),得到肯定的答复之后,才会去领导的办公室进行商讨,如果得到的是否定的答复,那么就等待一段时间,再进行时间的确认,这个就是悲观锁。
而员工B认为领导是比较闲的,所以找他商讨项目的事情大概率是有空解答的。因此员工B直接去领导办公室找他,如果领导在的话,问题就解决了,如果不在,就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.。
俩个锁的各有其优缺点。
如果领导确实比较忙,那么使用悲观锁的策略更合适,使用乐观锁就会导致“白跑很多趟”,消耗的资源比较多。
如果领导确实比较闲的话,那么乐观锁的策略更合适,悲观锁会让效率比较低。
而在java中,Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.
二、重量级锁与轻量级锁
锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
重量级锁
特点
基于内核:重量级锁的实现依赖于操作系统的内核机制。当线程尝试获取一个已经被占用的重量级锁时,线程会被挂起(阻塞),并进入内核态等待锁的释放。
性能开销大:由于涉及到用户态与内核态的切换,以及线程的挂起和唤醒操作,重量级锁的性能开销较大。每次加锁或解锁都需要消耗较多的系统资源。
适用场景:适用于锁持有时间较长的场景,因为在这种情况下,重量级锁的开销相对锁的持有时间可以忽略不计。
轻量级锁
轻量级锁是一种优化的锁机制,旨在减少锁操作的开销,提高多线程程序的性能。
特点
基于用户态:轻量级锁的实现主要在用户态完成,尽量避免进入内核态。当线程尝试获取一个已经被占用的轻量级锁时,线程不会立即被挂起,而是会尝试通过自旋(Spin)等方式等待锁的释放。
性能开销小:由于减少了内核态切换和线程阻塞的开销,轻量级锁在锁竞争不激烈的情况下性能更高。
适用场景:适用于锁持有时间非常短的场景,因为在这种情况下,自旋等待锁释放的开销比线程阻塞和唤醒的开销要小。
synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.
三、自旋锁
线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.。这样就可以使用挂起等待锁(就不尝试抢锁,等待锁释放,通过其他的方式进行抢锁)。
而自旋锁是什么,就类似于无限循环,如果拿锁失败,会在极短的时间内再次尝试拿起锁。伪代码如下:
while (抢锁(lock) == 失败) {}
比较适用于锁释放时间短的场景,如果抢锁失败,过不了多久锁会被释放,那就没必要放弃cpu,这个时候就可以使用自旋锁来处理这样的问题.。
四、公平锁与不公平锁
公平锁与非公平锁是什么呢?公平锁讲究的是“先进先出”,而非公平锁就不讲究“先进先出”。而在线程调度这边,是随机调度的,我们可以认为这是不公平的,如果想要让其变公平,则需要通过一些额外数据结构,记录其线程调度顺序,来让其公平,否则就不行。
synchronized 是非公平锁。
五、可重入锁与不可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。简单理解就是,可重入锁不至于把“自己”锁死,而不可重入锁有可能会把自己锁死。
例如:java中synchronized 是可以重入的。
synchronized (object1){synchronized(object1){try {object1.wait(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}
在上面的代码中,用了俩次synchronized,其实效果和用一次是一样的,因为在Java里面的锁是可重入的,而不可重入锁如一个简单的互斥锁(Mutex),但在性能这边,不可重入锁的性能会更好,而缺点就是不能存在于递归的场景。
六、读写锁
读写锁是一种特殊的锁,它允许多个线程之间同时读取共享资源,但写入操作是互斥的。也就是说,在写入操作时,任何线程(无论是读线程还是写线程)都不能访问共享资源。
总的来说:
俩个线程都只是读一个数据,此时并没有线程安全问题,直接并发的读取即可。(读加锁与读加锁,不互斥)
俩个线程都要写一个数据,有线程安全问题。(写加锁和写加锁,互斥)
一个先后曾读另外一个先后曾写,也有线程安全问题。(读加锁和写加锁之间,互斥)
Java标准库中提供了提供了ReentrantReadWriteLock类,实现了读写锁,其中ReentrantReadWriteLock.ReadLock类表示一个读锁.这个对象提供了lock/unlock方法进行加锁解锁.,还提供了ReentrantReadwriteLock.WriteLock类表示一个写锁,这个对象也提供了lock/unlock方法进行加锁解锁.