java有哪些锁的分类:
- 悲观与乐观锁
- 公平锁与非公平锁
- 自旋锁/重入锁
- 重量级锁与轻量级锁
- 独占锁与共享锁
文章目录
乐观锁悲观锁:
什么是悲观锁,什么是乐观锁
悲观锁:
-
mysql的角度分析: 悲观锁就是比较悲观,当多个线程同一个数据实现修改的时候,最后只有一个线程才能修改成功,只要谁能够获取到行锁 则其他线程时不能够对数据做任何修改操作,且是阻塞状态
-
java锁层面:如果没有获取到锁,则会阻塞等待,后期唤醒的锁成本就会非常高,从新被我们CPU从就绪调度为运行状态
Lock synchronized 锁 悲观锁 没有获取到锁的线程会阻塞等待;
乐观锁:
乐观锁比较乐观,通过预值或者版本号比较,如果不一致性的情况则通过循环控制修改,当 前线程不会被阻塞,是乐观,效率比较高,但是乐观锁比较消耗 cpu 的资源。
乐观锁:获取锁–如果没有获取到锁,当前线程是不会阻塞等待,通过死循环控制
乐观锁属于无锁机制,没有竞争锁的流程
注意:mysql 的 innodb 引擎中存在行锁的概念/
Mysql层面如何实现乐观锁了?
在我们表结构中,会新增一个字段就是版字段
``version varchar(255) DEFAULT NULL,`
多线程对同一行数据实现修改操作,提前查询当前最新的
version
版本号码作为
update
条件查询,如果当前version
版本号码发生了变化,则查询不到该数据,表示如果修改数据失败,则不断重试,有从新查询最新的版本实现update
需要注意的是 控制乐观锁的循环的次数,避免cpu飙高的问题
mysql的innodb引擎中存在行锁的概念
乐观锁的实现方式:
一
乐观锁,使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
记录1,id,status1,status2,stauts3,version,表示有三个不同的状态,以及数据当前的版本
操作1:update table set status1=1,status2=0,status3=0 where id=111;
操作2:update table set status1=0,status2=1,status3=0 where id=111;
操作3:update table set status1=0,status2=0,status3=1 where id=111;
没有任何控制的情况下,顺序执行3个操作,最后前两个操作会被直接覆盖。
加上version字段,每一次的操作都会更新version,提交时如果version不匹配,停止本次提交,可以尝试下一次的提交,以保证拿到的是操作1提交后的结果。
这是一种经典的乐观锁实现。
二
另外,java中的compareandswap即cas,解决多线程并行情况下使用锁造成性能损耗的一种机制。
CAS操作包含三个操作数,内存位置(V),预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会西东将该位置值更新为新值。否则,处理器不做任何操作。
记录2: id,stauts,status 包含3种状态值 1,2,3
操作,update status=3 where id=111 and status=1;
即 如果内存值为1,预期值为1,则修改新值。对于没有执行的操作则丢弃。
公平锁与非公平锁
公平锁与非公平锁的直接的区别
公平锁: 就是比较公平,根据请求锁的顺序排列,先来请求先来请求的就先获取锁,后来获取锁就最 后获取到, 采取队列存放…类似类似于吃饭排队。
队列---底层实现方式---数组或者链表实现
非公平锁:不是据请求的顺序排列,通过争抢的方式获取锁
非公平锁效率是公平锁效率要高,Synchronized 是非公平锁
New ReentramtLock()(true)—公平锁
New ReentramtLock()(false)—非公平锁
底层基于 aqs 实现
独占锁与共享锁
独占锁与共享锁之间的区别
独占锁: 在多线程中,只允许有一个线程获取到锁,其他线程都会等待。
共享锁: 多个线程可以同时持有锁,
例如 ReentrantLock 读写锁。读读可以共享、写写互斥、读写互斥
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存!
* 读-写 不能共存!
* 写-写 不能共存!
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 写入
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
// 读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
}
// 加锁的
class MyCacheLock {
private volatile Map<String, Object> map = new HashMap<>();
// 读写锁: 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock lock = new ReentrantLock();
// 存,写入的时候,只希望同时只有一个线程写
public void put(String key, Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
// 取,读,所有人都可以读!
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
/**
* 自定义缓存
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// 存,写
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
// 取,读
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
}
}
可重入性
在同一个线程中锁可以不断传递的,可以直接获取Syn/lock ``aqs
CAS(自旋锁)
CAS: 没有获得到锁的线程是不会阻塞的,通过循环控制一直不断的获取锁
CAS:Compare and Swap
,翻译成比较并交换,执行函数CAS(V,E,N)
三个操作数: 内存值V,
旧的预期值E
,要修改的新值N
当且仅当预期值E和内存值V相同时,将内存值V修改为N,负责什么都不做
- CAS是通过硬件指令,保证了原子性
- java是通过unsafe jni技术
原子类: AtomicBoolean
,AtomicInteger
,AtomicLong
等使用 CAS 实现
unsafe类
优点:
没有获取到锁的资源,会一直子啊用户态,不会阻塞,没有锁的线程会一直通过循环控 制重试
缺点:
通过死循环控制,消耗 cpu 资源比较高,需要控制循次数,避免 cpu 飙高问题;
Cas 本质的原理:
旧的预期值===>>>V(共享变量中值),才会修改我们的V
基于 cas 实现锁机制原理
Cas 无锁机制原理:
- 定义一个锁的状态
- 状态状态值=0,则表示没有线程获取到该锁
- 状态状态值=1,则表示有线程已经持有该锁
实现细节:
CAS获取锁:
将该锁的状态从0到1----能够修改成功 cas 成功则表示获取锁成功
如果获取锁失败–修改失败,则不会阻塞而是通过循环==(自旋控制重试)==
CAS 释放锁:
将该锁的状态从 1 改为 0 如果能够改成功 cas 成功则表示释放锁成功。
CAS 如何解决 ABA 的问题
CAS主要检查内存值V 与旧的预值值=E是否一致,如果一致的情况下,则修改
这时会存在ABA的问题:
如果将原来的值A,改为B,B有改为了A发现没有发生变化,实际上已经发生了变化,所以存在ABA问题
解决方法,通过版本号,对没个变量更新的版本号+1
引用原子引用,对应的思想:乐观锁
解决 aba 问题是否大:概念产生冲突,但是不影响结果,换一种方式 通过版本号码方式
package com.nie.juc.cas;/*
*
*@auth wenzhao
*@date 2021/4/25 17:19
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 演示 aba 的问题
* (1)第一个参数 expectedReference:表示预期值。
* (2)第二个参数 newReference:表示要更新的值。
* (3)第三个参数 expectedStamp:表示预期的时间戳。
* (4)第四个参数 newStamp:表示要更新的时间戳。
*/
public class CASDemo {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 注意:如果引用类型是 Long、Integer、Short、Byte、Character 一定一定要注意值的缓存区间!
// 比如 Long、Integer、Short、Byte 缓存区间是在-128~127,
// 会直接存在常量池中,而不在这个区间内对象的值 则会每次都 new 一个对象,
// 那么即使两个对象的值相同,CAS 方法都会返回 false
// 先声明初始值,修改后的值和临时的值是为了保证使用 CAS 方法不会因为对象不一样而返回 false
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
;
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Lock lock = new ReentrantLock(true);
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("a2=>----" + atomicStampedReference.getStamp());
System.out.println("更改" + atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("+++++++++++++++++++++++++++++++++++++++++++++");
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
输出:
a1=>1
b1=>1
a2=>----2
更改true
a3=>3
+++++++++++++++++++++++++++++++++++++++++++++
false
b2=>3
利用原子类手写 CAS 无锁
package com.nie.juc.cas;/*
*
*@auth wenzhao
*@date 2021/4/25 20:36
*/
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;
public class AtomicTryLock {
private AtomicLong cas = new AtomicLong(0);
private Thread lockCurrentThread;
/**
* 1 表示锁已经被获取
* 0 表示锁没有获取
* 利用 cas 将 0 改为 1 成功则表示获取锁
*
* @return
*/
//加锁
private boolean tryLock() {
boolean result = cas.compareAndSet(0, 1);
if (result) {
lockCurrentThread = Thread.currentThread();
}
return result;
}
//释放锁
private boolean unLock() {
if (lockCurrentThread != Thread.currentThread()) {
return false;
}
return cas.compareAndSet(0, 1);
}
public static void main(String[] args) {
AtomicTryLock atomicTryLock = new AtomicTryLock();
IntStream.range(1, 10).forEach((i) -> new Thread(() ->
{
try {
boolean result = atomicTryLock.tryLock();
if (result) {
atomicTryLock.lockCurrentThread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + ",获取锁成功~");
} else {
System.out.println(Thread.currentThread().getName() + ",获取锁失败~");
}
} catch (Exception e) {
} finally {
//释放锁
if (atomicTryLock != null) {
atomicTryLock.unLock();
}
}
}).start());
}
}
输出
Thread-0,获取锁成功~
Thread-3,获取锁失败~
Thread-2,获取锁失败~
Thread-1,获取锁失败~
Thread-4,获取锁失败~
Thread-7,获取锁失败~
Thread-8,获取锁失败~
Thread-6,获取锁失败~
Thread-5,获取锁失败~