美文网首页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