内部锁、显示锁和读写锁

线程同步机制

线程同步机制是一套用于协调线程间的数据访问及活动的机制。该机制用于保障线程安全及实现这些线程的共同目标。
java平台提供的线程同步机制:

  • volatile关键字
  • final关键字
  • static关键字
  • 其他(如:Object.wait()/Object.notify()等)

锁机制

锁机制 :将多线程并发访问共享数据转换为串行访问,一个共享数据每次只能被一个线程访问(获得锁),该线程访问结束后(释放锁)其他线程才能对其访问。
锁的获得 : 一个线程在访问数据前必须申请相应的锁。
锁的持有线程 : 一个线程获得某个锁。

一个锁一次只能被一个线程持有

临界区 :锁的持有线程在获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区。共享数据只能在临界区内进行访问,临界区一次只能被一个线程执行。
排它锁/互斥锁 :锁具有排他性,一次只能被一个线程持有,这种锁被称为排它或锁互斥锁。
java虚拟机对锁实现方式的分类:

  • 内部锁:通过关键字synchronized实现。
  • 显示锁:通过java.concurrent.locks.Lock的接口实现类实现。

锁的作用

保护共享数据实现线程安全:

  • 保障原子性:通过锁的互斥保障原子性,临界区代码一次只能被一个线程执行,没有其他线程访问,使得临界区代码执行操作具有不可分割的特性。
  • 可见性保障:

锁的几个概念

可重入性 : 一个线程在持有一个锁的时候还能够继续成功申请该锁,就称该锁具有可重入性,反之则称为非可重入性。
锁的粒度 : 一个锁实例可以保护一个或者多个共享数据,一个实例所保护的共享数据的数量大小就被称为该锁的粒度,锁保护的共享数据越大,我们就称该锁的粒度约粗,反之则称粒度细。

锁的开销

锁的开销包括几个:

  • 锁的申请和释放所产生的开销(主要是时间开销)
  • 锁的上下文切换(主要是时间开销)
  • 锁的不正确使用会导致一些线程活性故障
    • 锁泄露 :锁泄露指一个线程获得该锁之后,由于程序的错误、缺陷导致该锁一直无法被释放而其他线程一直无法获得该锁的现象。因此,锁泄露会导致同步在该锁上的所有线程都无法进展。
    • 死锁锁死等线程其他活性故障。

内部锁:Synchronized关键字

内部锁是通过Synchronized关键字实现的,Synchronized关键字可以用开修饰方法及代码块。

  • Synchronized关键字修饰的方法被称为同步方法
  • Synchronized关键字修饰的静态方法被称为同步静态方法
  • Synchronized关键字修饰的实例方法被称为同步实例方法
  • Synchronized关键字修饰代码块被称为同步块

同步方法的整个方法体就是一个临界区

同步块:Synchronized关键字修饰代码
Synchronized(锁句柄){
    //在此代码块访问共享数据
}

锁句柄是一个对象的引用。锁句柄可以直接填写this关键字表示当前对象。锁句柄对应的监视器被称为相应同步块的引导锁,相应的我们称呼相应的同步块为该锁引导的同步块。
锁句柄通常采用final修饰(private final)。这是因为
锁句柄的值一旦改变,会导致同一个代码块的多个线程实际上使用不同的锁,而导致竞态。
同步静态方法相当于当前类为引导锁的同步块。

public class SynchronizedMethodExample {
    public static sysnchronized void staticMethod(){
        //在此访问共享数据
    }
    //...
}

//相当于
public class SynchronizedMethodExample {
    public static void staticMethod(){
        sysnchronized(SynchronizedMethodExample.class){
             //在此访问共享数据
        }
    }
    //...
}

线程在执行临界区代码的时候必须持有该临界区的引导锁。一个线程执行到
同步代码块石必须申请该同步块的引导锁,只有申请成功该锁的线程才能够执行相的应临界区。一旦执行完临界区代码,引导该临界区的锁就会被自动释放。整个内部锁申请和释放的过程都是由java虚拟机负责实施的,所以synchronized实现的锁被称为内部锁。
内部锁不会导致锁泄露,java编译器在将同步代码块编译成字节码的时候,对临界区可能抛出的异常(未被捕获)进行了处理,所以即使临界区代码抛出异常也不会妨碍内部锁的释放。

内部锁的调度

Java虚拟机会给每个内部锁分配一个入口集(Entry Set),用于记录等待获取锁的线程。申请锁失败的线程会被存入入口集中等待再次申请锁的机会(这些线程处于BLOCKED状态,被称为等待线程)。
当锁被释放时,入口集中的一个线程被唤醒,得到再次申请锁的机会,仅仅是机会!因为内部锁的机制仅支持 非公平调度,所以可能被其他新的活跃线程抢占这个释放锁。

显示锁

jdk1.5引入的排他锁,其作用于内部锁相同, 但是它提供了一些内部锁所不具备的特性。

显示锁是java.util.concurrent.locks.Lock接口的实例。

摘要方法

void lock() 获取锁
void lockInterruptibly() 如果当前线程未被中断,则获取锁
newCondition() 返回绑定到此Lock实例的新Conditon实例
tryLock() 仅在调试时锁为空闲状态才获取锁
tryLock(long time, TimeUnit uinit) 如果说在给定的时间空闲,并且当前线程未被中断,则获取锁
unlock() 释放锁

显示锁的使用

//一个Lock接口实例就是一个显示锁的对象
private final Lock lock = ...;  //创建Lock接口实例

lock.lock();  //申请锁lock
try{
    //在此对共享数据访问
}finally{
    //总是在finally块中释放锁,避免锁泄露
    lock.unlock();  //释放锁
}
  • 创建Lock接口实例。如果没有特别要求,可以创建Lock接口的默认实现类ReentrantLock的实例作为显示是使用。
  • 在访问共享数据前申请相应的显示锁。Lock.lock()
  • 在临界区访问数据。Lock.lock()调用之后与Lock.unlock()调用之前的代码块区域。
  • 共享数据访问之后释放锁。
//使用显示锁实现循环递增的序列生成器
public class LockbasedCircularSeqGenerator implements CircularSeqGenerator {
  private short sequence = -1;
  private final Lock lock = new ReentrantLock();

  @Override
  public short nextSequence() {
    lock.lock();
    try {
      if (sequence >= 999) {
        sequence = 0;
      } else {
        sequence++;
      }
      return sequence;
    } finally {
      lock.unlock();
    }
  }
}

显示锁的调度

显示锁默认使用非公平策略调度。因为公平锁的开销比非公平锁的开销要大。

公平锁为保证公平增加了线程暂停和唤醒的可能性,导致了上下文切换的消耗要更大。所以公平锁适合用于锁被持有时间相对较长或线程申请锁时间相对较长的情况。总体来说公平锁的消耗比非公平锁消耗要大。

显示锁和内部锁应用区别

  1. 内部锁简单易用,且不会导致锁泄露;显示锁容易被错用而导致锁泄露(缺少释放锁的动作)。
  2. 内部锁是基于代码块的锁,灵活性较差,要么使用,用么不使用;而显示锁是基于对象的锁,灵活性强,比如可以在一个方法内申请锁,在另一个方法释放锁,而内部锁是无法做到的。
  3. 调度方面,内部是只支持非公平调度;显示锁两者都支持。

    显示锁的其他特性。

如果内部锁的持有线程一直不释放该锁(通常代码错误导致),同步在该锁的所以线程都会被暂停而使任务无法进展。显示锁可以避免此问题,使用显示锁的tryLock() 方法,锁处于空闲状态返回true,否则返回false

Lock lock = ...;
if(lock.tryLock()){   //也可给tryLock()指定时间
    try{
        //在此访问共享数据
    }finally{
        lock.unlock();
    }
}else{
    /执行其他任务
}

读写锁

是一种改进型的排它锁,也被称为共享/排他锁。读写锁允许多个线程同时读取共享变量,当一次只能允许一个线程对共享变量进行更新。
任何线程读取共享变量的时候其他线程无法对该共享变量进行更新,一个线程更新共享变量的时候其他线程都无法访问该变量。

读写锁是java.util.concurrent.locks.ReadWriteLock 接口的实例,默认实现类是java.util.concurrent.locks.ReentrantReadWriteLock。

ReadWriteLock接口定义了两个方法:readLock() 和 writeLock(),两个方法返回值都是lock类型

读写锁的两种角色

  • 读锁:读线程访问共享变量时必须持有相应的读锁。且读锁可以被多个线程持有。
  • 写锁:写锁是排他锁,一个线程持有写锁,其他成线程无法获得相应的写锁或读锁。

    多个读线程提高了并发性,而写锁保障了写线程能够独占的方式安全的更新共享变量。

读写锁的使用
public class ReadWriteLockUsage {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.write();

    //读线程执行
    public void reader(){
        readLock.lock();  //申请读锁
        try{
            //读取共享变量
        }finally{
            //释放锁避免泄露
            readLock.unlock();
        }
    }

    //写线程执行
    public void writer(){
        writeLock.lock();  //申请写锁
        try{
            //访问共享变量
        }finally{
            //释放锁避免泄露
            writeLock.unlock();
        }
    }
}
读写锁应用场景
  • 读操作比写操作频繁得多
  • 读线程持有锁的时间比较长

ReetrantReadWriteLock 说实现的读写锁是可重入锁;且支持锁的降级,即一个线程持有写锁的情况下可以获得相应的写锁。

锁的适用场景

  • check-then-act操作:一个线程读取共享数据,并在此基础上决定下一步操作是什么。
  • read-modify-write操作:一个线程读取共享数据并在此基础上更新该数据。
  • 多个线程对多个共享数据更新:如果这些共享数据之间存在关联关系,那么为力保证操作的原子性,可以考虑使用锁。

原文地址:https://www.cnblogs.com/sanzashu/p/11145724.html

时间: 2024-10-21 04:54:33

内部锁、显示锁和读写锁的相关文章

深入浅出 Java Concurrency (14): 锁机制 part 9 读写锁 (ReentrantReadWriteLock) (2)[转]

这一节主要是谈谈读写锁的实现. 上一节中提到,ReadWriteLock看起来有两个锁:readLock/writeLock.如果真的是两个锁的话,它们之间又是如何相互影响的呢? 事实上在ReentrantReadWriteLock里锁的实现是靠java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的.这个类看起来比较眼熟,实际上它是AQS的一个子类,这中类似的结构在CountDownLatch.ReentrantLock.Semapho

深入浅出 Java Concurrency (14): 锁机制 part 9 读写锁 (ReentrantReadWriteLock) (2)

这一节主要是谈谈读写锁的实现. 上一节中提到,ReadWriteLock看起来有两个锁:readLock/writeLock.如果真的是两个锁的话,它们之间又是如何相互影响的呢? 事实上在ReentrantReadWriteLock里锁的实现是靠java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的.这个类看起来比较眼熟,实际上它是AQS的一个子类,这中类似的结构在CountDownLatch.ReentrantLock.Semapho

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)[转]

从这一节开始介绍锁里面的最后一个工具:读写锁(ReadWriteLock). ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念.前面的章节中一直在强调这个特点.显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生.但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了.所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能

java多线程(七)提高锁的效率——使用读写锁

转载请注明出处:http://blog.csdn.net/xingjiarong/article/details/47947515 之前我们讲过ReentrantLock,这种锁不区分读操作和写操作,如果有一个线程在执行读操作,那么其他的所有的线程不能进行任何的读操作或者写操作.这样可以保证程序的互斥性,但是降低了程序的并发性,使执行效率降低,没有有效的发挥多线程的优势.比如说,有一个系统,主要是以读操作为主,比如有10个线程负责读数据,只有一个线程负责写数据.如果用我们之前的Reentrant

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)

从这一节开始介绍锁里面的最后一个工具:读写锁(ReadWriteLock). ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念.前面的章节中一直在强调这个特点.显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何"读/读","写/读","写/写"操作都不能同时发生.但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了.所

通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底层实现浅尝辄止,但是在需要时能够知道去查什么. 首先要打消一种想法,就是一个锁只能属于一种分类.其实并不是这样,比如一个锁可以同时是悲观锁.可重入锁.公平锁.可中断锁等等,就像一个人可以是男人.医生.健身爱好者.游戏玩家,这并不矛盾.OK,国际惯例,上干货. 〇.synchronized与Lock

写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底层实现浅尝辄止,但是在需要时能够知道去查什么. 首先要打消一种想法,就是一个锁只能属于一种分类.其实并不是这样,比如一个锁可以同时是悲观锁.可重入锁.公平锁.可中断锁等等,就像一个人可以是男人.医生.健身爱好者.游戏玩家,这并不矛盾.OK,国际惯例,上干货. 〇.synchronized与Lock

C# lock 语法糖实现原理--《.NET Core 底层入门》之自旋锁,互斥锁,混合锁,读写锁

原文:C# lock 语法糖实现原理--<.NET Core 底层入门>之自旋锁,互斥锁,混合锁,读写锁 在多线程环境中,多个线程可能会同时访问同一个资源,为了避免访问发生冲突,可以根据访问的复杂程度采取不同的措施 原子操作适用于简单的单个操作,无锁算法适用于相对简单的一连串操作,而线程锁适用于复杂的一连串操作 原子操作 修改状态要么成功且状态改变,要么失败且状态不变,并且外部只能观察到修改前或者修改后的状态,修改中途的状态不能被观察到 .NET 中,System.Threading.Inte

读写锁 与 互斥锁

相交进程之间的关系主要有两种,同步与互斥.所谓互斥,是指散步在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它 们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行.所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的 某种先后次序来运行,这种先后次序依赖于要完成的特定的任务. 显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步. 也就是说互斥是两个线程之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个