java线程锁二
java锁相关二
1.乐观锁和悲观锁
乐观锁概念:对于多线程的并发操作,乐观锁一直保持“乐观态度”,认为获取锁的线程在读写数据时,其它线程不会来干扰,所以不会添加锁,只会在修改数据之前去判断有无别的线程修改了
数据(比如通过版本号来判断),如果当前数据没有被更新,则将自己修改的结果写入。如果被修改了,则根据不同实现方式执行不同操作(重新获取数据或者报错)。
悲观锁概念:对于并发操作,悲观锁则认为当前线程修改数据的时候,一定会有其它线程修改数据,所以获取数据的时候会先加锁,这样可以确保其它线程无法对此数据进行修改。
synchronized关键字和Lock的实现类都是悲观锁。
适用场景:
- 悲观锁适合写操作多的场景,因为先加锁可以确保数据安全,在一个线程修改数据时不会有其它的线程修改数据
- 乐观锁适合读比较多的场景,因为不加锁的操作能够大大提高程序性能,并且读操作不会修改数据,不用担心因为不加锁导致数据出错
//java中synchronized关键字和Lock实现类都是悲观锁
//悲观锁实现方式
private synchronized void lockTest(){
//对同步资源操作
}
private Lock lock = new ReentrantLock();
//多个线程使用同一个锁
private void lockTest2(){
lock.lock();
//对同步资源操作
lock.unlock();
//释放锁
}
//乐观锁实现方式
private AtomicInteger atomicInteger = new AtomicInteger();
//多个线程使用同一个AtomicInteger
private void lockTest3(){
atomicInteger.incrementAndGet();//自增方法
}
多个线程使用同一个AtomicInteger对象
int count = 10;
do{
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int result = lo.lockTest3();
System.out.println(result);
}).start();
count--;
}while (count > 0);
new Thread(()->{
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(lo.atomicInteger.get());
}).start();
}
输出:
3
2
4
5
7
1
6
8
9
10
10
自增的最终结果是10,说明数据没有错误
源码:
AtomicInteger使用了CAS来实现乐观锁
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
该算法中有三个操作数,v(需要读写的内存值),A(进行比较的值),B(要写入的新值),每次修改数据时都会将v与A进行比较,来保证原子性,当且仅当v的值等于A时,
才会将新的值B写入,否则不会执行写入,而是进行重试
再来看源码,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI(Java Native Interface,java本地的接口)里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。
CAS存在的问题:
- ABA问题,如果原来数据的值是A,在修改数据的时候,被其它线程修改为B,然后又被修改回A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。为了解决这种情况,可以通过添加一个
版本号,通过版本号是否改变来判断值是否发生过变化 - 时间开销很大,如果CAS长时间不成功,会一直自旋,开销很大
- 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
2.可重入锁和不可重入锁
可重入锁又称递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,但是前提是在同一个对象或者同一个类中。 Java中ReentrantLock和synchronized都是可重入锁。 实现例子:public class LockTest2 {
private final Object obj = new Object();
//这种写法,锁的范围跟括号里写this是相同的
public void testLock() {
synchronized (obj){
System.out.println("aaa");
testLock2();//可重入锁
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "finish");
}
public void testLock2(){
synchronized (obj){
System.out.println("/xxxxxx");
}
}
public static void main(String[] args) {
LockTest2 lt1 = new LockTest2();
new Thread(()->{
lt1.testLock();
}).start();
new Thread(()->{
lt1.testLock();
}).start();
System.out.println("ok");
}
}
当线程调用testLock方法时,获取该对象的锁,这个方法内调用了 testLock2方法,该线程直接获取了该方法的锁,这就是可重入锁
如果是不可重入锁,由于在外层方法就获取了对象的锁,执行内层方法需要释放外层方法的锁,那么就会造成死锁,可重入锁可以一定程度上避免死锁的问题。
可重入锁ReentrantLock和非可重入锁NonReentrantLock的区别:
ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。
线程获取锁时,可重入锁会尝试获取并且更新status的值,如果status为0(表示没有其他线程执行该同步代码块),把status值设为1,线程成功获取锁,开始执行。
如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。
非可重入锁尝试获取锁时,如果status不为0,则会直接判断为获取锁失败,导致线程阻塞。
在释放锁的时候,当线程在持有当前锁的情况下,可重入锁会使status值-1,表示释放锁,如果status-1的值为0,那么就会判断当前线程释放了所有的锁,置status的值为0。
非可重入锁释放锁时,只要线程在持有当前锁的情况下,就会释放锁,并且直接置status的值为0。
3.公平锁和非公平锁
公平锁指多个线程按照申请锁的顺序来获取锁,就好比排队的时候每个人都遵守规则,按先来后到的顺序排队,公平锁可以保证所有申请锁的线程最终都可以获取锁,缺点就是除了等待队列中的第一个线程 其它线程都会被阻塞,CPU唤醒阻塞进程的开销大,所以使用公平锁会导致程序效率变低。 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,但是也可能会出现后来的线程一直获取锁,队列 中第一线程无法一直获取锁的情况,就好比排队的时候后来的人插队,导致队首的人一直轮不到的情况。非公平锁的优点是可以减少唤醒阻塞线程的开销,程序的吞吐量大,由于线程有几率不阻塞直接获取 锁,cpu不必要唤醒所有的线程。缺点是队列中的线程有可能被饿死,长时间无法获取锁。synchronized是非公平锁,lock可以指定是否为公平锁,
private Lock lock = new ReentrantLock(true);//指定是否为公平锁
ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁
公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。
这个方法主要判断同步队列中的等待线程是否有多个,并且当前线程是否在第一个,如果是则返回true,否则返回false
公平锁就是通过同步队列按照顺序来实现多个线程按顺序获取锁,实现公平性,非公平锁不用考虑顺序问题,后来的线程也可以直接获取锁。
参考博客:https://www.cnblogs.com/jyroy/p/11365935.html
https://tech.meituan.com/2018/11/15/java-lock.html
参考博客:https://www.cnblogs.com/qjjazry/p/6581568.html