并发编程的锁机制:synchronize和lock

并发编程中,锁是经常需要用到的。我们一起了解一下Java中的锁机制:Synchronized和Lock

1.锁的种类

锁的种类很多,包括自旋锁,自旋锁的其他种类、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁等等。我们了解一下如下几种锁:可重入锁、读写锁、可中断锁、公平锁

1.1可重入锁

如果所具备可重入性,则称为可重入锁。synchronized和ReentrantLock都是可重入锁,可重入性可以理解为表明锁的分配机制:基于线程的分配,而不是基于方法调用的分配。例如,当一个线程执行到method1的synchronize时,而在method1中会调用另外一个synchronized方法method2,此时该线程不必重新去申请锁,而是可以直接执行方法method2

1.2读写锁

读写锁将对一个资源的访问分成了2个锁,如文件,一个读锁和一个写锁。正因为有了读写锁,才使得多线程之间的读操作不会发生冲突。ReadWriteLock就是一个读写锁,他是一个接口,ReentrantReadWriteLock实现了这个接口,可以通过readLock()获取读锁,通过writeLock()获取写锁

1.3可中断锁

可中断锁,即可以中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别人的线程中中断它,这种就是可中断锁
Lock接口中的lockInterruptibly()方法就体现了Lock的可中断性。

1.4公平锁

公平锁即尽量以请求锁的顺序来获取锁。同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获取该锁,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。
synchronized是非公平,他无法保证等待的线程获取锁的顺序。对于ReentrantLock和ReentrantReadWriteLock,默认情况下是非公平锁,但是可以设置为公平锁。

2.synchronized和lock的用法

2.1synchronized

synchronize是Java的关键字,当他用来修饰一个方法或者一个代码块的时候,能过保证在同一时刻最多只有一个线程执行该段代码,简单总结如下四种用法。

2.1.1代码块

对某一代码块使用,synchronized后面跟括号,括号里面是变量,一次只有一个线程进入该代码块

public int syncMethod(int m){
        synchronized (m){
            //
        }
    }
2.1.2方法声明时

方法声明时使用,放在范围操作符之后,返回类型声明之前。即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。

public synchronized void syncMethod(int m){
        //
    }
2.1.3synchronized后面括号里是对象

synchronized后面括号里是一个对象,此时线程获得的是对象锁。

public void syncMethod(){
        synchronized(this){
            //
        }
    }
2.1.4synchronized后面括号里面是类

synchronized后面括号里是类,如果线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法,对于含有静态方法和静态变量的代码块同步,通常使用这种方式。

2.2Lock

Lock接口主要相关的类和接口如下。
Lock接口
ReadWriteLock是读写锁接口,其实现类为ReentrantReadWriteLock。ReentrantLock实现了Lock接口。

2.2.1Lock

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:用来获取锁,如果所被其他线程获取,处于等待状态。如果采用Lock,必须主动去释放锁,并且在发生异常的时候,不会自动释放锁。因此一般来说,使用Lock必须早try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被释放,防止死锁发生。

  • lockInterruptibly:通过这个这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。

  • tryLock:tryLock方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已经别其他线程获取),则返回false,也就是说这个方法无论如何都会立即返回。在获取不到锁的时候,不会再那一直等待。

  • tryLock(long time, TimeUnit unit):与tryLock类似,只不过是有等待时间,在等待时间内获取到锁返回true,超时返回false。

  • unlock:释放锁,一定要在finally块中释放

2.2.2ReentrantLock

实现了Lock接口,可重入锁,内部定义了公平锁与非公平锁。默认为非公平锁:

public ReentrantLock() {  
  sync = new NonfairSync();  
}

可以手动设置为公平锁:

public ReentrantLock(boolean fair) {  
  sync = fair ? new FairSync() : new NonfairSync();  
}
2.2.3ReadWriteLock
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。ReentrantReadWriteLock实现了ReadWriteLock接口,并未实现Lock接口。不过要注意的是:
如果有一个线程已经占用了读锁,则此时其他线程如果申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁

2.2.4ReentrantReadWriteLock

ReentrantReadWriteLock同样支持公平性选择,支持重进入,锁降级

public class RWLock {

    static Map<String ,Object> map = new HashMap<String ,Object>();
    static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    static Lock r = rwLock.readLock();
    static Lock w = rwLock.writeLock();

    //读
    public static final Object get(String key){
        r.lock();
        try {
            return map.get(key);
        } finally {
           r.unlock();
        }
    }

    //写
    public static final Object put(String key ,Object value){
        w.lock();
        try {
            return map.put(key,value);
        } finally {
            w.unlock();
        }
    }

}

只需在读操作是获取读锁,写操作时获取写锁。当写锁被获取时,后续的读写操作都会被阻塞,写锁释放后,所有操作继续执行。

3.两种锁的比较

3.1synchronized和lock的区别
  • lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很有可能造成死锁现象,因此使用lock时需要在finally块中释放锁;

  • lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  • 通过lock可以知道有没有成功获取锁,而synchronized却无法办到;

  • lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  • 性能上来说,在资源竞争不激烈的情形下,Lock性能稍微比synchronized差点(编译程序通常尽可能的进行优化synchronized)。但是当同步非常激烈的时候,synchronized的性能就会下降几十倍。而ReentrantLock确还能维持常态。

3.2性能比较

下面对synchronized与lock进行性能测试,分别开启100个线程,每个线程计数到1000000,统计两种锁同步锁花费的时间。网上也能找到这样的例子。

public class TestAtomicIntegerLock {

    private static int synValue;

    public static void main(String[] args) {
        int threadNum = 10;
        int maxValue = 1000000;
        testSync(threadNum, maxValue);
        testLocck(threadNum, maxValue);
    }
    //test Lock
    public static void testLock(int threadNum, int maxValue) {
        Thread[] t = new Thread[threadNum];
        Long begin = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            Lock locks = new ReentrantLock();
            synValue = 0;
            t[i] = new Thread(() -> {

                for (int j = 0; j < maxValue; j++) {
                    locks.lock();
                    try {
                        synValue++;
                    } finally {
                        locks.unlock();
                    }
                }

            });
        }
        for (int i = 0; i < threadNum; i++) {
            t[i].start();
        }
        //main线程等待前面开启的所有线程结束
        for (int i = 0; i < threadNum; i++) {
            try {
                t[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("使用lock所花费的时间为:" + (System.currentTimeMillis() - begin) + "ms");
    }
    // test synchronized
    public static void testSync(int threadNum, int maxValue) {
        int[] lock = new int[0];
        Long start = System.currentTimeMillis();
        Thread[] t = new Thread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            synValue = 0;
            t[i] = new Thread(() -> {
                for (int j = 0; j < maxValue; j++) {
                    synchronized(lock) {
                        ++synValue;
                    }
                }
            });
        }
        for (int i = 0; i < threadNum; i++) {
            t[i].start();
        }
        //main线程等待前面开启的所有线程结束
        for (int i = 0; i < threadNum; i++) {
            try {
                t[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("使用synchronized所花费的时间为:" + (System.currentTimeMillis() - start) + "ms");
    }
}
#开启10个线程,每个线程计数到1000000的对比结果
使用lock所花费的时间为去去去去:526ms
使用synchronized所花费的时间为:424ms
#开启100个线程,每个线程计数到1000000的对比结果
使用lock所花费的时间为:3522ms
使用synchronized所花费的时间为:3432ms
#开启1000个线程,每个线程计数到1000000的对比结果
使用lock所花费的时间为:28198ms
使用synchronized所花费的时间为:38041ms

可以通过测试结果发现,如果竞争不激烈的时候,synchronized的性能还是比较好的。如果竞争激烈的情况下,就需要采用lock了。synchronized的效率降低很明显。差异还是很明显的,本次测试基于jdk1.8。

在jdk1.6之后,对synchronized加入了很多优化措施,有自适应自旋,锁清除,锁粗化,轻量级锁,偏向锁等等。导致jdk1.6上的synchronized的性能并不比lock差。官方也表示,他们也更支持synchronized,在未来的版本中还有优化余地,所以还提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

4.总结

本文主要对并发编程中的锁机制synchronized和lock,进行详解。synchronized是基于jvm实现的内置锁,Java中的每一个对象都是可以作为锁的。对于同步方法,锁是是当前实例对象。对于静态同步方法,锁是当前对象的class对象。对于同步方法块,锁是synchronized括号里配置的对象。Lock是基于语言层面实现的锁,Lock可以被中断,支持定时锁。Lock可以提高对个线程进行读操作的效率。通知对比得知,Lock的效率明显高于synchronized关键字,一般对于数据结构设计或者框架的设计都倾向于使用Lock而非synchronized。
原文参考

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