赞
踩
目录
指到底如何实现锁
乐观锁:每次读写数据都认为不会发生冲突,线程不会阻塞,一般来说,只有在进行数据更新时才会检查是否发生冲突,若没有冲突,直接更新,只有冲突(多个线都在更新数据)了才解决冲突问题。
悲观锁:每次去读写数据都会冲突,每次在进行数据读写时都会上锁(互斥),保证同一时间段只有一个线程在读写数据。eg:同学A和同学B问老师问题
悲观锁策略(A):每次问问题,A都认为老师忙着呢。A先给老师发个信息"老师在吗?“(尝试加锁),老师没回或者回复“忙着呢”,A就等待(线程阻塞),一直等老师回复他"我好了"(CPU唤醒了等特线程,尝试重新加锁)。此时A被唤醒,然后对老师加锁。
乐观锁策略(B):B认为每次问问题,老师都闲着呢。B直接问问题(不上锁,直接访问数据)。若老师确实有空(直接响应,避免了加锁和解锁的操作);假设此时老师忙着呢,B一看老师没回复他就接着干别的事(线程不会阻塞,去干别的事),过段时间再来。
这两种策略都有相应的应用场景:
当线程冲突不严重的时候,可以采用乐观锁策略来避免多次的加锁解锁操作;
当线程冲突严重时,采用悲观锁策略,加锁来避免线程频繁访问共享数据失败带来的CPU空转问题。
乐观锁并不是真正把线程阻塞了,乐观锁的实现一般都会采用版本号机制来实现
假设我们需要多线程修改 "用户账户余额"
写入失败,就从主存中读取最新的版本号到工作内存中,然后尝试在最新的数据上进行操作,若最后写回成功(2==2),主存和工作内存的值+1。
线程2的version和主内存的version不相等情况下
1. 直接报错,线程2退出,不写回;
2. CAS策略,不断重试写回,直到成功为止。从主内存中读取最新的值50以及版本号2,再次在50的基础上执行-20操作= 30,然后尝试重新写回主内存。
核心就是,线程能否成功的刷新主内存的值。
工作内存中的版本号==主存中的版本号才能更新,更新成功之后,同步刷新自己的版本号和主内存的版本号,表示此时更新成功。
一般锁的实现都是乐观锁和悲观锁并用的策略。synchronized最开始就是乐观锁,当竞争激烈再升级为悲观锁 。
多线程访问数据时,并发读取数据不会有线程安全问题,只有在更新数据(增删改)时会有线程安全问题将锁分为读锁和写锁。特别适用于线程基本都在读数据,很少有写数据的情况
1. 多个线程并发访问读锁(读数据),则多个线程都能访问到读锁,读锁和读锁是并发的,不互斥。
2. 两个线程都需要访问写锁(写数据),则这两个线程互斥,只有一个线程能成功获取到写锁,其他写线程阻塞。3. 当一个线程读,另一个线程写(也互斥,只有当写线程结束,读线程才能继续执行)。
eg:网文中,作者在写新章节的时候,所有读者都得等着,等他写完才能继续读。
synchronized不是读写锁,JDK内置了另一个ReentrantReadWriteLock实现读写锁。
重量级锁:需要操作系统和硬件支持,线程获取重量级锁失败进入阻塞状态(操作系统从用户态切换到内核态,开销非常大)。
eg:去银行办业务
用户态:在窗口外自己处理的业务属于用户态;
内核态:窗口内部,需要工作人员协助。
若某个业务频繁需要在用户态和内核态切换,非常耗时。
eg:A去银行给B转500w,需要在窗口柜台和存取机来回辗转
1. 填写转账单 2. 确认转账金额 3. 进行转账操作,输入密码 4. 获取转账成功回执单
轻量级锁:尽量在用户态执行操作,线程不阻塞,不会进行状态切换。
eg:用户进行转账,只需在自动转账机将以下操作完成
1. 填写转账单 2. 确认转账金额 3. 进行转账操作,输入密码 4. 获取转账成功回执单
轻量级锁的常用实现就是采用自旋锁
之前方式,获取锁失败的线程就会进入Blocked状态,线程置入阻塞队列,等待锁被释放,由CPU唤醒(这个时间一般来说都比较长,有用户态到内核态的切换)。
自旋锁:自旋就是循环
while(获取lock == fasle){ //循环; }线程获取锁失败并不会让出CPU,线程也不阻塞,不会从用户态切到内核态,线程在CPU上空跑,当锁被释放,此时这个线程会很快获取到锁。
eg:等红绿灯时碰到红灯
1. 熄火,当绿灯之后再打火启动——挂起等待锁。
2. 发动机不熄火,踩着刹车,当绿灯亮了直接走——自旋锁。
公平锁:获取锁失败的线程进入阻塞队列,当锁被释放,第一个进入队列的线程首先获取到锁(等待时间最长的线程获取到锁)。
非公平锁:获取锁失败的线程进入阻塞队列,当锁被释放,所有在队列中的线程都有机会获取到锁,获取到锁的线程不一定就是等待时间最长的线程。synchronized就是非公平锁。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。