美文网首页Java高级进阶
2019-08-16-Java中实现的锁的区别和简单实现

2019-08-16-Java中实现的锁的区别和简单实现

作者: 王元 | 来源:发表于2019-07-30 09:17 被阅读0次

    Java中实现的锁的区别和实现的原理

    在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制。Java提供了多种多线程锁机制的实现方式,常见的有synchronized、ReentrantLock、Semaphore、AtomicInteger等。每种机制都有优缺点与各自的适用场景,必须熟练掌握他们的特点才能在Java多线程应用开发时得心应手。

    一,synchronized

    1,Java锁修饰的不同目标的区别,锁修饰对象有几种:

    • 修饰一个类,其作用的范围是synchronized后面括号括起来的部分, 作用的对象是这个类的所有对象
    • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法, 作用的对象是调用这个方法的对象
    • 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
    • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码, 作用的对象是调用这个代码块的对象

    2,使用synchronized修饰的代码具有原子性和可见性,在需要进程同步的程序中使用的频率非常高,可以满足一般的进程同步要求

    3,synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。

    • Java1.5中,synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
    • Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。
    • Java1.7与1.8中,均对该关键字的实现机理做了优化。

    4,当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。

    5,尽管Java实现的锁机制有很多种,并且有些锁机制性能也比synchronized高,但还是强烈推荐在多线程应用程序中使用该关键字,因为实现方便,后续工作由JVM来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。

    二,ReentrantLock implements Lock

    1,可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。

    • ReentantLock继承接口Lock并实现了接口中定义的方法,
    • 除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
    • Lock实现的机理依赖于特殊的CPU指令,可以认为不受JVM的约束,并可以通过其他语言平台来完成底层的实现。
    • 在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准,因此我们建议在高并发量情况下使用ReentrantLock。
    • ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,
    • 与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作

    代码实现如下

    private final ReentrantLock lock = new ReentrantLock();
    private void lock() {
        try {
            //设置为不响应中断锁
            //lock.lock();
            //设置为响应中断锁
            lock.lockInterruptibly();
            // do something
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    

    2,线程在等待资源过程中需要中断

    • ReentrantLock的在获取锁的过程中有2种锁机制,忽略中断锁和响应中断锁。
    • lock.lock()可设置锁机制为忽略中断锁,lock.lockInterruptibly()可设置锁机制为响应中断锁
    • 此处响应中断锁是指正在获取锁的过程中,如果线程此时并非处于获取锁的状态,通过此方法设置是无法中断线程的

    3,实现可轮询的锁请求

    • 在synchronized中,一旦发生死锁,唯一能够恢复的办法只能重新启动程序

    • tryLock()轮询方法来获得锁,如果锁可用则获取锁, 如果锁不可用,则此方法返回false,并不会为了等待锁而阻塞线程,这极大地降低了死锁情况的发生

        private void tryLock() {
            if(lock.tryLock()) {
                //锁可用,则成功获取锁
                try {
                    //获取锁后进行处理
                    //doSomething();
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            } else {
                //锁不可用,其他处理方法
            }
        }
      

    4,定时锁请求,基于3中提到的的lock.tryLock()方法

    • 在synchronized中,一旦发起锁请求,该请求就不能停止了,如果不能获得锁,则当前线程会阻塞并等待获得锁

    • Lock就提供了定时锁的机制,使用Lock.tryLock(long timeout, TimeUnit unit)来指定让线程在timeout单位时间内去争取锁资源,如果超过这个时间仍然不能获得锁,则放弃锁请求,定时锁可以避免线程陷入死锁的境地。

      public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
      return sync.tryAcquireNanos(1, unit.toNanos(timeout));
      }

    三,Semaphore 信号量

    • 上述两种锁机制类型都是“互斥锁”,学过操作系统的都知道,互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源,因此同时最多只能给一个线程提供服务。
    • 但是,在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。
    • Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源
    • Semaphone.acquire()方法默认为可响应中断锁,于ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
    • Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定
    • Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成
    • Semaphore支持多个临界资源,而ReentrantLock只支持一个临界资源。Semaphore的使用方法与ReentrantLock实在太过相似,在此不再举例说明。

    四,ReentrantReadWriteLock implements ReadWriteLock

    除了Lock接口外,Java的API还提供了另一种读写分离锁,那就是ReadWriteLock。ReadWriteLock是JDK1.5后才引入的,作为读写分离锁,可以有效的帮助减少锁的竞争,提升系统性能。

    用锁分离的机制来提升性能比较好理解

    • 读-读不互斥:读读之间不阻塞
    • 读-写互斥:读阻塞写,写也会阻塞读
    • 写-写互斥:写写阻塞

    其实就是一句话,可以同时读,但是读写,写读,写写是互斥的

    public interface ReadWriteLock {
        //读锁
        Lock readLock();
        //写锁
        Lock writeLock();
    }
    

    ReadWriteLock是一个接口,其使用的方式和Lock类似

    内部都是使用AbstractQueuedSynchronizer即AQS算法来实现的,内部Sync,具体实现可以查看源码

    五,StampedLock implements Serializable

    StampedLock是java8中新增的类,它是一个更加高效的读写锁的实现,而且它不是基于AQS来实现的,它的内部自成一片逻辑,让我们一起来学习吧。

    StampedLock具有三种模式:

    • 写模式
    • 读模式
    • 乐观读模式:乐观读时假定没有其它线程修改数据,读取完成后再检查下版本号有没有变化,没有变化就读取成功了,这种模式更适用于读多写少的场景。

    代码实现分别如下:

    private final StampedLock lock = new StampedLock();
    private int x = 0;
    private int y = 0;
    
    /**
     * 写锁
     * @param moveX
     * @param moveY
     */
    void tryWriteLock(int moveX, int moveY) {
        // 获取写锁,返回一个版本号(戳)
        long stampe = lock.tryWriteLock();
        x += moveX;
        y += moveY;
        //释放写锁,需要传入上面获取的版本号
        lock.unlockWrite(stampe);
    }
    
    /**
     * 乐观读
     */
    Point tryOptimisticRead() {
        //乐观读锁
        long stampe = lock.tryOptimisticRead();
        int currentX = x;
        int currentY = y;
        //// 验证版本号是否有变化
        if(lock.validate(stampe)) {
            currentX = x;
            currentY = y;
        }
        lock.unlockRead(stampe);
        return new Point(currentX, currentY);
    }
    
    /**
     * 读锁
     */
    Point tryReadLock(int newX, int newY) {
        //// 获取悲观读锁
        long stampe = lock.tryReadLock();
    
        while (x == 0 && y == 0) {
            // 转为写锁
            long ws = lock.tryConvertToWriteLock(stampe);
            if(ws != 0) {
                // 转换成功
                stampe = ws;
                x = newX;
                y = newY;
                break;
            } else {
                // 转换失败
                lock.unlockRead(stampe);
                // 获取写锁
                stampe = lock.writeLock();
            }
        }
        //释放锁
        lock.unlock(stampe);
        return new Point(x, y);
    }
    

    六,AtomicInteger

    • 此处AtomicInteger是一系列相同类的代表之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不同。
    • 令人兴奋地,还可以通过AtomicReference<V>将一个对象的所有操作转化成原子操作。
    • 通常AtomicInteger的性能是ReentantLock的好几倍。

    七,volatile关键字说明

    volatile属于稍弱线程同步方式,但是AtomicInteger的实现又离不开volatile实现的机制,此处对volatile进行说明

    • 加锁机制(即同步机制)既可以确保可见性又可以确保原子性
    • volatile只保证变量的可见性,不保证原子性。就是说多线程访问和修改volatile修饰的变量,修改之后的值对于其他线程是可见的
    • 当且仅当满足以下所有条件时,才应该使用 volatile 变量:对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
      该变量没有包含在具有其他变量的不变式中。
    • 解决的问题:在当前的 Java 内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
    • 就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取
    • 锁和volatile的区别:volatile,变量是一种稍弱的同步机制在访问,volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞。

    相关文章

      网友评论

        本文标题:2019-08-16-Java中实现的锁的区别和简单实现

        本文链接:https://www.haomeiwen.com/subject/zluwrctx.html