Java同步锁概述


1.synchronized

synchronized是一种简单的可重入锁,JDK6开始对synchronized做了大量优化,性能和ReentrantLock差距不大

2.ReentrantLock

ReentrantLock是可重入锁,它可以完全替代synchronized,每次使用都要显示的加锁与解锁,而且提供了新的特性: 公平锁,有条件,可定时,可轮询,可中断,可以有效避免死锁的活跃性问题.

公平锁: 线程按照发出请求的顺序来获取锁,创建ReentrantLock对象时传入true即可生成公平锁

非公平锁: 不必按照发出请求的顺序获取锁,可以插队,创建ReentrantLock时默认为非公平锁

条件:对于每个重入锁,都可以通过newCondition()方法绑定若干个条件对象,不同的线程可以等待不同的条件,从而实现更加细粒度的线程间通信

可轮询与可定时:在获取锁时,先使用tryLock()尝试获取所有的锁,如果不能同时获得,就退回,并重新尝试,休眠时间由一个特定的组件完成

可中断:lockInterruptibly方法能够在获取锁的同时保持对中断的响应

3.ReadWriteLock(读写锁)

ReentrantReadWriteLock实现了ReadWriteLock接口,构造器提供了公平锁与非公平锁两种创建方式,对于读多写少的场景,如果全部使用排它锁显然是没有必要的,因为不同的读操可以并发执行提高效率,如果使用排它锁则读操作会阻塞其他读操作降低效率,此时使用读写锁是更好的选择

读锁(共享锁):允许多个读线程并发执行

写锁(排他锁):不允许写线程与其他线程(读或者写线程)并发执行

读锁的升级:读锁升级为写锁,ReentrantReadWriteLock不支持锁升级

写锁的降级:持有写锁的同时去获取读锁,然后释放写锁

  • 写锁降级的简单介绍

首先要了解写锁的特点,线程间是互斥的,但是如果是当前线程则可重入,因此仅仅对当前线程而言,可以在获取到写锁以后再获取读锁,然后再释放写锁

  • 通过简化实例简单了解锁降级
//主要代码
public class Degrade {
    volatile int value = 0;
    public void getAndSetValue(){
        Thread.sleep(100);    //便于测试
        value++;                            //写操作
        Thread.sleep(100);    //便于测试
        System.out.println(value);          //读操作
    }
    public void getValue(){
        Thread.sleep(100);    //便于测试
        System.out.println("get:"+value);      //读操作
    }       
}
//测试代码
      @Test
    public void test(){
        Degrade deGrade = new Degrade();

        for(int i=0;i<2;i++){       //创建2个读写线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(;;){
                        deGrade.getAndSetValue();
                    }
                }
            }).start();
        }

        for (int i=0;i<5;i++){      //创建5个读线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (;;){
                        deGrade.getValue();
                    }
                }
            }).start();
        }

现在我们要完成每一次value值更新,都会至少被一个读操作读取到,显然不加锁的情况下,是不行的

  1. 测试方式1,改写如下

    //主要代码
    public class Degrade {
        volatile int value = 0;
        final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
        Lock read = rwlock.readLock();
        Lock write = rwlock.writeLock();
        public void getAndSetValue(){
            Thread.sleep(100);    //便于测试
            write.lock();
            value++;                            //写操作
            write.unlock();                    //写锁释放
            Thread.sleep(100);    //便于测试
            read.lock();
            System.out.println(value);          //读操作
            read.unlock();
        }
        public void getValue(){
            Thread.sleep(100);    //便于测试
            read.lock();
            System.out.println("get:"+value);      //读操作
            read.unlock();
        }       
    }
    
    输出结果片段:
    2
    get:2
    get:4
    
    //显然此时不能满足条件,因为当写锁释放时,此时可能由其他读写线程先执行,这样value刚更改过还没有被读取输出过就又没修改了,因此输出结果不续性
  1. 测试方式2,改写如下
```
//主要代码
public class Degrade {
    volatile int value = 0;
    final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
    Lock read = rwlock.readLock();
    Lock write = rwlock.writeLock();
    public void getAndSetValue(){
        Thread.sleep(100);    //便于测试
        write.lock();
        value++;                            //写操作
        Thread.sleep(100);    //便于测试
        System.out.println(value);          //读操作
        write.unlock();
    }
    public void getValue(){
        Thread.sleep(100);    //便于测试
        read.lock();
        System.out.println("get:"+value);      //读操作
        read.unlock();
    }       
}

输出结果为: 
1
2
get:2
get:2
get:2
get:2
get:2
3
get:3
get:3
get:3
4
get:4
get:4
//可以看到满足要求,输出结果连续,但是这种方式,将一个读操作也放入写锁中,每次写操作完成后必定执行此读操作,从而实现每次更新value值后,都至少有一个读操作读取到value值输出
//现在如果要写操作执行完成后,其他读操作都有机会先执行,该怎么实现?
```
  1. 测试方式3(写锁降级),修改如下
~~~
//主要代码
public class Degrade {
    volatile int value = 0;
    final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
    Lock read = rwlock.readLock();
    Lock write = rwlock.writeLock();
    public void getAndSetValue(){
        Thread.sleep(100);    //便于测试
        write.lock();    //获取写锁
        value++;                            //写操作
        read.lock();    //已经有写锁的情况下可直接获取到读锁(无需竞争)
        write.unlock();    //释放写锁,此时锁降级为读锁(其他读线程可以并行)
        Thread.sleep(100);    //便于测试
        System.out.println(value);          //读操作
        read.unlock();    //释放读锁        
    }
    public void getValue(){
        Thread.sleep(100);    //便于测试
        read.lock();
        System.out.println("get:"+value);      //读操作
        read.unlock();
    }       
}

输出结果:
1
get:2
get:2
get:2
get:2
get:2
get:2
2
get:2
get:2
get:2
get:2
3
get:3
get:3
get:4
get:4
get:4
4
get:4
get:4

//可以看到写操作执行完成之后,其他的读操作都有机会先执行,因为写操作完成后写锁降级为读锁,此时所有读线程都有机会抢到cpu执行权,例如先输出get:4,后输出4
~~~

显然通过上面的对比可以发现,写锁降级为读锁后,其他读锁都可以并发执行了,从侧面提升了读的效率,同时也使读线程可以更加敏感的感知到数据的更新


文章作者: Bryson
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Bryson !
评论
 上一篇
下一篇 
使用AQS实现一个简单的可重入锁 使用AQS实现一个简单的可重入锁
1.AbstractQueuedSynchronizer(AQS)简单介绍AQS定义了一套多线程访问共享资源的同步器框架,其核心功能都是围绕着其32位整型属性state进行,一般可以说它表示锁的数量,对同步状态的控制可以实现不同的同步工具,
2018-05-24
  目录