(1)悲观锁与乐观锁
(2)公平锁与非公平锁
(3)自旋锁与重入锁
(4)重量级锁与轻量级锁
(5)独占锁与共享锁
在Mysql数据库修改一行数据就是悲观锁,悲观锁就是比较悲观,当多个线程对同一行数据进行修改的时候,最后只有一个线程能够修改成功,只要谁能够获取到行锁,则其他线程都不能对该数据进行修改,且是阻塞状态。
在Java角度,悲观锁如果没有获取到锁,则会阻塞等待,唤醒锁的成本较高。
在java中,Lock 和 synchronize锁都是悲观锁。
乐观锁比较乐观,认为不会有其他线程修改数据,通过预期或者版本号比较,如果不一致的情况则通过循环控制修改,线程不会被阻塞,效率较高。但是会消耗CPU资源。
(1)在表字段中增加一个version字段
(2)多个线程对同一行数据实现修改操作时,提前获取当前的version作为更新的判断依据
(3)带着verison编号去执行更新操作,如果version变了则修改失败
(4)修改失败就不断重试,直到成功
就是比较公平,根据请求锁的顺序排队,先来请求的线程就先获取锁,后来的就后获取,采用队列进行存放, 类似于吃饭排队,打水排队。
公平锁示例:
public static void main(String[] args) {
//通过 ReentrantLock 创建一个公平锁,根据参数传递 true
ReentrantLock reentrantLock = new ReentrantLock(true);
//创建10个线程测试这个 获取锁的过程
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "--" + finalI);
} catch (Exception e) {
} finally {
if (reentrantLock != null) {
reentrantLock.unlock();
}
}
}).start();
}
}
}
顾名思义,不是根据请求顺序排列的,通过争抢的方式获取锁,哪个线程进来都先看一眼锁在不在,在就获取,不在再去排队等着抢。
非公平锁的效率比公平锁的效率要高,synchronized是非公平锁
非公平锁的示例:只需要把上述代码改成 false
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(true);
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "--" + finalI);
} catch (Exception e) {
} finally {
if (reentrantLock != null) {
reentrantLock.unlock();
}
}
}).start();
}
}
}
通过队列的方式去实现 线程获取锁的排队,根据顺序去获取锁并执行逻辑。
在同一个线程中锁是可以不断传递的,可以直接获取。
比如递归、A调用B这种情况,不可能等A把锁释放了再调用B吧,因为B是A方法的一部分,A也没有运行完。所以锁是线程级别的,它会记录是哪个线程获取了这个锁,然后那个线程就可以直接用这个锁去对应的方法直接用。
例如 synchronized、lock、aqs。
CAS:Compare And Swap ,比较并交换,执行函数CAS(V,E,N)
CAS不是Java实现的,是由JNI调用C++实现的。
通常来讲,锁的存在导致 没有获取到锁的线程 会阻塞,为了解决这个问题,CAS的存在让线程不再阻塞,而是靠着一直自旋(循环运行)。
CAS有三个操作数 ,内存值V(共享值),旧的期望值E(读取到的共享值),要修改的新值N
当且仅当预期值与内存值相同时,将内存值V修改为N,否则什么都不做,这个概念其实与乐观锁的类似。
当 E==V时,才会改变V。
优点:通过循环的方式解决了 线程因为锁阻塞的问题
缺点:循环次数过高的话,会导致CPU标高,所以需要控制次数。
通过以上可以知道, 比较V和E时,我们如何知道 即使V值 和E 相同,但是已经变过了呢??
一般可以增加一个版本号version