美文网首页
几个简单的锁

几个简单的锁

作者: 笔记本一号 | 来源:发表于2020-05-20 02:59 被阅读0次

    锁事七连

    1、要不要加锁?

    悲观锁

    悲观锁就是典型的互斥同步锁,总是悲观的认为每次获取并修改资源的同时就会有别人来对这个资源进行修改,所以每次获取并修改资源时就会对其进行上锁,这样别的线程就无法修改这个资源,直至它把锁释放了。而synchronized和lock的相关类就是悲观锁的实现,还有在实际开发中也有很多悲观锁在我们身边,例如Oracle中经常使用的select...for update也是悲观锁,执行期间如果没有提交或者回滚事务,否则期间是不允许其他人对这个表进行修改的
    悲观锁实例

    public class Beiguang {
        private static Lock lock=new ReentrantLock();
        public static void main(String[] args) {
            lock.lock();
            try {
                System.out.println("这个一个悲观锁的简单演示");
            }finally {
                lock.unlock();
            }
        }
    }
    

    乐观锁

    和悲观锁相反,乐观锁是典型的非互斥同步锁,总是乐观的认为每次获取并修改资源时是不会有其他人对这个资源进行修改的,所以每次获取并修改资源时都不会对其进行上锁,只是在更新数据时比对一下在修改数据的期间,数据有没有被其他线程修改过,如果没有被修改过那就证明在修改数据的期间没有其线程进来修改过,如果发现期间数据被修改过,那就会放弃数据的更新操作,而去选择其他的策略,例如重试、报错、放弃,而java的JUC的atomic包的实现原理是CAS,CAS就是乐观锁的实现,它所选择的策略就是重试(我有一篇博客写过C.A.S的原理,那里有CAS的源码分析,这里的源码我就不贴了,有兴趣的可以去瞄一瞄),我们使用的git也是乐观锁的原理,在提交代码时会检查远程分支有没有被修改过,有修改过的话就提交失败,需要我们解决冲突
    乐观锁 atomic包相关类的使用事例

    public class BingFa2 {
        private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2+1);
        public volatile int count=0;
        private static AtomicIntegerFieldUpdater<BingFa2> updater=
                AtomicIntegerFieldUpdater.newUpdater(BingFa2.class,"count");
        CountDownLatch countDownLatch = new CountDownLatch(1000);
    
        static BingFa2 bingFa2=new BingFa2();
        public void test1() throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                executorService.submit(() -> {
                    int qw;
                    do {
                      //从底层拿值,期望值
                         qw = updater.get(bingFa2);
                      //加一
                    }while (!updater.compareAndSet(bingFa2,qw,count+1));
                  countDownLatch.countDown();
                });
            }
            executorService.shutdown();
            countDownLatch.await();
            System.out.println(updater.get(bingFa2));
        }
    
        public static void main(String[] args) throws InterruptedException {
            bingFa2.test1();
        }
    }
    

    对比:一般来说悲观锁的开销比乐观锁大,但是如果乐观锁在遇到失败时,自旋时间过长或者重试的次数太多,就会造成资源消耗过多,从而开销就会超过悲观锁。
    适用场景:悲观锁适用于用于资源竞争激烈、并发的io操作量多、代码中的循环量大、持有锁时间长的情景,避免了等待一些无意义的自旋和重试带给程序的巨大消耗;乐观锁相反适用于资源竞争小、并发的io操作量少、代码中的循环量小、持有锁时间短的情景

    2、线程获取一把锁要不要排队?

    公平锁

    公平锁指的是线程按照线程请求的顺序去获得锁。公平锁可以使得所有的线程都能得到资源,不会饿死在队列中。但是队列里面除了排在第一的线程能获得锁,其他的线程都会处于阻塞,加上cpu唤醒处于阻塞的线程的是需要时间的,即时唤醒时间长,在唤醒线程的时间内系统处于空档期,即使这样其他处于活跃状态的线程也不能去拿锁执行任务,所以系统会处于空档期,造成了造成效率和吞吐量会的下降。ReentrantLock默认是非公平的,通过设置ReentrantLock(true)实现公平锁

    非公平锁

    非公平锁是Java默认的策略,指的是线程可以在合适的时机不按照线程请求的顺序去获取锁。合适的时机指的是在唤醒下一个排队的线程期间,其他处于活跃状态的线程突然来获取到了锁,避免了唤醒线程带来的空档期而造成对系统资源的浪费,非公平锁旨在提高系统的效率和吞吐量,但是也有可能会使队列中的一些线程长时间获取不到锁而导致饿死,ReentrantLock默认就是非公平锁

    从源码分析公平锁和非公平锁

    点开ReentrantLock查看源码

    //通过传递参数设置锁的公平和非公平
     public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    //这ReentrantLock的公平锁的方法
     protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
    //这里主要是通过hasQueuedPredecessors()是AQS内的方法,
    //判断当前线程的前面是否有线程在排队,只是利用链表实现的
    //没有则尝试去获取这把锁
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
              .......
    //这ReentrantLock的非公平锁的方法
     final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
    //我们看与公平锁的方法少了一步hasQueuedPredecessors()队列的判断
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
          ...............
    

    3、要不要共享一把锁?

    共享锁

    共享锁又称读锁,指的是线程获取共享锁之后,只能做读操作,不能做写操作,其他的线程可以同时获取共享锁,但是也是只能读不能写。由于多线程对资源进行读操作时,是不存在并发问题的,如果一味的将线程资源进行同步锁定的话,是会影响性能的,所以在读的地方使用读锁可以提高性能,可以通过new ReentrantReadWriteLock().readLock()创建读锁

    排它锁

    排它锁又名独享锁、独占锁、写锁,指的是线程获取锁之后既能对资源进行读操作,也能进行写操作,而且只能由获得该锁的线程才能拥有读写资源的权限,其他线程都得把手中的读锁和写锁释放,保证了线程的安全线,synchronized就是一个排它锁,也可以通过new ReentrantReadWriteLock().readLock()创建写锁

    读写锁的规则

    1、多个线程如果只申请读锁,都可以同时获取。
    2、如果有线程持有读锁,同时有其他线程申请写锁是申请不到的,申请写锁的线程会一直等待着线程把读锁释放。
    3、如果有线程持有写锁,同时有其他线程申请写锁或读锁,都是申请不到的,申请写锁或读锁的线程会一直等待着线程把写锁释放。
    总结就是:要么多个或一个线程拥有读锁,要么只有一个线程拥有写锁,两种情况不能同时出现

    读写锁的公平性

    一、在公平锁的条件下

    读锁和写锁全都不予许插队

    二、在非公平锁的条件下

    在读写锁中默认情况下的
    1、写锁可以插队
    写锁可以插队的原因在于,写锁只能由一个线程持有,当线程要进行写操作时试图插队争夺写锁的时候发现写锁已被其他的线程持有,我们已经提到写锁只能被一个线程拥有,如果抢不到写锁,这个线程就得排队,这个线程等待前面的线程把锁释放了,所以写锁插队的难度很大,不会轻易造成其他线程饿死的状况
    2、读锁不予许插写锁的队
    读锁不予许插写锁的队的原因在于读操作可以多线程同时持有读锁,假如读锁可以随意乱插队,那么就会出现这种情况,一个需要写锁的线程在排队等待一个读锁释放,后面一直有需要读锁的线程在插队,而这些线程是可以马上获取读锁的,所以只要前面是需要读锁的线程它就能马上插队,所以需要读锁的线程如果有可以插需要写锁线程的队的权限的话,那么需要写锁的线程就可能会被饿死。

    ReentrantReadWriteLock源码分析

    1、公平的情况

    //这个是ReentrantReadWriteLock的内部类,是公平的
     static final class FairSync extends Sync {
            private static final long serialVersionUID = -2274990926593161451L;
    //写时判断队列中是否有线程等待 ,有则阻塞排队  ,   
    final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
    //读时判断队列中是否有线程等待  ,有则阻塞排队           
            final boolean readerShouldBlock() {
                return hasQueuedPredecessors();
            }
        }
    

    2、非公平的情况

    static final class NonfairSync extends Sync {
            private static final long serialVersionUID = -8159625535654395037L;
    //非公平的写操作,直接返回false,指的是可以插队
            final boolean writerShouldBlock() {
                return false; 
            }
    //非公平的读操作,直接返回false,指的是可以插队
            final boolean readerShouldBlock() {      
    //调用的apparentlyFirstQueuedIsExclusive()方法
    //方法的作用是判断队列前的是否是读锁,如果是返回true,不是就返回false
    //意思是如果队列前的是读锁,就乖乖排队,不许插队
                return apparentlyFirstQueuedIsExclusive();
            }
        }
    
    

    在源码中我们也证实了写锁可以插队,读锁不可以插写锁的队的特点

    读写锁的升降级

    在java的读写锁ReentrantReadWriteLock中是支持写锁的降级为读锁,不予许读锁升级为写锁。这样设计的目的是为了提升系统的效率,避免死锁的发生,因为一个线程持有写锁时其它线程都是得放弃手中的读写锁的,但是当线程完成了写操作后要进行读操作时,由于写锁是一个排斥其他线程的锁,读操作是可以和其他线程共享而不会发生并发问题的,所以拿写锁进行读操作是一种浪费资源的现象,但是线程又不肯放弃手中的锁,此时如果将线程的写锁降级为读锁,就可以解决这个问题。相反如果读锁支持升级为写锁,会出现这种情况:多个持有读锁的线程在遇到写操作时都想升级为写锁,但是成为写锁的条件是其他线程都得放弃手中的写锁和读锁,所以可能会导致线程都在等待着其他线程把手中的锁释放,但是都不肯放弃自己手中的锁,而导致了死锁
    尝试读锁升级为写锁的演示

    public class WriteReadLock {
        private final static ReentrantReadWriteLock rrw=new ReentrantReadWriteLock();
        private final static ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
        private final static ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
     private static void ReadUp(){
            readLock.lock();
            String name = Thread.currentThread().getName();
            try {
                System.out.println(name+":读锁等待升级为写锁");
                writeLock.lock();
                System.out.println(name+":读锁升级为写锁");
            }finally {
                writeLock.unlock();
                System.out.println(name+":演示结束");
                readLock.unlock();
            }
        }
    public static void main(String[] args) {
            Thread thread=new Thread(()->ReadUp());
            Thread thread1=new Thread(()->ReadUp());
            Thread thread2=new Thread(()->ReadUp());
            thread.start();
            thread1.start();
            thread2.start();
        }
    

    我们看到结果,发生了线程的阻塞状态,各个线程都在等其他线程释放锁,自己有不愿意释放

    image.png

    尝试写锁降级为读锁的演示

    private static void WriteFall(){
            writeLock.lock();
            String name = Thread.currentThread().getName();
            try {
                System.out.println(name+":写锁等待降级为读书");
                readLock.lock();
                System.out.println(name+":写锁降级为读锁");
            }finally {
                readLock.unlock();
                System.out.println(name+":演示结束");
                writeLock.unlock();
            }
        }
     public static void main(String[] args) {
            Thread thread=new Thread(()->WriteFall());
            Thread thread1=new Thread(()->WriteFall());
            Thread thread2=new Thread(()->WriteFall());
            thread.start();
            thread1.start();
            thread2.start();
        }
    

    我们看到结果是写锁是按照线程顺序执行的证明线程在拿到写锁后,其他的线程都在排队,而且写锁也降级为了读锁,证明了写锁支持降级为读锁


    image.png

    4、一个线程能不能重复获取一把锁?

    可重入锁

    可重入锁又称为递归锁指的是同一个线程在获取一把锁的情况下无须释放这把锁就可以多次获取到这一把锁,这个性质避免了死锁的发生,例如我们在执行已经上锁的service1()方法的过程中获取到了锁,而service1()方法调用了同样上了这把锁的service2()方法,依然能获取到这把锁,从而能进入service2()的临界区,如果这把锁是不可重入的,那么就发生这种情况:线程获取了service1()方法的锁,在service1()的临界区内想调用上锁的service2(),那么线程必须释放service1()获取的锁,否则线程会一直拿不到锁而处于等待释放锁状态,从而导致了死锁。java中的synchronized和lock的相关类也是可重入锁

    public class KeChongRu {
        private static ReentrantLock lock=new ReentrantLock();
        private static int count=0;
        public static void test(){
            lock.lock();
            try {
                //记录线程在释放锁之间重复获取过几次锁
                //如果没有重复获取10次锁,就继续递归下去,直至重复获取10次锁
                if ((count=lock.getHoldCount())<10){
                test();
                }
            }finally {
                lock.lock(); 
            }
        }
        public static void main(String[] args) {
            test();
            System.out.println(count);
        }
    }
    
    
    image.png

    不可重入锁

    与可重入锁相反的是,不可重入锁就是如果当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取这个锁时,就会获取不到而被阻塞。这种情况容易引发死锁情况,在代码中需要我们重复的对锁进行上锁解锁操作

    从源码分析ReentrantLock可重入性的原理

    //ReentrantLock可重入的实现方法
       final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
                final Thread current = Thread.currentThread();
    //记录上锁次数,每次上锁都会加1
                int c = getState();
                if (c == 0) {
    //acquires通常是1,使用原子性的CAS安全的改变线程的同步状态,
    //修改获取锁的数量,持有锁数量修改为acquires
    //使线程的持有锁能状态加+acquires,compareAndSetState能保证这个操作是安全的
                    if (compareAndSetState(0, acquires)) {
    //传入当前线程,将锁设置为当前线程所持有的状态
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
    //判断锁的持有者是否是当前线程
                else if (current == getExclusiveOwnerThread()) {
    //持有锁的数量加acquires,acquires一般为1
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
    //将线程的持有锁数量重新设值
                    setState(nextc);
                    return true;
                }
                return false;
            }
    //这个是unlock调用的方法,主要使用了tryRelease,去释放一次锁
    public void unlock() {
            sync.release(1);
        }
    //将释放一次锁
            protected final boolean tryRelease(int releases) {
    //将持有锁的次数减releases,在上面我们看到releases一般是1
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
    //将锁释放完后
                if (c == 0) {
                    free = true;
    //把当前线程持有的状态该为null,就是让当前线程没有持有这把锁
                    setExclusiveOwnerThread(null);
                }
    //重新设值线程的锁持有数
                setState(c);
                return free;
            }
    
    自己实现一个可重入锁
    public class MyLock_KeChongRu {
        private boolean isFirstGetLock=false;
        private Thread hasLockThread=null;
        private int hasLockCount=0;
        final Object olock=new Object();
        public  void lock() throws Exception {
            synchronized(olock) {
                Thread t = Thread.currentThread();
                isHasProsse(t);
                hasLockThread = t;
                hasLockCount++;
                isFirstGetLock = true;
            }
        }
        //判断线程是否是有锁的
        private void isHasProsse(Thread t){
            try {
               //isFirstGetLock为false表示没有任何线程拿过锁
               // isFirstGetLock为true表示线程拿过了锁,并判断是不是当前线程持有的
                //如果不是当前线程持有,则让线程让出锁和cpu时间
                if (isFirstGetLock&&hasLockThread!=t){
                    //将线程的锁及CPU时间让出来
                    olock.wait();
                }
            }catch (Exception e){
                System.out.println(e);
            }
    
        }
        public  void unlock(){
            synchronized(olock) {
                Thread t = Thread.currentThread();
                if (hasLockCount < 0) {
                    return;
                }
                if (isFirstGetLock && hasLockThread == t) {
                    hasLockCount--;
                }
                //当前线程已经把锁归还完,唤醒其他线程让他去争夺锁
                if (hasLockCount == 0) {
                    hasLockThread = null;
                    isFirstGetLock = false;
                    olock.notify();
                }
            }
        }
    
        public int getHasLockCount() {
            return hasLockCount;
        }
    }
    
    

    5、该如何等待一把锁?

    自旋锁

    线程的挂起和恢复是需要切换CPU,所以会消耗一定的CPU时间以致给系统带来了开销,要是为了一段执行时间很短的代码而改变线程持有锁的状态,为了这段很短的时间而去阻塞和唤醒一个线程对系统而言是一件得不偿失的事情。而自旋锁就是在持有锁的线程在执行任务时,要求下一个线程进行自旋,在一定的时间内不放弃CPU时间,等待前面的线程释放锁,如果持有锁的线程在自旋时间内释放了锁,并被自旋线程直接拿到了锁,就避免了线程切换状态的开销。自旋锁适用于持有锁时间短的场景,也就是同步代码块执行时间短的场景,自旋锁能提高系统的效率,但是在锁持有时间长的场景下回浪费CPU资源。自旋锁的自旋次数默认是10次,可以通过-XX:PreBlockSpin来更改次数。在JUC的atomic包的CAS使用的策略就是自旋

    非自旋锁

    和自旋锁相反,就是在线程没拿到锁的情况下,直接把线程阻塞,直至轮到线程拿锁时才会唤醒,非自旋锁适用于锁持有时间长的场景

    源码分析自旋锁的原理

    //unsafe类的一个方法,使用了CAS 也就是compareAndSwapInt()方法
      public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
    //计算出内存中修改更新的变量实际的值
                var5 = this.getIntVolatile(var1, var2);
    //对比实际的值和期望的值是否相等,不相等则更新期望值,
    //重新获取内存中的值在执行判断,如果和期望值相等的话则进行更新操作,
    //CAS是线程安全的一种实现方法,它是一种乐观锁的策略
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
            return var5;
        }
    

    从源码中我们看到了,JUC的atomic包的相关类使用了自选策略,任何修改失败就会执行do-while一直循环重试

    6、等待这把锁的过程能不能中断?

    可中断锁

    在线程持有锁执行任务的过程中,由于时间过长,让另一个正在等待这把锁的线程放弃等待,让它去做其他的事情,这个就是可中断锁,这里中断的是等待锁的线程,而不是中断持有锁的线程,ReentrantLock就是可中断锁 tryLock()和lockInterruptibly()提供了中断等待的功能

    不可中断锁

    不可中断锁和可中断锁相反,在线程排队等待锁的过程中不可以中断等待,只能乖乖等待锁,synchronized就是不可中断锁

    相关文章

      网友评论

          本文标题:几个简单的锁

          本文链接:https://www.haomeiwen.com/subject/cussohtx.html