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存在的问题:

  1. ABA问题,如果原来数据的值是A,在修改数据的时候,被其它线程修改为B,然后又被修改回A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。为了解决这种情况,可以通过添加一个
    版本号,通过版本号是否改变来判断值是否发生过变化
  2. 时间开销很大,如果CAS长时间不成功,会一直自旋,开销很大
  3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,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

posted @ 2021-01-24 14:36  TidalCoast  阅读(69)  评论(0编辑  收藏  举报