目录
一、锁策略
1、乐观锁和悲观锁
乐观锁:在加锁之前预估当前出现锁冲突的概率不大,加锁过程做的事情比较少,加锁速度快,但也会引起其他问题,例如:消耗更多的cpu资源。
悲观锁:在加锁之前预估当前出现锁冲突的概率较大,加锁过程做的事情就会较多,加锁速度慢,但是整个过程不容易出现其他问题。
2、轻量级锁和重量级锁
轻量级锁:加锁的开销小,加锁速度快。一般就是乐观锁。
重量级锁:加锁的开销大,加锁速度慢。一般就是悲观锁。
3、自旋锁和挂起等待锁
自旋锁:轻量级锁的一种典型实现。加锁时搭配while循环使用,当加锁成功时,自然结束循环;当加锁不成功时,也不会阻塞放弃cpu,而是进行新一轮的尝试加锁。这个反复快速执行的过程称为“自旋”,其他线程一旦释放锁,使用自旋锁的线程能第一时间拿到锁。自旋锁也是乐观锁,出现锁冲突的概率不大,循环执行次数少,cpu消耗少。如果锁冲突概率大,就会一直自旋,浪费cpu.
挂起等待锁:重量级锁的一种典型实现。当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁。所以该线程真正拿到锁的时间就会长一些。可用于锁冲突概率较大的情况下。
4、普通互斥锁和读写锁
普通互斥锁:同一时刻同一个对象,只能有一个线程进行访问,当其他线程想获取锁访问时,该线程获取锁失败,线程进入睡眠,等待其他线程释放锁后被唤醒。
读写锁:将加锁分成两种情况:加读锁和加写锁。读锁和读锁之间不会出现锁冲突,允许多个线程同时获得读操作;写锁和写锁之间会出现锁冲突,同一时刻同一对象只能有一个线程获取到写操作,其他线程获取失败只能进入睡眠状态,当锁被释放时就会被唤醒;读锁和写锁之间会出现锁冲突,写锁会阻塞其他读锁,当线程获取到写锁后,其他线程想获取读锁也会被阻塞。
为什么要引入读写锁???synchronized锁不是读写锁,对于一些只涉及到读的操作,本身就是线程安全的,加synchronized,读操作之间互斥,会降低效率。但是对读操作不加锁也不行,对于有些线程,既有读的操作,也有写的操作,会存在线程不安全问题,需要加锁。读写锁就很好的解决了上述问题。
5、公平锁和非公平锁
公平的定义:先来后到的顺序。
要想实现公平锁,就需要额外引入数据结构(队列,存放锁的先后顺序)。使用公平锁,可以解决线程饿死问题。
synchronized锁是非公平锁,线程的调度是无序的。
6、可重入锁和不可重入锁
可重入锁:对于同一个线程,加锁两次,不会出现死锁问题,只会锁计数器+-1;
不可重入锁:对于同一个线程,加锁两次,会出现死锁问题。
二、synchronized锁和系统原生锁特点
1、synchronized锁
乐观锁/悲观锁自适应;轻量级锁/重量级锁自适应;自旋锁/挂起等待锁自适应;是非公平锁;可重入锁;普通互斥锁(不是读写锁)
2、系统原生锁(Linux提供的mutex锁)
悲观锁;重量级锁;挂起等待锁;非公平锁;不可重入锁;普通互斥锁(不是读写锁)
三、synchronize锁内部原理
1、升级过程
①偏向锁阶段
该阶段,并没有加锁。由于没有其他线程竞争,只是做了一个非常轻量的标记。如果一直没有锁竞争,整个过程就把加锁的操作省略了,也不会有互斥现象。
②轻量级锁阶段
一旦有其他线程来竞争锁,该线程就会在另一个线程之前获取到锁,将锁升级为轻量级锁。synchronized内部也会统计当前锁对象上有多少个线程在竞争锁,有竞争但数目不多,就是轻量级锁,其他未获取到锁对象的线程自旋获取锁。
③重量级锁阶段
synchronized内部统计发现当前锁对象上有多个线程在竞争锁,大量线程自旋获取锁的话cpu消耗就多了,此时拿不到锁的线程就转为重量级锁,不再自旋获取锁,而是挂起等待,让出cpu。当线程释放锁后,再随机唤醒一个阻塞等待的线程。
注:该过程只能升级,不能降级!!!
2、锁消除
synchronized内置的一种优化策略。在编译代码时,发现代码不需要加锁,就会自动把锁干掉。比如:只有一个线程或者线程里没有涉及到成员变量的修改等等。
3、锁粗化
针对同一个锁对象,把多个细粒度的锁合并成一个粗粒度的锁。一般情况下,synchronized{}大括号里代码越少,就认为锁的粒度越细,代码越多,就认为锁的粒度越粗。粗化是为了提高效率,细化有利于多个线程并发执行。
四、CAS(compare and swap)
1、概念
是一个特殊的cpu指令,完成的工作就是比较和交换。
伪代码:
address是内存地址,expectValue是expect寄存器的值,swapValue是swap寄存器的值。将内存地址的值与expect寄存器的值比较,如相同则将内存地址的值与swapValue的值交换,并且返回true;如不相同,则返回false。
以上cpu指令是原子的。
2、应用
考虑到CAS指令是原子的,基于CAS指令,可以考虑实现多线程的线程安全问题。
public class test {
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (lock){
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (lock){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
以上对于线程不安全问题采用的是加锁,将count++变成原子的,基于CAS,不加锁实现上述代码.
java标准库中对于CAS进行了封装,放在了unsafe包里,提供了一些工具类可以直接使用。最主要的工具是原子类。
针对以上对象进行多线程修改时,就是线程安全的。
import java.util.concurrent.atomic.AtomicInteger;
public class test1 {
private static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement(); //相当于count++
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement(); //相当于count++
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
}
以上count是原子类整型,多个线程对他进行修改时是线程安全的。
针对count对象还有其他操作:
3、CAS实现原理
上述过程并未加锁,CAS是怎么实现线程安全的???
针对上述实现count++,伪代码:
value是内存中的值,oldValue是寄存器中的值,若发现内存中的值和寄存器中的值一样,就交换,将oldValue+1的值与value值交换,返回True,结束循环;若发现内存中的值和寄存器中的值不一样(其他线程改过),进入循环,重新获取内存值赋值于寄存器,再次比较交换。
之前线程不安全是因为内存中的值变了,但是寄存器中的值未改变。CAS指令可以识别出内存中的值是否改变,未改变就交换,改变了就重新获取内存值,然后再比较交换。
以上比较和交换是自旋式的,会消耗更多的cpu资源,但不会引起线程阻塞。
4、CAS的ABA问题
(1)什么是ABA问题
假设存在两个线程t1、t2,他们拥有共享变量value,value的初始值为A,线程t1想使用CAS把value修改为C,此时就需要先读取value的值,并赋值给oldValue寄存器中;判断当前value的值是否等于oldValue,若等于,将value值交换变为C。但是在进行这两个操作之间,线程t2可能把value的值修改为B,又从B修改为A。线程t1在进行判断时,无法判断value始终是这个值还是经历了变化。
(2)ABA问题导致的BUG
假设现在余额是100,取款50元,有两个线程t1、t2来执行这个任务。
正常情况:线程t1执行CAS(value,oldvalue,oldvalue-50),value==oldvalue,value与oldvalue-50交换,变为50,返回true;线程t2执行CAS(value,oldvalue,oldvalue-50),value!=oldvalue,执行失败,返回false。即:总体就取款了50
异常情况:线程t1执行CAS(value,oldvalue,oldvalue-50),value==oldvalue,value与oldvalue-50交换,变为50,返回true;此时又来了个线程t3执行CAS(value,oldvalue,oldvalue+50),value与oldvalue+50交换,变为100,返回true;然后线程t2执行CAS(value,oldvalue,oldvalue-50),value==oldvalue,value与oldvalue-50交换,变为50,返回true;即:总体就取款了两个50。
(3)如何解决ABA问题
给要修改的值引入版本号,在比较当前value的值和oldvalue值是否相同的同时,也要比较版本号是否符合预期。在value的值和oldvalue值相同时,如当前版本号和之前读到的版本号一样,则修改数据,并将版本号+1,如当前版本号高于之前读到的版本号,认为数据已被修改过,操作失败。