悲观锁、乐观锁
乐观锁与悲观锁并不是特指某个锁,而是在并发情况下保证数据完整性的不同策略。是一种理念
悲观锁具有强烈的独占和排他特性。它指的是对数据被外界修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态
传统关系型数据库里面的很多锁就是采用的这种机制,例如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
java里面的synchronize和ReentrantLock等重入锁就是采用的这种机制;
乐观锁总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,
一般会使用版本号机制或CAS操作实现。乐观锁适用于多读的应用类型,
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁的两种实现方式:
1.版本号机制
即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
2.cas算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,
也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
a.需要读写的内存值 V
b.进行比较的值 A
c.拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
java中的java.util.concurrent.atomic包下的原子操作,就是用的cas算法
AtomicInteger.getAndIncrement 相当于i++操作,线程安全的
原子操作是调用的unsafe类的方法,这个类的方法是native方法,也就是调用的本地方法
unsafe类中的所有方法都是native修饰的,也就是unsafe类中的方法都直接调用操作系统底层资源相应任务,可以直接操作内存
就是jvm内存模型中的本地方法栈和本地执行引擎,
getAndIncrement方法内部先获取当前对象的属性值的内存偏移量,根据偏移量取到该值,之后在取出来一次该值是否等于第一次的值,
如果等于,就进行更新,不等于就继续获取,比较,直至更新成功,在进行获取和比较的过程是操作系统层面的,是一条原语,原语是一个原子操作,不会被打断
| public final int getAndAddInt(Object var1, long var2, int var4) {//当前对象;内存偏移量即获取属性值;要更新的值 int var5; do { //需要进行比较的值 var5 = this.getIntVolatile(var1, var2); //再次获取一次当前对象的内存偏移量对应的值是否与var5相同,如果相同,则进行交换,返回fasle则继续循环获取,比较;原子操作 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } |
|---|
cas的缺点
1.一直循环比较,循环时间长,开销大。故适用于偏读操作。
2.只能保证一个共享变量的原子操作。
3.引出aba问题
cas算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较替换,那么会在这个时间差中会导致数据的变化:
比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two将值变成了B,并写回了主内存,然后线程two
又将V位置的数据变成A,这个时候线程one进行cas操作发现内存中任然为A,然后线程one操作成功了。
尽管线程one的cas操作成功,但是不代表这个过程就是没有问题的。
atomicreference原子引用,可以对对象进行原子操作
|
|
AtomicStampedReference
印章
解决ABA问题可以使用添加版本号
以AtomicStampedReference为例,添加一个版本号信息,在进行compareandset的时候需要传4个参数
1.期望值
2.需要更新的值
3.期望的版本号
4.需要修改的版本号
| AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);//初始值;初始版本号 |
|---|
lock与synchronized的区别
1.原始构造
synchronized是关键字,属于jvm层面;
lock是具体的类,是juc(java.utile.concurrent包),是api层面的锁
2.使用方法
synchronized 不需要用户手动释放锁,当synchronized代码执行完毕后系统会自动让线程释放对锁的占用
reenttrantlock则需要用户手动释放锁,若没有主动的释放锁,就有可能导致死锁现象。
3.等待是否可中断
synchronized 不可中断,除非抛出异常或者正常运行完成
reentrantlock 可中断,1.设置超时方法 trylock(long timeout,timeunite unie)
2.lockinterruptibly()放代码块中,调用intrrupt()方法可中断
4.加锁是否公平
synchronized 非公平锁
reentrantlock 两则都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
5.锁绑定多个condition
synchronized 没有
reentrantlock可以绑定多个condition,reentrantlock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,
而不是像synchronized要么随机唤醒一个,要么全部唤醒线程。
公平锁与非公平锁
公平锁:在并发环境下,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,就占有锁,否则就会加入到等待队列中,按照先进先出的规则中取到自己。
非公平:非公平锁比较粗鲁,上来就尝试占有锁,如果尝试失败,就再采取类似公平锁的方式。
非公平锁的吞吐量比公平锁大,可以让优先级高的线程先获取到锁
reentrantlock 默认是非公平锁,在构造方法中可以指定为公平锁
synchronized 是非公平锁
可重入锁(也叫递归锁)
指的是同一个线程外层获得锁之后,内层递归函数任然能获取该锁的代码,
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
[
public synchronized void A(){
B();
}
public synchronized void B(){
System.out.println(123);
}
]
可以理解为A在获取锁的时候,也获得了B的锁,线程只有拿到了A的锁和B的锁,才算是获取到了锁
死锁
死锁是指两个或以上的进程在执行过程中,因挣夺资源而造成的一种相互等待的现象,若无外力干涉,那他们都将无法推进下去,如果系统资源充足,
进程资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因挣夺有限的资源而陷入死锁。
查看是否存在死锁定位分析
jps 类似于liunx 的 ps -ef | grep "xxx"
jstack 打印java的堆栈信息
a. jps -l
获取进程编号,推断出是某个进程出现问题 xxx
b. jstack xxx
打印堆栈信息进行分析
会出现 Found 1 deadlock. 表示出现死锁
自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式获取锁,这样的好处是减少线程上线文切换的消耗,缺点是循环会消耗cpu
可以参考上面的automicInteger的i++的底层操作
独占锁、共享锁
独占锁:指该锁一次只能被一个线程所持有。reentrantlock和sync都是独占锁
共享锁:指该锁可以被多个线程所持有
对于reentrantReadWriteLock读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写是互斥的,读读是共享的
独占锁和共享锁代码演示
开启5个线程写数据,每个线程耗时1秒,开启5个线程读数据,每个线程耗时1秒,运行程序,
可以看到写线程是一个一个的执行,即写锁互斥,一个线程拿到写锁后独占,只有该线程释放该锁后,下一个线程才可以获取到该锁;
读线程是一起执行的,即读锁共享,可以并发读。
|
|
手写自旋锁
|
|
死锁的代码演示
|
|
本文介绍了悲观锁与乐观锁的概念,重点讲解了乐观锁的实现方式,包括版本号机制和CAS算法。通过Java的AtomicStampedReference类展示了如何解决ABA问题。同时对比了锁(如synchronized和ReentrantLock)的使用区别,讨论了公平锁与非公平锁、可重入锁的概念,并提供了死锁的分析方法。最后,简述了自旋锁和独占锁、共享锁的概念。
463

被折叠的 条评论
为什么被折叠?



