来聊聊并发编程中的锁的概念

来聊聊并发编程中的锁的概念,第1张

来聊聊并发编程中的锁的概念 Lock接口 为什么需要Lock,它于sync锁的优劣

锁是一种解决资源共享问题的解决方案,相比于sync锁,lock锁增加了一些更高级的功能,例如增加锁等待,锁中断可释放以及锁重入等功能。
但这并不能表明,lock锁是sync锁的替代品,他俩都有各自的适用场合。

lock接口几个常见api示例 lock和unlock

如下所示,这就是lock锁的基础使用方式,需要注意的是lock不像sync锁那样会在出现异常时自动释放锁,对此jdk要给我们提供了一段lock的使用基础示例

 class X {
 *   private final ReentrantLock lock = new ReentrantLock();
 *   // ...
 *
 *   public void m() {
 *     lock.lock();  // block until condition holds
 *     try {
 *       // ... method body
 *     } finally {
 *       lock.unlock()
 *     }
 *   }
 * }}

只有按照上文这样,我们才能保证正确的手动释放锁

public class LockDemo {
    private static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            int i=1/0;
            System.out.println("拿到锁,执行业务逻辑");
        }catch (Exception e){
            System.out.println("异常了");
        }finally {
            lock.unlock();
        }

        System.out.println(lock.isLocked());
    }
}
tryLock

相比于普通的lock来说,tryLock相对更加强大一些,tryLock可以根据是否可以取到锁进而让程序按需进行下一步。
而且tryLock可以立即返回,如果拿得到就拿锁并返回true,反之返回false。
同时它也支持在指定时间内等待锁。

package lock.lock;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class TryLockDemo implements Runnable {

    private int flag;

    //注意使用static 否则锁的粒度用错了会导致无法锁住彼此
    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public int getFlag() {
        return flag;
    }

    public void setFlag(int flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        while (true) {
            //先取锁1再取锁2
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程1拿到了锁1");
                            //睡一会,保证线程2拿锁锁2
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程1取到锁2");
                                    System.out.println("线程1拿到两把锁,执行业务逻辑了。。。。");
                                    break;
                                } finally {
                                    lock2.unlock();

                                }
                            } else {
                                System.out.println("线程1取锁2失败,再次尝试中");
                            }
                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));

                        }


                    } else {
                        System.out.println("线程1取锁1失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            } else {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                       try{
                           System.out.println("线程2拿到了锁2");
                           Thread.sleep(new Random().nextInt(1000));
                           if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                               try {
                                   System.out.println("线程2取到锁1");
                                   System.out.println("线程2拿到两把锁,执行业务逻辑了。。。。");
                                   break;
                               } finally {
                                   lock1.unlock();

                               }
                           } else {
                               System.out.println("线程2取锁失败,再次尝试中");
                           }
                       }finally {
                           lock2.unlock();
                           //睡一会把锁让别人取
                           Thread.sleep(new Random().nextInt(1000));

                       }

                    } else {
                        System.out.println("线程2取锁2失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

package lock.lock;

public class TestTryLock {
    public static void main(String[] args) {
        TryLockDemo tryLockDemo=new TryLockDemo();
        tryLockDemo.setFlag(1);

        TryLockDemo tryLockDemo2=new TryLockDemo();
        tryLockDemo2.setFlag(2);

        new Thread(tryLockDemo).start();
        new Thread(tryLockDemo2).start();
    }

}

lockInterruptibly

这个api相当于将tryLock时间设置为无限,同时支持的等待锁过程中,将线程中断。

package lock.lock;

import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptiblyDemo implements Runnable {


    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 尝试取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + " 取锁成功");
                Thread.sleep(5000);

            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 执行业务逻辑时被中断");

            } finally {
                lock.unlock();
            }

        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "尝试取锁时被中断");
        }

    }
}

package lock.lock;

public class LockInterruptiblyTest {

    public static void main(String[] args) throws InterruptedException {
        LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
        Thread thread0 = new Thread(lockInterruptiblyDemo);
        thread0.start();
        Thread thread1 = new Thread(lockInterruptiblyDemo);
        thread1.start();
        Thread.sleep(2000);
        thread1.interrupt();
    }
}

锁的可见性保证

如下图所示,根据happens-before原则,前一个线程 *** 作的结果,后一个线程是都可以看到的
关于happens-before可以移步笔者这篇文章
并发编程必知必会——Happens-before

锁的分类 简介

按照不同的方式分类可归纳为如下几种

悲观锁和乐观锁 悲观锁

悲观锁认为自己在修改数据过程中,其他人很可能会过来修改数据,为了保证数据的准确性,他会在自己修改数据时候持有锁,并且其他线程在他释放锁之前,无法持有这把锁。
在java中sync锁和lock锁都是悲观锁。

乐观锁

乐观锁认为自己的修改数据时不会有其他人会修改数据,所以他每次修改数据后会判断修改前的数据是否被修改过,如果没有就修改数据。
在java中乐观锁常常用cas来实现。
如下代码所示,他就是乐观锁的简单实现

 public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
    }
悲观锁和乐观锁的比较
    悲观锁的开销远高于乐观锁,但他确实一劳永逸的,临界区持有锁的时间就算越来越长也不会对互斥锁有任何的影响。反之乐观锁假如持有锁的时间越来越长的话,其他等待线程的自选时间也会增加,从而导致资源消耗愈发严重。悲观更适合那些写多读少的情况,而乐观锁更适合那些读锁写少的情况。
重入锁和非可重入锁 可重入锁示例

相比于非可重入锁来说,可重入锁支持再次拿锁之前无需释放锁,具体案例如下所示

public class MyRecursionDemo {
    private ReentrantLock lock = new ReentrantLock();

    public void accessResource() {
        lock.lock();
        try {
            System.out.println("第"+lock.getHoldCount()+"处理资源中");
            if (lock.getHoldCount() < 5) {
                System.out.println("当前线程是否是持有这把锁的线程"+lock.isHeldByCurrentThread());
                System.out.println("当前等待队列长度"+lock.getQueueLength());
                System.out.println("递归处理资源中");
                accessResource();
            }
        } catch (Exception e) {
        } finally {
            System.out.println("处理结束,释放可重入锁");
            lock.unlock();
        }

    }
}
public class MyRecursionDemoTest {
    public static void main(String[] args) {
        MyRecursionDemo myRecursionDemo=new MyRecursionDemo();
        myRecursionDemo.accessResource();
    }
}

非可重入锁

sync就是常见的非可重入锁,他想获取另一个锁之前必须得先释放当前锁。

源码解析可重入锁和非可重入锁区别

如下所示,我们通过debug发现,可重入锁进行锁定逻辑时,会走到如下逻辑中

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果当前线程仍然持有这把锁,记录一下持有锁的次数 并返回拿锁成功
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

相比之下非可重入锁的逻辑就比较简单了,如下所示,除非这把锁没人用,否则这把锁你就不能持有

两者优劣

相比之下,非可重入锁无需释放锁就能持有别的锁,所以非可重入锁可避免线程死锁的发生,但是安全性相对非可重入锁来说会差很多。

公平锁和非公平锁 简介

公平锁可以保证线程持锁顺序会有序进行,而非公平锁则可以在某些特定情况下让线程可以插队
非公平锁的设计初衷也很明显,非公平锁的设计就是为了在线程唤醒期间的空档期让其他线程可以插队,从而提高程序运行效率的最佳解决方案。

公平锁和非公平锁的示例

如下实例所示,这是一段公平锁和非公平锁的简单实例,通过一个锁类,然后定义一个任务类,使用不同的线程运行这个任务,就可以看到公平锁和非公平锁的是使用运行结果

package lock.reentrantlock;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class MyPrintQueue {

    
    private ReentrantLock lock = new ReentrantLock(true);

    public void printStr() {
        lock.lock();
        try {
            int s = new Random().nextInt(10) + 1;
            System.out.println("正在打印第一份文件。。。。当前打印线程:" + Thread.currentThread().getName() + " 需要" + s + "秒");
            Thread.sleep(s * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        lock.lock();
        try {
            int s = new Random().nextInt(10) + 1;
            System.out.println("正在打印第二份文件。。。。当前打印线程:" + Thread.currentThread().getName() + " 需要" + s + "秒");
            Thread.sleep(s * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class MyJob implements Runnable {

    private MyPrintQueue myPrintQueue;

    public MyJob(MyPrintQueue myPrintQueue) {
        this.myPrintQueue = myPrintQueue;
    }

    @Override
    public void run() {
        System.out.println("准备打印,当前线程名 "+Thread.currentThread().getName());
        myPrintQueue.printStr();
        System.out.println("打印完成,当前线程名 "+Thread.currentThread().getName());
    }
}
public class FairLockTest {
    public static void main(String[] args) {
        MyPrintQueue myPrintQueue = new MyPrintQueue();
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new MyJob(myPrintQueue));
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
            try{
                Thread.sleep(100);
            }catch (Exception e){

            }
        }

        


        
    }
}
通过源码查看两者实现逻辑

如下所示,我们可以在构造方法中看到公平锁和非公平锁是如何根据参数决定的。

 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

而下面这段代码则是公平锁和非公平锁核心逻辑所在,可以看出公平锁拿锁的逻辑要求当前等待队列中没有前驱节点。后非公平只需cas判断当前没有人持有这把锁就行。

公平锁和非公平锁总结

相对之下公平锁由于是有序执行,所以相对非公平锁来说执行更慢,吞吐量更小一些。
而非公平锁可以在特定场景下实现插队,所以很有可能出现某些线程被频繁插队而导致"线程饥饿"的情况。

共享锁和非共享锁(独占锁) 简介

共享锁最常见的使用就是ReentrantReadWriteLock,他的读锁就是共享锁,当某一线程使用读锁时,其他线程也可以使用读锁,以为读不会修改数据,无论多少个线程读都可以。
而写写锁就是独占锁的典型,当某个线程执行写时,为了保证数据的准确性,其他线程无论使用读锁还是写锁,都得阻塞等待当前正在使用写锁的线程释放锁才能执行。

代码示例
package lock.readwrite;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class baseRWdemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            readLock.unlock();
        }

    }


    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁");
            Thread.sleep(1000);
        } catch (Exception e) {

        }finally {
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
            writeLock.unlock();
        }

    }


    public static void main(String[] args) {
        //读锁可以一起获取
        new Thread(() -> read(), "thread1").start();
        new Thread(() -> read(), "thread2").start();
        //等上面读完写锁才能用 从而保证线程安全问题
        new Thread(() -> write(), "thread3").start();
        //等上面写完 才能开始写 避免线程安全问题
        new Thread(() -> write(), "thread4").start();
    }
}

读写锁插队示例

我们都知道读锁是共享锁,而写锁是独占锁,所以当队列中前驱节点是读锁时且我们设置的读写锁是非公平锁的情况下,我们就可以进行插队 *** 作。
这时候就有读者会问了,那java是否支持写锁持有锁的情况下,读锁也能持有锁呢?很明显是不行,假如我们允许这样的 *** 作,那么写锁就会遇到大量写锁插队,造成线程饥渴的情况出现,读写锁插队代码如下所示:

package lock.readwrite;

import java.util.concurrent.locks.ReentrantReadWriteLock;


public class NoFairReadSDemo {
    //设置为false之后 非公平 等待队列前是读锁 就可以让读锁插队
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁");
            Thread.sleep(20);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            readLock.unlock();
        }

    }


    private static void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获取读锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁");
            Thread.sleep(40);
        } catch (Exception e) {

        } finally {
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
            writeLock.unlock();
        }

    }


    public static void main(String[] args) {
        //写锁在前 非公平锁 避免饥饿 读锁不让上前
        new Thread(() -> write(), "Thread1").start();
//        两个读锁并行
        new Thread(() -> read(), "Thread2").start();
        new Thread(() -> read(), "Thread3").start();
        //写锁在前 非公平锁 避免饥饿 读锁不让上前
        new Thread(() -> write(), "Thread4").start();
        new Thread(() -> read(), "Thread5").start();

//        尝试让读锁插队 读锁在前的情况
        new Thread(() -> {
            Thread[] threads = new Thread[1000];
            for (int i = 0; i < 1000; i++) {
                threads[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
            }

            for (int i = 0; i < 1000; i++) {
                threads[i].start();
            }
        }).start();

    }
}

源码解析读写锁插队原理

通过debug我们可以看到一个tryAcquireShared方法,这里面有个核心判断readerShouldBlock方法,他会判断获取当前读锁时是否阻塞,我们不妨步进看看

 protected final int tryAcquireShared(int unused) {
      
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

可以看到逻辑也很简单,如果现在等待队列中的节点是独占锁即读锁时,返回true,反之返回false。这就应证了我们上述的观点,等待队列首节点是读锁占有锁的情况下,后续的读锁也可以进行插队。

final boolean readerShouldBlock() {
    
            return apparentlyFirstQueuedIsExclusive();
        }
锁的升降级

锁的升降级常用于如下这样一段场景

例如:我们现在有一段功能需要在日志中写入一段内容,然后在进行日志读取统计 *** 作,这时候我们就需要先使用写锁,然后再使用读锁。

如果我们这种 *** 作在正常场景下,我们需要频繁的释放写锁然后再使用读锁,那么程序执行的性能就会大打折扣。
所以,java对此进行了优化,但我们使用写锁的时,可以让他降级变为读锁,这样就可高效完成某个先读后写的 *** 作。
这时候肯定有人问了,那是否可以先读后写呢?答案是不行的,我们都知道使用写锁的前提是释放读锁,因为是写锁的独占锁,他要求当前这把锁只能它拥有。
假如我们有两个线程ab,a持有读锁,b也持有一把读锁,这时候他们都需要当前这把写锁,于是双方都在等待对方释放读锁,于是就造成了线程死锁。所以juc包设计就使得读写锁只支持降级,不支持升级。
所以,读写锁的非公平锁更适合于那些读多写少的情况。

自旋锁和非自旋锁 简介

我们都知道java阻塞或者唤醒一个线程都需要切换cpu状态的,这样的 *** 作非常耗费时间,而很多线程切换后执行的逻辑仅仅是一小段代码,为了这一小段代码而耗费这么长的时间确实是一件得不偿失的事情。对此java设计者就设计了一种让线程不阻塞,原地"稍等"即自旋一下的 *** 作。

代码示例
public class MySpinLock {
    private AtomicReference sign = new AtomicReference<>();

    public void lock() {
        Thread curThread = Thread.currentThread();
        while (!sign.compareAndSet(null, curThread)) {
            System.out.println(curThread.getName() + "未得到锁,自旋中");
        }
    }

    public void unLock() {
        Thread curThread = Thread.currentThread();
        sign.compareAndSet(curThread, null);
        System.out.println(curThread.getName() + "释放锁");

    }

    public static void main(String[] args) {
        MySpinLock mySpinLock = new MySpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "尝试获取自旋锁");
                mySpinLock.lock();
                System.out.println(Thread.currentThread().getName() + "得到了自旋锁");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    mySpinLock.unLock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }

            }
        };

        Thread t1=new Thread(runnable,"t1");
        Thread t2=new Thread(runnable,"t2");
        t1.start();
        t2.start();
    }
}

可中断锁和非可中断锁
package lock.lock;

import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptiblyDemo implements Runnable {


    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 尝试取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + " 取锁成功");
                Thread.sleep(5000);

            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 执行业务逻辑时被中断");

            } finally {
                lock.unlock();
            }

        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "尝试取锁时被中断");
        }

    }
}

package lock.lock;

public class LockInterruptiblyTest {

    public static void main(String[] args) throws InterruptedException {
        LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
        Thread thread0 = new Thread(lockInterruptiblyDemo);
        thread0.start();
        Thread thread1 = new Thread(lockInterruptiblyDemo);
        thread1.start();
        Thread.sleep(2000);
        thread1.interrupt();
    }
}

锁优化 jvm对锁的优化 自旋锁和自适应

当自旋锁自选到一定次数,jvm会对其进行阻塞

锁消除

锁消除即删除不必要的加锁 *** 作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。

锁粗化

假设一系列的连续 *** 作都会对同一个对象反复加锁及解锁,甚至加锁 *** 作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步 *** 作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的 *** 作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个 *** 作序列的外部。

个人使用锁的注意事项
    缩小同步代码块尽量不要锁住方法,减少锁的粒度减少请求锁的次数锁中尽量不要包含锁选择合适的锁类型
源码地址

并发编程示例源码

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5722604.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存