目录
乐观锁与悲观锁
悲观锁
以悲观的心态看待线程冲突问题,所以每次都加锁操作共享变量。总是假设最坏的情况发生,每次去拿数据的时候都认为其他线程会对其进行修改,因此每次在拿数据的时候都进行上锁操作,这样其他线程想拿到这个数据就会阻塞直到它拿到锁
乐观锁
假设数据一般情况下不会发生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突,则返回用户错误的信息,让用户决定应该怎么处理。以乐观的心态看待线程冲突问题,所以每次都不加锁(层序层面),就直接操作共享变量。
乐观锁好还是悲观锁好
没有绝对的更好,不同的锁策略适用不同的场景,当大部分情况下存在线程冲突时,就使用悲观锁;当大部分情况下不存在线程冲突时,就使用乐观锁。
· 线程冲突:多个线程并发并行的对同一个共享变量进行操作(存在线程安全问题)
读写锁
多线程之间,如果是单纯的读操作,不会产生线程安全问题,但在多个线程进行写操作时、或者是
写操作与读操作之间就会产生线程安全问题,就需要进行互斥。如果两种场景下都用同一个锁,就会产生很大的性能的消耗,因此就引入了读写锁。
读写锁(readers-writer lock):从英文上来看,readers是复数,而writer是单数,说明在执行加锁操作时需要额外表明读写意图,复数读者之间不互斥,而写者需要与所有的读者、写者互斥.
一个线程对于数据的访问,主要就是读数据操作和写数据操作.
· 两个线程都是读取同一个数据,此时不存在线程安全问题,直接并发的读就可以
· 两个线程同时要写一个数据时,此时存在线程安全问题
· 一个线程读,另一个线程写时也存在线程安全问题
读写锁就是把读操作和写操作区分开来,Java标准库提供了ReentrantReadWriteLock类,实现了读写锁.
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
写操作
//写操作
writeLock.lock();//写锁加锁
//写操作
writeLock.unlock();//写锁释放锁
读操作
//读操作
readLock.lock();//读锁加锁
//读操作
readLock.unlock();//读锁释放锁
注意
· 读加锁和读加锁之间不互斥
· 写加锁和写加锁之间互持
· 读加锁与写加锁之间互斥
· 只要涉及到“互斥”,就会产生线程的挂起等待,一旦线程挂起,再次被唤醒就不知道要隔多长时间了。
· 减少产生“互斥”的机会,就是提高效率的重要途径
读写锁特别适合一些“频繁读,不频繁写”的场景中
就比如我们在学校教务系统中查看课表的操作,课表只导入一次,而我们会不断地在教务系统进行查看课表,我们查询课表相当于读操作,课表的导入相当于写操作,此时就适合应用读写锁.
重量级锁与轻量级锁
· 重量级锁:加锁机制重度依赖了OS提供的mutex
· 大量的内核态用户态切换
· 很容易引发线程的调度
· 轻量级锁:轻量级锁的加锁机制尽可能不使用mutex,而是尽量在用户态代码完成,实在不行了,再使用mutex
· 少量的内核态用户态切换
· 不容易引发线程的调度
什么是用户态与内核态?
就比如去银行办理业务.
· 在窗口外自己办理业务,这就是用户态,用户态的时间成本是可控的
· 在窗口内工作人员办理业务,这是内核态,内核态的时间成本是不可控的
· 如果办业务的时候需要反复和工作人员沟通,此时就需要不断地重新排队,这样的效率是很低下的.
例如synchronized开始时是一个轻量级锁,如果锁的冲突比较严重,就会变成重量级锁
在后续的synchronized原理中会详细论述synchronized从轻量级锁到重量级锁的转换