1、锁的分类
自旋锁: 线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起
阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态
重入锁:支持线程再次进入的锁,就跟我们有房间钥匙,可以多次进入房间类似
读写锁: 两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享
互斥锁: 上厕所,进门之后就把门关了,不让其他人进来
悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
公平锁:大家都老老实实排队,对大家而言都很公平
非公平锁:一部分人排着队,但是新来的可能插队
偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
独占锁:独占锁模式下,每次只能有一个线程能持有锁
共享锁:允许多个线程同时获取锁,并发访问共享资源
part 2
Lock的使用
实例:
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
// 提供一把锁
private static Lock lock = new ReentrantLock();
public static void inCreate(){
lock.lock(); // 利用锁实现
num++;
lock.unlock();}
public static void test(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) {
System.out.println(num);
break;
}
}
}
}
lock与synchronized的区别
lock 获取锁与释放锁的过程,都需要程序员手动的控制 Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作
synchronized托管给jvm执行 原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
实现了lock接口的锁
自定义一把锁:
/**
* 自定义一把锁
* 重入锁的定义:
* 重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞
*/
public class Mylock implements Lock {
private boolean isHoldLock =false; // 用来记录锁是否被持有
private int reentryCount = 0;// 重入锁有一个重入的次数
private Thread holdLocalThread = null; // 用于保存持有的线程
/**
* 同一时刻有且仅能只有一个线程可以获取锁,
* 其他线程只能先等待锁释放后才能去获取锁
*/
@Override
public synchronized void lock() { // 利用synchrod关键字修饰是因为wait的时候必须持有锁
if(isHoldLock && Thread.currentThread() != holdLocalThread){
try {
wait(); // 锁被持有了,进入wait状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
holdLocalThread = Thread.currentThread(); // 持有锁的这个线程等于当前的这个线程
// 不被持有,则锁可用,同时锁状态改变
isHoldLock = true;
reentryCount++;
}
/**
*释放锁
*/
@Override
public synchronized void unlock() {
// 判断当前线程是否是持有锁的这个线程,是,重入次数-1,不是则不做处理
if(Thread.currentThread()==holdLocalThread){
reentryCount--;
}
// 当持有这个锁的线程数为0 的时候,就释放
if ((reentryCount==0)){
notify();
isHoldLock = false;}
}
part 3 AbstractQueuedSynchronizer浅析
AbstractQueuedSynchronizer -- 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。 此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。 子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。 假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。 应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。
此类支持默认的独占 模式和共享 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如ReadWriteLock 中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。
此类通过支持独占模式的子类定义了一个嵌套的 AbstractQueuedSynchronizer.ConditionObject 类,可以将这个类用作 Condition 实现。isHeldExclusively() 方法将报告同步对于当前线程是否是独占的;使用当前 getState() 值调用release(int) 方法则可以完全释放此对象;如果给定保存的状态值,那么 acquire(int) 方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer 方法创建这样的条件,因此,如果无法满足此约束,则不
要使用它。AbstractQueuedSynchronizer.ConditionObject 的行为当然取决于其同步器实现的语义。
此类为内部队列提供了检查、检测和监视方法,还为 condition 对象提供了类似方法。可以根据需要使用用于其同步机制的 AbstractQueuedSynchronizer 将这些方法导出到类中。
此类的序列化只存储维护状态的基础原子整数,因此已序列化的对象拥有空的线程队列。需要可序列化的典型子类将定义一个 readObject 方法,该方法在反序列化时将此对象恢复到某个已知初始状态。
公平锁与非公平锁的区别
公平锁:顾名思义--公平,大家老老实实排队 非公平锁:只要有机会,就先尝试抢占资源 公平锁与非公平锁其实有点像在公厕上厕所。公平锁遵守排队的规则,只要前面有人在排队,那么刚进来的就老老实实排队。而非公平锁就有点流氓,只要当前茅坑没人,它就占了那个茅坑,不管后面的人排了多久。
非公平锁的弊端
可能导致后面排队等待的线程等不到相应的cpu资源,从而引起线程饥饿;
ReentrantLock reentrantLock = new ReentrantLock(true); //传入true说明是公平锁
AQS -----AbstractQueuedSynchronizer 此类支持默认的独占和共享模式之一,独占模式时,只能由一个线程可以成功获取该锁,处于共享模式的时候,可能由多个线程获得这个锁
公平锁跟非公平锁实现上的差别?
读写锁--ReentrantReadWriteLock
特性:写写互斥、读写互斥、读读共享
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
/**
* 读写锁的实现demo
*/
public class ReentrantReadWriteLockDemo {
private int i=0;
private int j=0;
// 构建读写锁
private ReadWriteLock lock= new ReentrantReadWriteLock() ;
// 分别获取读写锁
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
/**
* 读线程
* 可能出现在i++之后,j还未开始执行 j ++,out函数就已经执行,出现结果的错误
* 可以对out和increate函数加synchronized锁,但是当读的次数多,写的次数少的时候,
* 会浪费资源; 所以加入读写锁更合适
*/
public void out(){
readLock.lock(); // 加入读锁
try{
System.out.println(Thread.currentThread().getName()+" the value of i is "+i+" "+
" the value of j is "+j);}
finally {
readLock.unlock(); // 释放读锁
}
}
public void inCreate(){
writeLock.lock();
try {
i++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
j++;}finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo reentrantReadWriteLockDemo = new ReentrantReadWriteLockDemo();
for(int i=0;i<3;i++){
new Thread(
()->{reentrantReadWriteLockDemo.inCreate();
reentrantReadWriteLockDemo.out();}
).start();
}
}
}
AQS如何用单一int值表示读写两种状态?
一个int是32位的,将其拆分成两个无符号的short
高位表示读锁,低位表示写锁
锁降级:
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
注意点:锁降级之后,写锁并不会直接降级成读锁,不会随着读锁的释放而释放,因此需要显式地释放写锁
是否有锁升级?
在ReentrantReadWriteLock里面,不存在锁升级这一说法
锁降级的应用场景
用于对数据比较敏感,需要在对数据修改之后,获取到修改后的值,并进行接下来的其他操作
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock readLock = reentrantReadWriteLock.readLock();
Lock writeLock = reentrantReadWriteLock.writeLock();
readLock.lock();
writeLock.lock(); // 锁降级
writeLock.unlock();
readLock.unlock();
System.out.println("程序运行结束");
}
/**
* 锁降级Demo
*/
public class LockDegradeDemo {
private int i = 0;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
public void doSomething() {
writeLock.lock();
try {
i++;
readLock.lock();
}finally {
writeLock.unlock();
}
try {
//模拟其他复杂的操作
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
if (i == 1) {
System.out.println("i的值是======》1");
} else {
System.out.println("i的值是"+i);
}
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
LockDegradeDemo lockDegradeDemo = new LockDegradeDemo();
for (int i = 0; i < 4; i++) {
new Thread(()->{
lockDegradeDemo.doSomething();
}).start();
}
}
}
StampedLock原理及使用:
这个是在jdk1.8之后才有:
一般应用,都是读多写少,ReentrantReadWriteLock 因读写互斥,故读时阻塞写,因而性能上上不去。可能会使写线程饥饿。所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功; 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致; StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁) 支持锁升级跟锁降级 可以乐观读也可以悲观读 使用有限次自旋,增加锁获得的几率,避免上下文切换带来的开销 乐观读不阻塞写操作,悲观读,阻塞写得操作。
StampedLock的优点:
相比于ReentrantReadWriteLock,吞吐量大幅提升
StampedLock的缺点:
api相对复杂,容易用错 内部实现相比于ReentrantReadWriteLock复杂得多
StampedLock的原理:
每次获取锁的时候,都会返回一个邮戳(stamp),相当于mysql里的version字段 释放锁的时候,再根据之前的获得的邮戳,去进行锁释放
使用stampedLock注意点:
如果使用乐观读,一定要判断返回的邮戳是否是一开始获得到的,如果不是,要去获取悲观读锁,再次去读取。
public class StampedLockDemo { // 成员变量 private double x, y; // 锁实例 private final StampedLock sl = new StampedLock(); // 排它锁-写锁(writeLock) void move(double deltaX, double deltaY) { long stamp = sl.writeLock(); //返回一个邮戳 try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } } // 乐观读锁 double distanceFromOrigin() { // 尝试获取乐观读锁(1) long stamp = sl.tryOptimisticRead(); // 将全部变量拷贝到方法体栈内(2) double currentX = x, currentY = y; // 检查在(1)获取到读锁票据后,锁有没被其他写线程排它性抢占(3) if (!sl.validate(stamp)) { // 乐观读判断邮戳是否是一开始获取的,不是,就去获取悲观读 // 如果被抢占则获取一个共享读锁(悲观获取)(4) stamp = sl.readLock(); try { // 将全部变量拷贝到方法体栈内(5) currentX = x; currentY = y; } finally { // 释放共享读锁(6) sl.unlockRead(stamp); } } // 返回计算结果(7) return Math.sqrt(currentX * currentX + currentY * currentY); } // 使用悲观锁获取读锁,并尝试转换为写锁 void moveIfAtOrigin(double newX, double newY) { // 这里可以使用乐观读锁替换(1) long stamp = sl.readLock(); try { // 如果当前点在原点则移动(2) while (x == 0.0 && y == 0.0) { // 尝试将获取的读锁升级为写锁(3) long ws = sl.tryConvertToWriteLock(stamp); // 升级成功,则更新票据,并设置坐标值,然后退出循环(4) if (ws != 0L) { stamp = ws; x = newX; y = newY; break; } else { // 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试(5) sl.unlockRead(stamp); stamp = sl.writeLock(); } } } finally { // 释放锁(6) sl.unlock(stamp); } } }