美文网首页
java 锁小记

java 锁小记

作者: nhhnhh | 来源:发表于2020-04-11 11:57 被阅读0次

锁是为了在多线程情况下保证程序运行正确的一种手段。
锁可以从多个维度进行分类:
1.从是否占用资源,锁可以分为悲观锁与乐观锁。
2.从锁的获取是否公平,可以分为公平锁与非公平锁
3.从当前线程能否重复获取这把锁,可以为分可重入锁与不可重入锁
4.从多个线程能否获取同一把锁可以分为排它锁跟共享锁
java的锁,主要有synchronized与lock这2种。
其中synchronized是系统提供的关键字。而lock是底层由AQS实现的类锁。

synchronized

synchronized的使用往往非常简单。看如下示例:

public class Demo {
    private static Object object = new Object();

    public static synchronized String getDemo0() {
        return "Demo0";
    }

    public synchronized String getDemo1(){
        return "Demo1";
    }

    public String getDemo2(){
        synchronized (this){
            return "Demo2";
        }
    }
    public String getDemo3(){
        synchronized (Demo.class){
            return "Demo3";
        }
    }
    public String getDemo4(){
        synchronized (object){
            return "Demo4";
        }
    }
}

虽然synchronized的使用非常简单,但是如果用的不好也会导致性能不佳。
synchronized主要分为2种,一是作用于方法,二是作用于代码块。

同步方法

synchronized修饰在方法上,如getDemo0是静态方法,这时候锁的就是类锁,不管你new了多少对象,锁都是同一个。
getDemo1则是实例方法,只有用一个对象上的多个线程对这个方法进行调用才会互斥,new多个对象时不会进行互斥。

同步代码块

synchronized (this)时,这个就是锁的是当前的对象,new多个对象时互不影响,同一个对象的不同带有synchronized的方法也会互相影响,即对同一个对象而言,每次只能有一个线程访问synchronized代码块,没有synchronized修饰的方法或者代码块不影响。
synchronized (Demo.class)时,锁的就是当前这个类,new多个对象时也会互相影响
synchronized (object)时,就是以object这个对象为锁。
所以synchronized使用虽然简单,但是放置的地方不一样,锁的粒度也是不同的。

synchronized小结

synchronized虽然使用起来非常方便,但是在jdk1.6以前是非常重量级的锁,只要其中一个线程占用了这个锁,其他线程全部阻塞。即使你等待的时间非常非常短,也会阻塞然后切换到其他线程,而线程的切换也是非常耗费资源的。所以在jdk1.6对synchronized做了优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等
synchronized的锁等级从小到大可以分为偏向锁,轻量级锁,重量级锁。
synchronized依赖于对象头(MarkWord)的结构。如下图所示


对象头.png

根据对象头的锁标志位来判断到底是什么锁。
我自己总结了一下:
偏向锁:查看锁的线程id是否指向当前线程,如果是就执行代码
如果不是,就cas替换现场id。成功就执行代码,失败就进化为轻量级锁
偏向锁认为其实就一个线程持有这个锁。

轻量级锁:认为是在不同的时间段内有不同的线程来获取这把锁,不存在竞争
当a线程将该对象的markowrd记录在自己的锁记录中,将markword指向自己的锁记录,线程b也做这个操作,失败,然后自旋,自旋一定次数失败,就重量级锁,全部阻塞

重量级锁:全部阻塞。

lock

lock是一个锁的接口,大概有2种锁,一种是独占锁,一种是共享锁,
独占锁的实现类如ReentrantLock ,共享锁的实现入ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock 。
我们一般比较常用的还是ReentrantLock。
lock的使用就比较灵活了,但是必须要自己解锁,锁的解除必须放在finally里,防止异常情况下导致无法解锁。相对于synchronized而言,lock可以设置一个锁的超时时间,时间一到自动解锁。

Lock lock=new ReentrantLock();
        lock.lock();
        try{
            //doSomething
        }finally{
            lock.unlock();
        }

现在我们来看一下ReentrantLock的底层实现。
首先我们来看下ReentrantLock的构造器

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

可以看出来ReentrantLock支持公平锁跟非公平锁2种锁方式,默认的是非公平锁的方式。从下图可以看出来非公平锁的继承方式


图片.png
 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         *非公平锁进来就尝试获取锁,如果获取失败就走acquire逻辑
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
         /**
         *这段代码是AQS中的代码。tryAcquire提供了一个模板方法,
         *由具体的实现来实现这个这个方法,意思就是试着来获取一下锁。以下这段代码的意思就是如果获取锁失败,并且入队成功,就给当前线程一个中断标记
         */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

可以看出非公平锁跟公平锁都继承的Sync这个类继承了AbstractQueuedSynchronizer这个类。AbstractQueuedSynchronizer一般称为AQS,是java多线程并发包的核心。它提供了一套模板方法,以及实现了一个CLH的FIFO的双项队列来满足java底层的多线程实现。
ReentrantLock的非公平锁实现tryAcquire的逻辑如下

final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取state的值,这个state是AQS中很重要的一个参数,
           //如果大于0则认为该锁已被持有,可以实现可重入锁逻辑,
           //持有一次则加一,解锁则减一,如果等于0则认为没有线程持有这把锁了
            int c = getState();
            //如果等于0则认为没有线程获取这把锁,则cas一下,
            //如果cas成功就设置排他线程为当前线程
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
           //如果当前线程与排他线程一致,则认为可以重复获取这把锁,就将state往上加
            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;
        }

如果tryAcquire失败,则认为无法获取当前锁执行以下逻辑
首先先addWaiter

private Node addWaiter(Node mode) { //mode=Node.EXCLUSIVE
        //将当前线程封装成Node
        Node node = new Node(Thread.currentThread(), mode); 
      
        Node pred = tail;
        //tail不为空的情况,说明队列中存在节点数据
        if (pred != null) { 
           //将当前线程的Node的prev节点指向tail
            node.prev = pred; 
            if (compareAndSetTail(pred, node)) {//通过cas讲node添加到AQS队列
                pred.next = node;//cas成功,把旧的tail的next指针指向新的tail
                return node;
            }
        }
       //如果tail=null,将node添加到同步队列中,end方法就是自旋然后塞入队列的队尾
        enq(node); 
        return node;
    }
      /**
     * 将当前线程组装成node节点,塞入队列
     */
   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
           //进行自旋
            for (;;) {
                final Node p = node.predecessor();
                //获取的前驱结点是头节点并且cas成功,就可以认为获取了锁,不再入队
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

相关文章

网友评论

      本文标题:java 锁小记

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