Java并发编程:Lock

上一节中我们介绍了jvm中的一个关键字:synchronized,这个关键字主要用于避免多线程同时操作共享资源而引起的并发问题。线程A在访问synchronized修饰的方法或者代码块之前,必须先拿到这个锁才能进行访问,这个时候线程B如果需要访问这个方法或者代码块则需要线程A释放锁才行。

但是,这样会引起一个问题,就是如果线程A一直持有这个锁,那么后面所有等待的线程都只能在这等着,这就很影响效率了。

因此就需要一种机制可以不让等待的线程一直等待下去,通过Lock就可以办到。

再举个例子:当有多个线程同时进行读写文件时,读和写操作会发生冲突,写和写操作也会发生冲突,而读和读操作时没有问题的。
这个时候如果用synchronized来实现同步的话,就会导致同时只有一个线程能进行读操作,这个也就很影响效率了。

另外,通过Lock可以知道有没有成功获取到锁,这个是synchronized无法做到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意一下两点:

  • Lock不是Java内置的,是JDK1.5引入的一个类,而synchronized是JVM的一个关键字。
  • Lock使用过后一定要释放锁,如果不释放锁有可能引起死锁,而synchronized用完之后由系统自动去释放这个锁。

Lock相关锁的使用

1. Lock
通过源码可以看到Lock是一个接口,主要有以下几种方法。

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock():void,最常用到的方法,用来获取锁,如果被其他线程持有则等待。前面讲到,使用lock一定要释放锁,并且在发生异常时不会释放锁,所以同步代码块一定要放在try中,并且在finally中去unlock()。

Lock lock = ...;
lock.lock();
try{
  //同步代码块
}
catch(Exception e){
  //处理异常
}
finally{
  lock.unlock();
}

tryLock():boolean,与lock方法的区别就是这个方法是有返回值的,表示获取锁有没有成功,获取成功则返回true,获取失败返回false。这样如果获取锁失败,就不必一直等待了,可以去做别的事情。
tryLock(long time, TimeUnit unit):boolean,和tryLock类似,只不过区别在于,这个方法如果直接拿到锁就返回true,如果没有拿到,那么就等待一段时间,经过time时间之后,如果还没有拿到就返回false,拿到了就返回true。

Lock lock = ...;
if(lock.tryLock()){
  try{
    //同步代码块
  }
  catch(Exception e){
  }
  finally{
      lock.unlock();
  }
}
else{
  //do something
}

lockInterruptibly():void,表示获取锁,但是这个线程能够响应中断,获取锁的过程是可以被打断、终止的。也就是说,当两个线程同时通过lockInterruptibly去获取锁,如果线程A已经获取到了锁,而线程B处于等待状态时是可以通过threadB.interrupt()去中断等待的。

Lock lock = ...;
lock.lockInterruptibly();
try{
}
catch(){
}
finally{
lock.unlock();
}

注意:当一个线程已经获取到了锁,这个时候线程是不能被interrupt的,因为interrupt不能终止正在运行的线程,只能中断阻塞的线程。

2. ReentrantLock
中文翻译为“可重入锁”,关于这个概念下一节来讲。ReentrantLock是唯一实现了Lock接口的实现类。下面通过一个实例来看下ReentrantLock的使用方法:
例子1:使用lock方法

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

    private void insert(String threadName) {
        lock.lock();
        try {
            System.out.println(threadName + "获得了锁");
            for (int i = 0; i < 3; i++) {
                System.out.println(threadName + "--" + i);
            }
        } catch (Exception e) {

        } finally {
            System.out.println(threadName + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final LockDemo demo = new LockDemo();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                super.run();
                demo.insert("Thread1");
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                super.run();
                demo.insert("Thread2");

            }
        };
        thread1.start();
        thread2.start();
    }
}
输入结果:
Thread1获得了锁
Thread1--0
Thread1--1
Thread1--2
Thread1释放了锁
Thread2获得了锁
Thread2--0
Thread2--1
Thread2--2
Thread2释放了锁

例子2:使用tryLock()方法

private void insert(String threadName) {
        if (lock.tryLock()) {
            try {
                System.out.println(threadName + "获得了锁");
                for (int i = 0; i < 3; i++) {
                    System.out.println(threadName + "--" + i);
                }
            } catch (Exception e) {

            } finally {
                System.out.println(threadName + "释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(threadName + "尝试获取锁失败,可以嘿嘿嘿了");
        }
    }

输出结果:
Thread1获得了锁
Thread2尝试获取锁失败,可以嘿嘿嘿了
Thread1--0
Thread1--1
Thread1--2
Thread1释放了锁

例子3:使用lockInterruptibly

private void insert(String threadName) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            System.out.println(threadName + "获得了锁");
            Thread.sleep(1000);
            for (int i = 0; i < 3; i++) {
                System.out.println(threadName + "--" + i);
            }
        } catch (Exception e) {
        } finally {
            System.out.println(threadName + "释放了锁");
            lock.unlock();
        }

    }
public static void main(String[] args) {
        final LockDemo demo = new LockDemo();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    demo.insert("Thread1");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    demo.insert("Thread2");
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+"被中断");
                    e.printStackTrace();
                }

            }
        };
        thread1.start();
        thread2.start();
        try{
            Thread.sleep(100);
            thread1.interrupt();
            thread2.interrupt();
        }
        catch (Exception e){

        }
    }

输出结果:
Thread1获得了锁
Thread1释放了锁
Thread-1被中断
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.program.LockDemo.insert(LockDemo.java:12)
    at com.program.LockDemo.access$000(LockDemo.java:8)
    at com.program.LockDemo$2.run(LockDemo.java:45)

3. ReadWriteLock
ReadWriteLock和Lock一样,也是一个接口。只有两个方法,一个获取读锁,一个获取写锁。也就是说将读写操作分开来上锁,多个线程可以同时读,但是不能同时写和同时读&写。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

4. ReentrantReadWriteLock
ReentrantReadWriteLock实现了ReadWriteLock接口,最主要就两个方法,readLock&writeLock用来获取读写锁。

下面用一个例子来看下ReentrantReadWriteLock的使用:

public class LockDemo {
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    private void get(String threadName) {
        lock.readLock().lock();
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName + "正在读取");
            }
            System.out.println(threadName + "读取结束======");
        } catch (Exception e) {

        } finally {
            lock.readLock().unlock();
        }
    }

    private void put(String threadName) {
        lock.writeLock().lock();
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName + "正在写入");
            }
            System.out.println(threadName + "写入结束=====");
        } catch (Exception e) {

        } finally {
            lock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        final LockDemo demo = new LockDemo();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                super.run();
                demo.get("Thread1");
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                super.run();
                demo.put("Thread2");
            }
        };
        Thread thread3 = new Thread() {
            @Override
            public void run() {
                super.run();
                demo.put("Thread3");

            }
        };
        Thread thread4 = new Thread() {
            @Override
            public void run() {
                super.run();
                demo.get("Thread4");

            }
        };
        thread1.start();
        thread2.start();
        thread4.start();
        thread3.start();
    }

上面的代码中,有两个操作,分别是get和put,用来模拟读写操作。然后开启两个线程来读,两个线程来写。
根据上面的理论,thread1和thread4应该是并发的,而thread2和thread3应该是串行的。
但是,But

Thread1正在读取
Thread1正在读取
Thread1正在读取
Thread1正在读取
Thread1正在读取
Thread1读取结束======
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2写入结束=====
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3写入结束=====
Thread4正在读取
Thread4正在读取
Thread4正在读取
Thread4正在读取
Thread4正在读取
Thread4读取结束======

我试了很多次,thread1和thread4都没有并发过。这不是坑吗?
我调整一下线程启动顺序,将两个读线程放一块,也就是读线程之间没有插入写线程。

thread1.start();
thread4.start();
thread2.start();
thread3.start();
输出结果:
Thread4正在读取
Thread1正在读取
Thread4正在读取
Thread1正在读取
Thread1正在读取
Thread4正在读取
Thread4正在读取
Thread1正在读取
Thread4正在读取
Thread1正在读取
Thread1读取结束======
Thread4读取结束======
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2写入结束=====
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3写入结束=====

可以看到thread1和thread4并发读取了。
具体原因,可以参考一下ReentrantReadWriteLock有坑,小心读锁!

synchronized还是Lock?

总结来说,synchronized和Lock有以下几点不同:

  1. synchronized是jvm里的一个关键字,而Lock是JDK1.5之后提供的一个上层接口。
  2. synchronized同步结束之后系统会自动释放锁,而Lock需要手动释放锁。
  3. 使用Lock锁的线程在等待过程中可以被中断,提高效率。
  4. 使用Lock锁,线程可以拿到是否成功获取锁的回调,不必等待。
  5. 使用Lock锁,可以提高多个线程同时读取的效率。

在性能上说,如果竞争资源不激烈,可以直接使用synchronized,更加方便。而如果竞争资源很激烈(也就是有大量线程竞争),这个时候使用Lock更加高效。

锁的相关概念

1.可重入锁
如果锁具备可重入性,那么这个锁就是可重入锁。synchronized和Lock都是可重入锁。其实可重入性表明了锁的分配机制:基于线程分配,而不是基于方法调用的分配。举个🌰

pubic synchronized void methodA(){
  methodB();
}
pubic synchronized void methodB(){
}

我们想一下,某个时刻,线程A执行到了methodA方法中,线程A已经拿到了这个对象锁。如果,锁不具有可重入性,那么访问方法B需要重新申请锁,但是这个锁就在自己身上啊,自己又没有释放,这样就会死锁啦。

由于synchronized和Lock都具备可重入性,所以不会出现上面这种问题。

2.可中断锁
顾名思义,这个锁是可以被中断。这里不是说锁可以被中断,其实指的是在等待获取这个锁的线程是可以被中断的。

如果某一个线程A正在执行同步代码,而线程B只能等待着获取锁,如果突然灵机一动,不想等了,可以中断这个等待的过程。

synchronized是不能被中断的,Lock是可中断锁。

3.公平锁
公平锁,顾名思义,就是看这个锁是否公平了。什么意思呢?就是比如多个线程都在争取这个锁,那这个锁到底给谁呢?

公平锁,就是尽量按照请求锁的顺序来执行,即谁先请求的,就给谁,一碗水端平。

非公平锁,就是无法保证这个锁到底给谁,这样就可能导致某个线程或者某些线程一直获取不到锁,这也太不公平了,但是这个世界就是这样,人间不值得。

synchronized就是非公平锁,无法保证获取锁的顺序。Lock默认也是非公平锁,但是通过ReentrantLock和ReentrantReadWriteLock的构造参数来设置成公平锁。

4.读写锁
读写锁将对共享资源的访问分为读锁和写锁,这样既能保证多个线程可以同时读取,也可以保证不能同时写入和同时读&写。ReadWriteLock就是读写锁,上面已经介绍过啦。

参考文章:https://www.cnblogs.com/dolphin0520/p/3923167.html

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

推荐阅读更多精彩内容