乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。
读写锁
-
把加锁操作分成了俩种 一是读锁二是写锁 也就是说在读和读之间是没有互斥的 但是在读写和写写之间就会存在互斥
-
如果一个场景是一写多度 那么使用这个效率就会很高
重量级锁 VS 轻量级锁
-
首先我们要知道加锁有一个很重要的特性就是保证原子性 原子性的功能其实来源于硬件(硬件提供了相关的原子操作的指令, 操作系统把这些指令统一封装成一个原子操作的接口, 应用程序才能使用这样的操作)
-
所以在加锁过程中 如果整个加锁逻辑都是依赖于操作系统内核 那此时就是重量级锁(代码在内核中的开销会很大) 如果大多数操作都是用户自己完成的 少数由操作系统内核完成 这种就是轻量级锁
挂起等待锁 VS 自旋锁
-
挂起等待锁表示当前获取锁失败后, 对应的线程就要在内核中挂起等待 (放弃CPU进入等待队列) 需要在锁对象释放之后由操作系统唤醒 (通常都是重量级锁)
-
自旋锁表示当前获取锁失败后 不是立刻放弃CPU 而是快速频繁的再次访问锁的持有状态, 一旦锁对象被释放就能立刻获取到锁(通常都是轻量级锁)
自旋锁的效率更高, 但是会浪费一些CPU资源 (自旋相当于CPU在那空转)
公平锁 VS 非公平锁
-
这种情况就是如果已经有多个线程在等待一把锁的释放 当释放之后, 恰好又来了一个新的线程也要获取锁
-
公平锁: 保证之前先来的线程优先获取锁
-
非公平锁: 新来的线程直接获取到锁, 之前的线程还得接着等待
实现公平锁就需要付出一些额外的代价 所以公平锁的效率是略低于非公平锁的
可重入锁
- 一个线程针对同一把锁连续加锁俩次, 不会死锁, 这种就是可重入锁
可重入锁这就像是大门的三保险锁一样 我锁一层再锁一层 这种并不会造成我们死锁住自己 因为当我们想出去的时候又可以一层一层的开锁
死锁
-
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
-
我们常说的死锁有三个经典场景
- 一个线程一把锁 连续加锁俩次才 (保证使用的不是可重入锁)
- 俩个线程, 俩把锁, 相互获取对方的锁
- n个线程, n把锁, 哲学家就餐问题
- 死锁产生的四个必要条件:(较为理论简单了解)
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
CAS
-
CAS的全称是 compare and swap(字面意思就是比较交换) 他是基于硬件提供的一种基础指令, 也是基于这样的指令, 就可以实现一些特殊的功能(实现锁)
-
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理
-
简而言之,是因为硬件予以了支持,软件层面才能做到。
我们假设内存中的原数据val,旧的预期值new,需要修改的新值tmp。
- 比较 new 与 val 是否相等。(比较)
- 如果比较相等,将 tmp 写入 val。(交换)
- 返回操作是否成功。
- 可见当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。
CAS的使用
上图所示就是使用的CAS封装了一些原子类如下面代码示例第一个使用CSA的锁第二个不使用 显然结果第一个是线程安全的第二个线程是不安全的
import java.util.concurrent.atomic.AtomicInteger;
/**
-
Created with IntelliJ IDEA.
-
Description: If you don’t work hard, you will a loser.
-
User: Listen-Y.
-
Date: 2020-08-04
-
Time: 20:40
*/
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
atomicInteger.addAndGet(1);
}
}
};
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
atomicInteger.addAndGet(1);
}
}
};
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println(atomicInteger.get());
}
}
/**
-
Created with IntelliJ IDEA.
-
Description: If you don’t work hard, you will a loser.
-
User: Listen-Y.
-
Date: 2020-08-04
-
Time: 20:53
*/
public class Demo3 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
count++;
}
}
};
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
count++;
}
}
};
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println(count);
}
}
CAS的缺陷 ABA问题
- 这个问题就是加入现在有个num为0 有一个线程把他修改为1, 然后紧接着又有一个线程把他修改为0了 那此时仅仅通过CAS的比较是
无法区分的
- 解决这个问题就需要引入额外的信息 (给变量加一个版本号 每次进行修改 都递增版本号)
synchronize的原理
- synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性,原子性就是能够保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等该线程处理完数据后才能进行;可见性就是当一个线程在修改共享数据时,其他线程能够看到,保证可见性,volatile关键字也有这个功能;有序性就是,被synchronize锁住后的线程相当于单线程,在单线程环境jvm的重排序是不会改变程序运行结果的,可以防止重排序对多线程的影响。
以synchronize为例学习锁优化
编辑器和JVM配合进行的
锁消除
- 锁消除本质是以编辑器和JVM代码运行的情况智能的判断当前的锁有没有必要加 如果没有必要, 就会直接把锁干掉
/**
-
Created with IntelliJ IDEA.
-
Description: If you don’t work hard, you will a loser.
-
User: Listen-Y.
-
Date: 2020-08-04
-
Time: 21:21
*/
public class Demo4 {
public static void main(String[] args) {
StringBuffer buffer = new StringBuffer();
buffer.append(“listen”);
buffer.append(“listen”);
buffer.append(“listen”);
buffer.append(“listen”);
System.out.println(buffer);
}
}
- 到库中我们可以发现StringBuffer是加锁线程安全的 但是在我们上面写的代码中完全不用考虑线程安全问题 所以在实际运行的时候就把锁消除了
偏向锁
buffer.append(“listen”);
System.out.println(buffer);
}
}
- 到库中我们可以发现StringBuffer是加锁线程安全的 但是在我们上面写的代码中完全不用考虑线程安全问题 所以在实际运行的时候就把锁消除了