Java多线程进阶——Java并发包之锁的使用和实现原理

目录

Lock接口

Lock接口具备synchronized不具备的特性

队列同步器

独占锁和共享锁

以独占锁的实现为例

同步队列原理

独占锁获取与释放原理

共享锁的获取与释放

超时获取独占锁

 可重入锁

ReentrantLock可重入锁

公平锁和非公平锁

为什么非公平锁性能好?

读写锁

写锁的获取与释放

读锁的获取和释放

锁降级

LockSupport工具

Condition接口


Lock接口

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

Lock接口具备synchronized不具备的特性

lock提供了synchronized不具备的特性:超时获取锁,中断获取锁。

超时获取锁:线程尝试获取锁,若获取锁的时间用光了,则放弃获取锁。

中断获取锁:线程在获取锁的过程中,或者在同步队列等待获取锁时,其它线程调用了该线程的interrupt方法,线程会响应中断,放弃获取锁。

队列同步器

队列同步器AbstractQueuedSynchronizer,是用来构建锁或其它同步组件的基础框架,使用了一个int类型的state变量来表示同步状态,通过内置了FIFO队列来为任务分配线程。

独占锁和共享锁

在AbstractQueuedSynchronizer类中,如下方法是会抛出UnsupportedOperationException异常。

//独占锁的获取
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

//独占锁的释放
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

//共享锁的获取
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

//共享锁的释放
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

需要使用独占锁,则子类实现tryAcquire和tryRelease方法;

需要使用共享锁,则子类实现tryAcquireShared和tryReleaseShared方法。

以独占锁的实现为例

public class MyMutex implements Lock {

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0,1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if(getState()==0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);//当前锁没有线程占用
            setState(0);//锁未占用状态
            return true;
        }
        Condition newCondition(){
            return new ConditionObject();
        }
    }

    final Sync sync=new Sync();

    @Override
    public void lock() {
        sync.acquire(1);//sync父类中的acquire方法,它将会调用子类的tryAcquire方法,后面原理会解释
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//可中断的方式获取锁
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);//sync父类中的release方法,它将会调用子类的tryRelease方法,后面原理会解释
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

独占锁只有状态0和1,0表示锁未被占用,1表示锁已占用。

同步队列原理

在基础同步器类AbstractQueuedSynchronizer中,提供了一个同步队列,用来暂存获取锁失败的线程状态和线程引用。

当一个线程释放锁时,会唤醒首结点对应线程;

而当一个线程获取锁失败时,会将线程状态和线程引用信息新建一个Node,加入到同步队列的尾结点。

 也就是,先进先出FIFO的方式。

独占锁获取与释放原理

锁获取分析

 

锁释放分析

释放比较简单,因为锁是独占的,持有锁的线程直接释放就好了,tryRelease修改state状态,然后唤醒首结点线程(如果有的话)。

共享锁的获取与释放

独占锁是最多只有一个线程获得该锁(如写锁),共享锁是可以有多个线程同时获取该锁(如读锁,Semaphore)。

锁获取分析

tryAcquireShared返回>=0时,说明获取共享锁成功。

若返回值<0,则进入doAcquireShared进行自旋获取锁。

 只有当前Node的前驱结点为首结点时,才会尝试获取锁,tryAcquireShared返回值r>=0时,才说明获取锁成功,同时会响应中断。

锁释放分析

由于共享锁可以被多个线程持有,因此释放共享锁需要循环CAS自旋的方式。

tryReleaseShared修改state状态,然后调用doReleaseShared方法(循环+CAS)。

超时获取独占锁

 

流程如下:

 可重入锁

看如下例子:

public class M {
    public void synchronized m1(){
        m2();
    }

    public void synchronized m2() {
        ...
    }
}

当线程进入m1()方法后(必然获取了M对象的锁),还需要继续进入m2()方法,m1方法和m2方法的对象锁是一样的,因此在m1里的线程可以直接进入m2,这就是可重入锁。synchronized隐式是支持可重入锁的。

可重入锁,每进入一个同步块都需要加一把锁,离开同步块都需要释放一把锁。上述例子实际上是一个线程(进入m2方法后)持有了两把M锁,离开m2方法时,释放一把M锁,再离开m1方法时,再释放一把M锁。

ReentrantLock可重入锁

可重入锁的条件:1.获得锁的线程再次获取锁,获取成功,state++; 2. 释放锁时,需要state--,直到持有锁的线程释放所有的锁时,即state=0时,才会唤醒其它阻塞的线程。

公平锁和非公平锁

在ReentrantLock中,静态内部类Sync继承AbstractQueuedSynchronizer,也内置了FairSync和UnfairSync(都继承自Sync)。

公平锁,简单理解就是,所有想获得锁的线程,都会老老实实排队等候,禁止插队存在,按照先来后到的顺序获取锁,这叫“文明锁”;

公平锁的获取,可重入性关键在于current==getExclusiveOwerThread(),若已经持有锁的线程再次尝试获取锁。若c==0,说明没有线程持有锁,这是要判断是不是队列首结点,只有队列首结点才能获得锁!

非公平锁,已经在排队获得锁的线程,不会插队,当服务员喊道下一个,这时,队列首结点线程和刚刚来准备排队的线程会一哄而上,竞争锁,这叫“野蛮锁”。

非公平锁的获取

第一次尝试CAS获取锁,获取成功则当前线程获得锁,否则acquire(1);

 尝试tryAcquire获得锁,若失败后,则生成结点,加入队列尾部,acquireQueued方法使得线程循环获取锁。

非公平锁获取的方法在ReentrantLock的静态内部类Sync中实现了。 

 非公平的关键在于if(compareAndSetState(0,acquires))这一行,不需要判断是不是队列首结点,这是因为,只要是醒着的线程都可以竞争锁。

公平锁和非公平锁的释放,都在Sync中实现了。

通过观察源码中的state变量,可以发现,可重入锁并不能保证state的值就是1,第一次获得锁,state=1,第二次获得锁,state=2,.......

释放锁也是一样,只有当state减到0时,才会返回free=true,设置当前独占锁线程为null。

公平锁的优缺点:公平锁不会出现饿死线程的情况,但性能通常比较低;

非公平锁的优缺点:非公平锁的性能比较高,有可能会饿死线程。

为什么非公平锁性能好?

CPU频繁切换线程,会造成性能损好,切换线程的过程,需要保存“离开线程”的现场计算信息,也需要调入“进场线程”的信息。

非公平锁模式下,持有锁的线程释放锁(1号竞争者),唤醒首结点线程(2号竞争者),还有其它刚到的线程(3,4,5...),同时竞争这把锁,而队列的第2~第n号结点不参与竞争,如果刚刚释放锁的线程重新获得了锁,这时候就可以减少线程的切换,提高吞吐量。同时,在队列后面的线程,由于没有竞争权利,很可能会出现长时间不能竞争锁,而饿死。

而公平锁模式下,持有锁的线程释放锁后,会唤醒首结点线程,首结点线程理所当然地获取到了锁,刚释放锁的线程乖乖去队列尾部再次排队。这样自然很公平,但是cpu整体性能不高,花费了大量cpu在线程切换上。

公平锁的耗时大约是非公平锁的100倍,线程切换次数大约也是100倍左右。

读写锁

读锁直接使用共享锁,写锁使用排他锁。前文提到的ReentrantLock锁就是排它锁。

读写锁有两把锁,一把是读锁,一把是写锁。分离读写锁,可以有效提升线程读取的性能。

读写锁的特性:支持公平锁和非公平锁(默认是非公平锁,因为吞吐量高),重入性(所有获得锁的线程可以再次获得锁),锁降级(写锁能降级为读锁)。

Java的读写锁是ReentrantReadWriteLock,读写锁用一个32位的变量维持状态:

高16位表示读锁状态,第16位表示写锁状态,数字表示读锁和写锁获取的次数(包括可重入)。

写锁状态=S&0x0000FFFF,位运算取低16位,写锁状态增加1时,直接S+1,减一为S-1;

读锁状态=(S>>16),位运算取高16位,读锁状态增加1时,S+(1<<16),减一为S-(1<<16)。

由于读状态和写状态都是16位的,最大值为65535。

写锁的获取与释放

        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {//c!=0,说明必然存在读锁或写锁中的一个
                int w = exclusiveCount(c);//取低16位,即写锁
                //若w=0,说明当前没有写锁,有读锁, 当前写锁获取失败
                //若当前线程不为已经获得写锁的线程,当前写锁获取失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //当前线程重入锁的次数达到上限,65535
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            //执行到这里,可能是state=0(读写锁为空),
            //也可能是当前线程获得了写锁,正在进行可重入
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

写锁的独占的,线程持有写锁时,当前必然没有读锁和其他写锁,因此释放写锁比较简单。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

读锁的获取和释放

        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                //若存在写锁(排它锁),
                //或当前线程没有获取写锁(因为写锁可以可重入读锁),则获取读锁失败(这里是写锁降级为读锁的关键)
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                //获取读锁状态
                int r = sharedCount(c);
                //达到读锁上限
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //尝试将读锁状态+1
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    //如果原来的读锁状态为0
                    if (r == 0) {
                        //第一个线程读锁为当前线程
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        //第一个线程读锁可重入数量+1
                        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 true;
                }
            }
        }
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                //若第一位读者,只重入了一次,则firstReader设置为null;否则减一
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else 
                    firstReaderHoldCount--;
            } else {
                //读锁数量-1
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            //尝试循环CAS来将读锁状态-1
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

只有当读锁减少为0时,才返回true,否则返回false(读锁仍然被占用)。

锁降级

先看简易版本:

public void test(){
    writeLock.lock();
    readeLock.lock;
    writeLock.unlock();
    //现在只能读,不能写了,写锁降级为读锁
    //...
    readLock.unlock();
}
public class LockUngrade {
    private int i = 0;

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock writerLock = readWriteLock.writeLock();
    private Lock readLock = readWriteLock.readLock();

    public void doSomething() {
        writerLock.lock();
        try {
            i++;
            readLock.lock();
        } finally {
            writerLock.unlock();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            System.out.println("i = " + i);
        } finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) {
        LockUngrade lock = new LockUngrade();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lock.doSomething();
            }).start();
        }
    }
}

LockSupport工具

 阻塞和唤醒线程的工具。

park()会一直阻塞线程,直到unpark(当前线程)调用,或者当前线程被中断,才会返回;

parkNanos()超时阻塞,被唤醒会立刻返回,或时间到了会自动返回。

parkUntil(),被unpark唤醒,或直到某个时间点,自动返回。

unpark(),唤醒线程。

Condition接口

 Object和Condition都是监视器(即锁),Condition比Object有更多的支持,如在等待下不响应中断,等待到某个时间点,多个等待队列等。

有一个子类ConditionObject,能用就用Condtion吧。

参考书籍:

Java并发编程的艺术

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学无止境jl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值