Java 锁的知识总结及实例代码

java中有哪些锁

这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够。于是再次翻看了一下书里的内容,突然有点打开脑门的感觉。看来确实是要学习的最好方式是要带着问题去学,并且解决问题。

在java中锁主要两类:内部锁synchronized和显示锁java.util.concurrent.locks.Lock。但细细想这貌似总结的也不太对。应该是由java内置的锁和concurrent实现的一系列锁。

为什么这说,因为在java中一切都是对象,而java对每个对象都内置了一个锁,也可以称为对象锁/内部锁。通过synchronized来完成相关的锁操作。

而因为synchronized的实现有些缺陷以及并发场景的复杂性,有人开发了一种显式的锁,而这些锁都是由java.util.concurrent.locks.Lock派生出来的。当然目前已经内置到了JDK1.5及之后的版本中。

synchronized

首先来看看用的比较多的synchronized,我的日常工作中大多用的也是它。synchronized是用于为某个代码块的提供锁机制,在java的对象中会隐式的拥有一个锁,这个锁被称为内置锁(intrinsic)或监视器锁(monitor locks)。线程在进入被synchronized保护的块之前自动获得这个锁,直到完成代码后(也可能是异常)自动释放锁。内置锁是互斥的,一个锁同时只能被一个线程持有,这也就会导致多线程下,锁被持有后后面的线程会阻塞。正因此实现了对代码的线程安全保证了原子性。

可重入

既然java内置锁是互斥的而且后面的线程会导致阻塞,那么如果持有锁的线程再次进入试图获得这个锁时会如何呢?比如下面的一种情况:

public class BaseClass {
  public synchronized void do() {
    System.out.println("is base");
  }

}

public class SonClass extends BaseClass {
  public synchronized void do() {
   System.out.println("is son");
   super.do();
  }

}

SonClass son = new SonClass();
son.do();

此时派生类的do方法除了会首先会持有一次锁,然后在调用super.do()的时候又会再一次进入锁并去持有,如果锁是互斥的话此时就应该死锁了。

但结果却不是这样的,这是因为内部锁是具有可重入的特性,也就是锁实现了一个重入机制,引用计数管理。当线程1持有了对象的锁a,此时会对锁a的引用计算加1。然后当线程1再次获得锁a时,线程1还是持有锁a的那么计算会加1。当然每次退出同步块时会减1,直到为0时释放锁。

synchronized的一些特点

修饰代码的方式

修饰方法

public class BaseClass {
  public synchronized void do() {
    System.out.println("is base");
  }

}

这种就是直接对某个方法进行加锁,进入这个方法块时需要获得锁。

修饰代码块

public class BaseClass {
  private static Object lock = new Object();
  public void do() {
    synchronized (lock) {
      System.out.println("is base");
    }
  }

}

这里就将锁的范围减少到了方法中的部分代码块,这对于锁的灵活性就提高了,毕竟锁的粒度控制也是锁的一个关键问题。

对象锁的类型

经常看到一些代码中对synchronized使用比较特别,看一下如下的代码:

public class BaseClass {
  private static Object lock = new Object();
  public void do() {
    synchronized (lock) {
    }
  }

  public synchronized void doVoid() {
  }

  public synchronized static void doStaticVoid() {
  }

  public static void doStaticVoid() {
    synchronized (BaseClass.class) {

    }
  }  

}

这里出现了四种情况:修饰代码块,修饰了方法,修饰了静态方法,修饰BaseClass的class对象。那这几种情况会有什么不同呢?

修饰代码块

这种情况下我们创建了一个对象lock,在代码中使用synchronized(lock)这种形式,它的意思是使用lock这个对象的内置锁。这种情况下就将锁的控制交给了一个对象。当然这种情况还有一种方式:

public void do() {
  synchronized (this) {
    System.out.println("is base");
  }
}

使用this的意思就是当前对象的锁。这里也道出了内置锁的关键,我提供一把锁来保护这块代码,无论哪个线程来都面对同一把锁咯。

修饰对象方法

这种直接修饰在方法是咱个情况?其实和修饰代码块类似,只不过此时默认使用的是this,也就是当前对象的锁。这样写起代码来倒也比较简单明确。前面说过了与修饰代码块的区别主要还是控制粒度的区别。

修饰静态方法

静态方法难道有啥不一样吗?确实是不一样的,此时获取的锁已经不是this了,而this对象指向的class,也就是类锁。因为Java中的类信息会加载到方法常量区,全局是唯一的。这其实就提供了一种全局的锁。

修饰类的Class对象

这种情况其实和修改静态方法时比较类似,只不过还是一个道理这种方式可以提供更灵活的控制粒度。

小结

通过这几种情况的分析与理解,其实可以看内置锁的主要核心理念就是为一块代码提供一个可以用于互斥的锁,起到类似于开关的功能。

java中对内置锁也提供了一些实现,主要的特点就是java都是对象,而每个对象都有锁,所以可以根据情况选择用什么样的锁。

java.util.concurrent.locks.Lock

前面看了synchronized,大部分的情况下差不多就够啦,但是现在系统在并发编程中复杂性是越来越高,所以总是有许多场景synchronized处理起来会比较费劲。或者像<java并发编程>中说的那样,concurrent中的lock是对内部锁的一种补充,提供了更多的一些高级特性。

java.util.concurrent.locks.Lock简单分析

这个接口抽象了锁的主要操作,也因此让从Lock派生的锁具备了这些基本的特性:无条件的、可轮循的、定时的、可中断的。而且加锁与解锁的操作都是显式进行。下面是它的代码:

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  void unlock();
  Condition newCondition();
}

ReentrantLock

ReentrantLock就是可重入锁,连名字都这么显式。ReentrantLock提供了和synchronized类似的语义,但是ReentrantLock必须显式的调用,比如:

public class BaseClass {
  private Lock lock = new ReentrantLock();

  public void do() {
    lock.lock();
    try {
    //....
    } finally {
     lock.unlock();
    }

  }

}

这种方式对于代码阅读来说还是比较清楚的,只不过有个问题,就是如果忘了加try finally或忘 了写lock.unlock()的话导致锁没释放,很有可能导致一些死锁的情况,synchronized就没有这个风险。

trylock

ReentrantLock是实现Lock接口,所以自然就拥有它的那些特性,其中就有trylock。trylock就是尝试获取锁,如果锁已经被其他线程占用那么立即返回false,如果没有那么应该占用它并返回true,表示拿到锁啦。

另一个trylock方法里带了参数,这个方法的作用是指定一个时间,表示在这个时间内一直尝试去获得锁,如果到时间还没有拿到就放弃。

因为trylock对锁并不是一直阻塞等待的,所以可以更多的规避死锁的发生。

lockInterruptibly

lockInterruptibly是在线程获取锁时优先响应中断,如果检测到中断抛出中断异常由上层代码去处理。这种情况下就为一种轮循的锁提供了退出机制。为了更好理解可中断的锁操作,写了一个demo来理解。

package com.test;

import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

public class TestLockInterruptibly {
 static ReentrantLock lock = new ReentrantLock();

 public static void main(String[] args) {
 Thread thread1 = new Thread(new Runnable() {

  @Override
  public void run() {
  try {
   doPrint("thread 1 get lock.");
   do123();
   doPrint("thread 1 end.");

  } catch (InterruptedException e) {
   doPrint("thread 1 is interrupted.");
  }
  }
 });

 Thread thread2 = new Thread(new Runnable() {

  @Override
  public void run() {
  try {
   doPrint("thread 2 get lock.");
   do123();
   doPrint("thread 2 end.");
  } catch (InterruptedException e) {
   doPrint("thread 2 is interrupted.");
  }
  }
 });

 thread1.setName("thread1");
 thread2.setName("thread2");
 thread1.start();
 try {
  Thread.sleep(100);//等待一会使得thread1会在thread2前面执行
 } catch (InterruptedException e) {
  e.printStackTrace();
 }
 thread2.start();
 }

 private static void do123() throws InterruptedException {
 lock.lockInterruptibly();
 doPrint(Thread.currentThread().getName() + " is locked.");
 try {
  doPrint(Thread.currentThread().getName() + " doSoming1....");
  Thread.sleep(5000);//等待几秒方便查看线程的先后顺序
  doPrint(Thread.currentThread().getName() + " doSoming2....");

  doPrint(Thread.currentThread().getName() + " is finished.");
 } finally {
  lock.unlock();
 }
 }

 private static void doPrint(String text) {
 System.out.println((new Date()).toLocaleString() + " : " + text);
 }
}

上面代码中有两个线程,thread1比thread2更早启动,为了能看到拿锁的过程将上锁的代码sleep了5秒钟,这样就可以感受到前后两个线程进入获取锁的过程。最终上面的代码运行结果如下:

2016-9-28 15:12:56 : thread 1 get lock.
2016-9-28 15:12:56 : thread1 is locked.
2016-9-28 15:12:56 : thread1 doSoming1....
2016-9-28 15:12:56 : thread 2 get lock.
2016-9-28 15:13:01 : thread1 doSoming2....
2016-9-28 15:13:01 : thread1 is finished.
2016-9-28 15:13:01 : thread1 is unloaded.
2016-9-28 15:13:01 : thread2 is locked.
2016-9-28 15:13:01 : thread2 doSoming1....
2016-9-28 15:13:01 : thread 1 end.
2016-9-28 15:13:06 : thread2 doSoming2....
2016-9-28 15:13:06 : thread2 is finished.
2016-9-28 15:13:06 : thread2 is unloaded.
2016-9-28 15:13:06 : thread 2 end.

可以看到,thread1先获得锁,一会thread2也来拿锁,但这个时候thread1已经占用了,所以thread2一直到thread1释放了锁后才拿到锁。

**这段代码说明lockInterruptibly后面来获取锁的线程需要等待前面的锁释放了才能获得锁。**但这里还没有体现出可中断的特点,为此增加一些代码:

thread2.start();
try {
 Thread.sleep(1000);
} catch (InterruptedException e) {
 e.printStackTrace();
}
//1秒后把线程2中断
thread2.interrupt();

在thread2启动后调用一下thread2的中断方法,好吧,先跑一下代码看看结果:

2016-9-28 15:16:46 : thread 1 get lock.
2016-9-28 15:16:46 : thread1 is locked.
2016-9-28 15:16:46 : thread1 doSoming1....
2016-9-28 15:16:46 : thread 2 get lock.
2016-9-28 15:16:47 : thread 2 is interrupted. <--直接就响应了线程中断
2016-9-28 15:16:51 : thread1 doSoming2....
2016-9-28 15:16:51 : thread1 is finished.
2016-9-28 15:16:51 : thread1 is unloaded.
2016-9-28 15:16:51 : thread 1 end.
和前面的代码相比可以发现,thread2正在等待thread1释放锁,但是这时thread2自己中断了,thread2后面的代码则不会再继续执行。

ReadWriteLock

顾名思义就是读写锁,这种读-写锁的应用场景可以这样理解,比如一波数据大部分时候都是提供读取的,而只有比较少量的写操作,那么如果用互斥锁的话就会导致线程间的锁竞争。如果对于读取的时候大家都可以读,一旦要写入的时候就再将某个资源锁住。这样的变化就很好的解决了这个问题,使的读操作可以提高读的性能,又不会影响写的操作。

一个资源可以被多个读者访问,或者被一个写者访问,两者不能同时进行。

这是读写锁的抽象接口,定义一个读锁和一个写锁。

public interface ReadWriteLock {
  /**
   * Returns the lock used for reading.
   *
   * @return the lock used for reading
   */
  Lock readLock();

  /**
   * Returns the lock used for writing.
   *
   * @return the lock used for writing
   */
  Lock writeLock();
}

在JDK里有个ReentrantReadWriteLock实现,就是可重入的读-写锁。ReentrantReadWriteLock可以构造为公平的或者非公平的两种类型。如果在构造时不显式指定则会默认的创建非公平锁。在非公平锁的模式下,线程访问的顺序是不确定的,就是可以闯入;可以由写者降级为读者,但是读者不能升级为写者。

如果是公平锁模式,那么选择权交给等待时间最长的线程,如果一个读线程获得锁,此时一个写线程请求写入锁,那么就不再接收读锁的获取,直到写入操作完成。

简单的代码分析 在ReentrantReadWriteLock里其实维护的是一个sync的锁,只是看起来语义上像是一个读锁和写锁。看一下它的构造函数:

public ReentrantReadWriteLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
  readerLock = new ReadLock(this);
  writerLock = new WriteLock(this);
}

//读锁的构造函数
protected ReadLock(ReentrantReadWriteLock lock) {
  sync = lock.sync;
}
//写锁的构造函数
protected WriteLock(ReentrantReadWriteLock lock) {
  sync = lock.sync;
}

可以看到实际上读/写锁在构造时都是引用的ReentrantReadWriteLock的sync锁对象。而这个Sync类是ReentrantReadWriteLock的一个内部类。总之读/写锁都是通过Sync来完成的。它是如何来协作这两者关系呢?

//读锁的加锁方法
public void lock() {
  sync.acquireShared(1);
}

//写锁的加锁方法
public void lock() {
  sync.acquire(1);
}

区别主要是读锁获得的是共享锁,而写锁获取的是独占锁。这里有个点可以提一下,就是ReentrantReadWriteLock为了保证可重入性,共享锁和独占锁都必须支持持有计数和重入数。而ReentrantLock是使用state来存储的,而state只能存一个整形值,为了兼容两个锁的问题,所以将其划分了高16位和低16位分别存共享锁的线程数量或独占锁的线程数量或者重入计数。

其他

写了一大篇感觉要写下去篇幅太长了,还有一些比较有用的锁:

CountDownLatch

就是设置一个同时持有的计数器,而调用者调用CountDownLatch的await方法时如果当前的计数器不为0就会阻塞,调用CountDownLatch的release方法可以减少计数,直到计数为0时调用了await的调用者会解除阻塞。

Semaphone

信号量是一种通过授权许可的形式,比如设置100个许可证,这样就可以同时有100个线程同时持有锁,如果超过这个量后就会返回失败。

感谢阅读此文,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • Java 高并发四:无锁详细介绍

    在[高并发Java 一] 前言中已经提到了无锁的概念,由于在jdk源码中有大量的无锁应用,所以在这里介绍下无锁. 1 无锁类的原理详解 1.1 CAS CAS算法的过程是这样:它包含3个参数CAS(V,E,N).V表示要更新的变量,E表示预期值,N表示新值.仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么 都不做.最后,CAS返回当前V的真实值.CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成 操作.当多个线程同时使用CAS操

  • Java 高并发九:锁的优化和注意事项详解

    摘要 本系列基于炼数成金课程,为了更好的学习,做了系列的记录. 本文主要介绍: 1. 锁优化的思路和方法 2. 虚拟机内的锁优化 3. 一个错误使用锁的案例 4. ThreadLocal及其源码分析 1. 锁优化的思路和方法 在[高并发Java 一] 前言中有提到并发的级别. 一旦用到锁,就说明这是阻塞式的,所以在并发度上一般来说都会比无锁的情况低一点. 这里提到的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差.但是再怎么优化,一般来说性能都会比无锁的情况差一点. 这里要注意的是,在[高并

  • Java编程实现排他锁代码详解

    一 .前言 某年某月某天,同事说需要一个文件排他锁功能,需求如下: (1)写操作是排他属性 (2)适用于同一进程的多线程/也适用于多进程的排他操作 (3)容错性:获得锁的进程若Crash,不影响到后续进程的正常获取锁 二 .解决方案 1. 最初的构想 在Java领域,同进程的多线程排他实现还是较简易的.比如使用线程同步变量标示是否已锁状态便可.但不同进程的排他实现就比较繁琐.使用已有API,自然想到 java.nio.channels.FileLock:如下 /** * @param file

  • Java分布式锁的三种实现方案

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的left_count减1,在并发的情况下如何要保证left_count不为负数,乐观锁的实现方式为在红包表上添加一个版本号字段(version),默认为0. 异常实现流程 -- 可能会发生的异常情况 -- 线程1查询,当前left_count为1,则有记录 select * from t_bonus

  • java 多线程-锁详解及示例代码

    自 Java 5 开始,java.util.concurrent.locks 包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需要去了解怎样使用这些锁. 一个简单的锁 让我们从 java 中的一个同步块开始: public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } } 可以看到在 inc()方法中有一个 synchronized(th

  • Java 锁的知识总结及实例代码

    java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够.于是再次翻看了一下书里的内容,突然有点打开脑门的感觉.看来确实是要学习的最好方式是要带着问题去学,并且解决问题. 在java中锁主要两类:内部锁synchronized和显示锁java.util.concurrent.locks.Lock.但细细想这貌似总结的也不太对.应该是由java内置的锁和concurrent实现的一系列锁. 为什么这说,因为在java中一切都是对象,而java对每

  • java锁机制ReentrantLock源码实例分析

    目录 一:简述 二:ReentrantLock类图 三:流程简图 四:源码分析 lock()源码分析: 非公平实现: 公平锁实现: tryAcquire()方法 公平锁实现: 非公平锁实现: addWaiter() acquireQueued() shouldParkAfterFailedAcquire() parkAndCheckInterrupt() unlock()方法源码分析: tryRelease() unparkSuccessor() 五:总结 一:简述 ReentrantLock是

  • Java使用正则表达式(regex)匹配中文实例代码

    只能输入中文 /** * 22.验证汉字 * 表达式 ^[\u4e00-\u9fa5]{0,}$ * 描述 只能汉字 * 匹配的例子 清清月儿 */ @Test public void a1() { Scanner sc = new Scanner(System.in); String input = sc.nextLine(); String regex = "^[\\u4e00-\\u9fa5]*$"; Matcher m = Pattern.compile(regex).matc

  • Java web的读取Excel简单实例代码

    目录结构: Data.xls数据: 后台页面: public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //System.out.println(this.getServletContext().getRealPath ("/")); try{ Workbook wb = Workbook.getWorkbook(

  • Java Web 简单的分页显示实例代码

    本文通过两个方法:(1)计算总的页数. (2)查询指定页数据,实现简单的分页效果. 思路:首先得在 DAO 对象中提供分页查询的方法,在控制层调用该方法查到指定页的数据,在表示层通过 EL 表达式和 JSTL 将该页数据显示出来. 先给大家展示下效果图: 题外话:该分页显示是用 "表示层-控制层-DAO层-数据库"的设计思想实现的,有什么需要改进的地方大家提出来,共同学习进步.废话不多说了,开始进入主题,详细步骤如下所示: 1.DAO层-数据库 JDBCUtils 类用于打开和关闭数据

  • java 日期各种格式之间的相互转换实例代码

    java 日期各种格式之间的相互转换实例代码 java日期各种格式之间的相互转换,直接调用静态方法 实例代码: java日期各种格式之间的相互转换,直接调用静态方法 package com.hxhk.cc.util; import java.text.SimpleDateFormat; import java.util.Date; import com.lowagie.text.pdf.codec.postscript.ParseException; public class DateUtil

  • java 文件大数据Excel下载实例代码

    java 文件大数据Excel下载实例代码 excel可以用xml表示.故可以以此来实现边写边下载文件 package com.tydic.qop.controller; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.I

  • iOS中关于音乐锁屏控制音乐(锁屏信息设置)的实例代码

    废话不多说了,直接给大家贴代码了,具体代码如下所示: <pre name="code" class="objc">appDelegate里面加入如下代码获取后台播放权限</pre><pre name="code" class="objc">- (void)setAudioBackstagePlay{ AVAudioSession *audioSession = [AVAudioSession

  • Java 大小写最快转换方式实例代码

    Java 大小写最快转换方式实例代码          这里直接给出实现代码,在代码中注释都很清楚,不多做介绍. Java代码 package io.mycat; import java.util.stream.IntStream; /** * 小写字母的 'a'=97 大写字母 A=65 更好相差32利用这个差进行大小写转换 * @author : Hpgary * @date : 2017年5月3日 10:26:26 * @mail: hpgary@qq.com * */ public cl

  • Java中自定义异常详解及实例代码

    Java中自定义异常详解及实例代码 下面做了归纳总结,欢迎批评指正 自定义异常 class ChushulingException extends Exception { public ChushulingException(String msg) { super(msg); } } class ChushufuException extends Exception { public ChushufuException(String msg) { super(msg); } } 自定义异常 En

随机推荐