Java中的锁——Lock和synchronized

一、Lock接口

1、Lock接口和synchronized内置锁

a)synchronized:Java提供的内置锁机制,Java中的每个对象都可以用作一个实现同步的锁(内置锁或者监视器Monitor),线程在进入同步代码块之前需要或者这把锁,在退出同步代码块会释放锁。而synchronized这种内置锁实际上是互斥的,即没把锁最多只能由一个线程持有。

b)Lock接口:Lock接口提供了与synchronized相似的同步功能,和synchronized(隐式的获取和释放锁,主要体现在线程进入同步代码块之前需要获取锁退出同步代码块需要释放锁)不同的是,Lock在使用的时候是显示的获取和释放锁。虽然Lock接口缺少了synchronized隐式获取释放锁的便捷性,但是对于锁的操作具有更强的可操作性、可控制性以及提供可中断操作和超时获取锁等机制。

2、lock接口使用的一般形式

Lock lock = new ReentrantLock(); //这里可以是自己实现Lock接口的实现类,也可以是jdk提供的同步组件
lock.lock();//一般不将锁的获取放在try语句块中,因为如果发生异常,在抛出异常的同时,也会导致锁的无故释放
try {
}finally {
    lock.unlock(); //放在finally代码块中,保证锁一定会被释放
}

3、Lock接口的方法

图片.png
public interface Lock {

    /**
     * 获取锁,调用该方法的线程会获取锁,当获取到锁之后会从该方法但会
     */
    void lock();

    /**
     * 可响应中断。即在获取锁的过程中可以中断当前线程
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 尝试非阻塞的获取锁,调用该方法之后会立即返回,如果获取到锁就返回true否则返回false
     */
    boolean tryLock();

    /**
     * 超时的获取锁,下面的三种情况会返回
     * ①当前线程在超时时间内获取到了锁
     * ②当前线程在超时时间内被中断
     * ③超时时间结束,返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁
     */
    void unlock();

    /**
     * 获取等待通知组件,该组件和当前锁绑定,当前线程只有获取到了锁才能调用组件的wait方法,调用该方法之后会释放锁
     */
    Condition newCondition();
}

4、相比于synchronized,Lock接口所具备的其他特性

①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁
②能被中断的获取锁lockInterruptibly():获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁
③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false

二、重入锁

1、重入锁的概念

当某个线程请求一个被其他线程所持有的锁的时候,该线程会被阻塞(后面的读写锁先不考虑在内),但是像synchronized这样的内置锁是可重入的,即一个线程试图获取一个已经被该线程所持有的锁,这个请求会成功。重入以为这锁的操作粒度是线程级别而不是调用级别。我们下面说到的ReentrantLock也是可重入的,而除了支持锁的重入之外,该同步组件也支持公平的和非公平的选择。

说明:
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。
可重入锁的意义之一在于防止死锁。
实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1
如果同一个线程再次请求这个锁,计数器将递增;
每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁(很好理解吧)。

public synchronized methodA1(){

methodA2();

}

public synchronized methodA2(){

                    //具体操作
}

也是A类中的同步方法,当当前线程调用A类的对象methodA1同步方法,如果其他线程没有获取A类的对象锁,那么当前线程就获得当前A类对象的锁,然后执行methodA1同步方法,方法体中调用methodA2同步方法,当前线程能够再次获取A类对象的锁,而其他线程是不可以的,这就是可重入锁。

2、ReentrantLock

a)ReentrantLock实现的可重入性

对于锁的可重入性,需要解决的两个问题就是:

①线程再次获取锁的识别问题(锁需要识别当前要获取锁的线程是否为当前占有锁的线程);
②锁的释放(同一个线程多次获取同一把锁,那么锁的记录也会不同。一般来说,当同一个线程重复n次获取锁之后,只有在之后的释放n次锁之后,其他的线程才能去竞争这把锁)
③ReentrantLock的可重入测试

package part1;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestCR {
    Lock lock = new ReentrantLock();
    void m1(){
        try{
            lock.lock();
            for (int i = 0; i < 4; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("m1() method "+i + " "+Thread.currentThread().getName());
            }
            m2();
        }catch(InterruptedException e){
            
        }finally{
            lock.unlock();
        }
    }
    void m2(){
        System.out.println("m2 start :"+Thread.currentThread().getName());
        lock.lock();
        System.out.println("m2 method"+ " "+Thread.currentThread().getName());
        lock.unlock();
    }
    public static void main(String[] args) {
        final TestCR test = new TestCR();
        
        new Thread(new Runnable(){
            
            public void run(){
                System.out.println(Thread.currentThread().getName());
                test.m1();
            }
        }).start();
        
        new Thread(new Runnable(){
            
            public void run(){
                System.out.println(Thread.currentThread().getName());
                test.m2();
            }
        }).start();
        
    }
}
图片.png

说明: Thread-0,Thread-1同时启动,Thread-0首先获取到同步锁,在m1方法进行休眠等待。Thread-1调用m2方法进行获取锁的操作。未获取成功,等待Thread-0在调用完m1,m2后才能获取,Thread-0的m1调用m2时为获取重用锁,虽然m2有获取锁的动作,但在同一线程内,获取成功,属于重入锁。

三、Synchronized

1、Synchronized作用对象

①对于普通方法,锁的是当前实例对象
②对于静态同步方法,锁的是类的Class对象
③对于同步代码块,锁的是Synchronized括号中的对象
如下所示的三种情况

package cn.source.sync;

public class TestSync01 {
    private static int count = 0;
    private Object object = new Object();

    public void testSyn1() {
        //同步代码块(这里面是锁临界资源,即括号中的对象)
        synchronized (object) {
            System.out.println(Thread.currentThread().getName()
                +" count =" + count++);
        }
    }

    public void testSyn2() {
        //锁当前对象(相当于普通同步方法)
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()
                    +" count =" + count++);
        }
    }

    //普通同步方法:锁当前对象
    public synchronized void testSyn3() {
        System.out.println(Thread.currentThread().getName()
                +" count =" + count++);
    }

    //静态同步方法,锁的是当前类型的类对象(即TestSync01.class)
    public static synchronized void testSyn4() {
        System.out.println(Thread.currentThread().getName()
                +" count =" + count++);
    }

    //下面的这种方式也是锁当前类型的类对象
    public static void testSyn5() {
        synchronized (TestSync01.class) {
            System.out.println(Thread.currentThread().getName()
                    +" count =" + count ++);
        }
    }
}

2、synchronized的实现原理

没写

Lock 中的公平锁

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

b)下面分析ReentrantLock的部分源码来学习这个同步组件(默认的非公平锁实现)

①首先可以知道ReentrantLock实现Lock接口public class ReentrantLock implements Lock

abstract static class Sync extends AbstractQueuedSynchronizer {
    /**
     * 创建非公平锁的方法
     */
    abstract void lock();

    /**
     * 执行非公平的tryLock。 tryAcquire实现于
     * 子类,但两者都需要tryf方法的非公平尝试。
     */
    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) // 对同步状态的值进行非法判断
                throw new Error("Maximum lock count exceeded");
            setState(nextc); //更新state的值
            return true;
        }
        return false;
    }

    /**
     * 释放同步状态的处理逻辑
     */
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases; //对同一线程而言,就是减去相应的获取次数
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false; //返回值
        if (c == 0) { //只有该线程将获取的次数全部释放之后,才会返回true,并且将当前同步状态的持有者设置为null
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c); //更新state
        return free;
    }

        /**
         * 判断当前同步状态的持有者线程
         */
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

        /**
         * 返回当前持有者线程
         */
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

        /**
         * 返回持有同步状态的线程获取次数
         */
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

        /**
         * 判断当前是否有线程获取到同步状态(根据state值进行判断)
         */
    final boolean isLocked() {
        return getState() != 0;
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

1.获取逻辑:首先通过nonfairTryAcquire方法增加了对于同一线程再次获取同步状态的逻辑处理(通过判断当前线程是否为已经同步状态的持有者,来决定是否能够再次获取同步状态,如果当前线程是已经获取到同步状态的那个线程,那么就能够获取成功,并且同时以CAS的方式修改state的值)

2.释放逻辑:对于成功获取到同步状态的线程,在释放锁的时候,通过tryRelease方法的实现可以看出,如果该锁被线程获取到了n次,那么前(n-1)次释放的操作都会返回false,只有将同步状态完全释放才会返回true。最终获取到同步状态的线程在完全释放掉之后,state值为0并且持有锁的线程为null。

c)关于ReentrantLock的公平和非公平实现

①非公平锁

公平和非公平是针对于获取锁而言的,对于公平锁而言获取锁应该遵循FIFO原则,上面我们通过源码分析了非公平锁的实现(对于非公平锁而言,tryAcquire方法直接使用的是ReentrantLock静态内部类Sync的nofairTryAcquire方法)

//非公平锁实现
static final class NonfairSync extends Sync {

    /**
     * 以CAS方式原子的更新state的值
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    /**
     * 非公平锁的实现是直接调用Sync的nonfairTryAcquire方法
     */
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

②公平锁实现
公平锁的实现和非公平实现的主要区别就是tryAcquire方法的实现

static final class FairSync extends Sync {

    final void lock() {
        acquire(1); //调用AQS的模板方法实现锁的获取
    }

    /**
     * 公平锁的处理逻辑
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread(); //获取当前线程
        int c = getState(); //获取当前同步状态的值
        if (c == 0) { //当前同步状态没有被任何线程获取的时候
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { //这个点的主要处理逻辑就是:hasQueuedPredecessors判断当前线程所在的结点是否含有前驱结点,                                  如果返回值为true表示有前驱结点,那么当前线程需要等待前驱结点中的线程获取并释放锁之后才能获取锁,保证了FIFO
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { //支持重入的逻辑,和非公平锁的实现原理相同
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
//hasQueuedPredecessors的处理逻辑
public final boolean hasQueuedPredecessors() {
    // 简单而言,就是判断当前线程是否有前驱结点
    // 当前结点含有前驱结点时候返回true;当前结点为头结点挥着队列为空的时候返回false
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

d)公平锁和非公平锁的测试

①测试目的

验证上面通过源码分析的,非公平锁在获取锁的时候会首先进行抢锁,在获取锁失败后才会将当前线程加入同步队列队尾中,而公平锁则是符合请求的绝对顺序,也就是会按照先来后到FIFO。在下面的代码中我们使用一个静态内部类继承了ReentrantLock并重写等待队列的方法,作为测试的ReentrantLock。然后创建5个线程,每个线程连续两次去获取锁,分别测试公平锁和非公平锁的测试结果

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.junit.Test;

public class TestReentrantLock {
    /**
     * ReentrantLock的构造方法
     * public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
     */
    private Lock fairLock = new ReentrantLock2(true);
    private Lock unFairLock = new ReentrantLock2(false);

    @Test
    public void testFair() throws InterruptedException {
        testLock(fairLock); //测试公平锁
    }

    @Test
    public void testUnFair() throws InterruptedException {
        testLock(unFairLock); //测试非公平锁
    }

    private void testLock(Lock lock) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(lock)) {
                public String toString() {
                        return getName();
                }
            };
            thread.setName(i+"");
            thread.start();
        }
        Thread.sleep(12000);
    }

    private static class Job extends Thread {
        private Lock lock;
        public Job(Lock lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            //两次打印当前线程和等待队列中的Threads
            for (int i = 0; i < 2; i++) {
                lock.lock(); //获取锁
                try {
                    Thread.sleep(1000);
                    System.out.println("当前线程=>" + Thread.currentThread().getName() + " " +
                            "等待队列中的线程=>" + ((ReentrantLock2)lock).getQueuedThreads());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock(); //释放锁
                }
            }
        }

    }

    private static class ReentrantLock2 extends ReentrantLock {
        public ReentrantLock2(boolean fair) {
            super(fair);
        }
        public Collection<Thread> getQueuedThreads() { //逆序打印等待队列中的线程
            List<Thread> list = new ArrayList<Thread>(super.getQueuedThreads());
            Collections.reverse(list);
            return list;
        }
    }
}
图片.png

由上面的测试结果简单的得到关于非公平锁的一个结论:通过nofairTryAcquire方法可以得到这样一个前提,当一个线程请求一个锁时,判断获取成功的条件就是这个线程获取到同步状态就可以,那么某个刚刚释放锁的线程再次获取到同步状态的几率就会更大一些(当然实验中也出现并非连续两次获取这把锁的情况,比如下面的测试结果)

图片.png

③测试公平锁

通过分析下面的测试结果,对于使用公平锁而言,即便是同一个线程连续两次获取锁释放锁,在第一次释放锁之后还是会被放在队尾并从队列头部拿出线程进行执行。并没有出现像非公平锁那样连续两次获取锁的那种情况

图片.png

④由上面的测试可以看出:非公平锁可能导致在队尾的线程饥饿,但是又因为同一个线程在释放锁的时候有更大的概率再次获取到这把锁,那么这样的话线程的切换次数就会更少(这带来的就是更大的吞吐量和开销的减小)。而虽然公平锁的获取严格按照FIFO的规则,但是线程切换的次数就会更多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341