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

自 Java 5 开始,java.util.concurrent.locks 包中包含了一些锁的实现,因此你不用去实现自己的锁了。但是你仍然需要去了解怎样使用这些锁。

一个简单的锁

让我们从 java 中的一个同步块开始:

public class Counter{
  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

可以看到在 inc()方法中有一个 synchronized(this)代码块。该代码块可以保证在同一时间只有一个线程可以执行 return ++count。虽然在 synchronized 的同步块中的代码可以更加复杂,但是++count 这种简单的操作已经足以表达出线程同步的意思。

以下的 Counter 类用 Lock 代替 synchronized 达到了同样的目的:

public class Counter{
  private Lock lock = new Lock();
  private int count = 0;

  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}

lock()方法会对 Lock 实例对象进行加锁,因此所有对该对象调用 lock()方法的线程都会被阻塞,直到该 Lock 对象的 unlock()方法被调用。

这里有一个 Lock 类的简单实现:

public class Counter{
public class Lock{
  private boolean isLocked = false;

  public synchronized void lock()
    throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}

注意其中的 while(isLocked)循环,它又被叫做“自旋锁”。当 isLocked 为 true 时,调用 lock()的线程在 wait()调用上阻塞等待。为防止该线程没有收到 notify()调用也从 wait()中返回(也称作虚假唤醒),这个线程会重新去检查 isLocked 条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果 isLocked 为 false,当前线程会退出 while(isLocked)循环,并将 isLocked 设回 true,让其它正在调用 lock()方法的线程能够在 Lock 实例上加锁。

当线程完成了临界区(位于 lock()和 unlock()之间)中的代码,就会调用 unlock()。执行 unlock()会重新将 isLocked 设置为 false,并且通知(唤醒)其中一个(若有的话)在 lock()方法中调用了 wait()函数而处于等待状态的线程。

锁的可重入性

Java 中的 synchronized 同步块是可重入的。这意味着如果一个 java 线程进入了代码中的 synchronized 同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个 java 代码块。下面是一个例子:

public class Reentrant{
  public synchronized outer(){
    inner();
  }

  public synchronized inner(){
    //do something
  }
}

注意 outer()和 inner()都被声明为 synchronized,这在 Java 中和 synchronized(this)块等效。如果一个线程调用了 outer(),在 outer()里调用 inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。

前面给出的锁实现不是可重入的。如果我们像下面这样重写 Reentrant 类,当线程调用 outer()时,会在 inner()方法的 lock.lock()处阻塞住。

public class Reentrant2{
  Lock lock = new Lock();

  public outer(){
    lock.lock();
    inner();
    lock.unlock();
  }

  public synchronized inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}

调用 outer()的线程首先会锁住 Lock 实例,然后继续调用 inner()。inner()方法中该线程将再一次尝试锁住 Lock 实例,结果该动作会失败(也就是说该线程会被阻塞),因为这个 Lock 实例已经在 outer()方法中被锁住了。

两次 lock()之间没有调用 unlock(),第二次调用 lock 就会阻塞,看过 lock()实现后,会发现原因很明显:

public class Lock{
  boolean isLocked = false;

  public synchronized void lock()
    throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}

一个线程是否被允许退出 lock()方法是由 while 循环(自旋锁)中的条件决定的。当前的判断条件是只有当 isLocked 为 false 时 lock 操作才被允许,而没有考虑是哪个线程锁住了它。

为了让这个 Lock 类具有可重入性,我们需要对它做一点小的改动:

public class Lock{
  boolean isLocked = false;
  Thread lockedBy = null;
  int lockedCount = 0;

  public synchronized void lock()
    throws InterruptedException{
    Thread callingThread =
      Thread.currentThread();
    while(isLocked && lockedBy != callingThread){
      wait();
    }
    isLocked = true;
    lockedCount++;
    lockedBy = callingThread;
 }

  public synchronized void unlock(){
    if(Thread.curentThread() ==
      this.lockedBy){
      lockedCount--;

      if(lockedCount == 0){
        isLocked = false;
        notify();
      }
    }
  }

  ...
}

注意到现在的 while 循环(自旋锁)也考虑到了已锁住该 Lock 实例的线程。如果当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该 Lock 实例加了锁,那么 while 循环就不会被执行,调用 lock()的线程就可以退出该方法(译者注:“被允许退出该方法”在当前语义下就是指不会调用 wait()而导致阻塞)。

除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。否则,一次 unblock()调用就会解除整个锁,即使当前锁已经被加锁过多次。在 unlock()调用没有达到对应 lock()调用的次数之前,我们不希望锁被解除。

现在这个 Lock 类就是可重入的了。

锁的公平性

Java 的 synchronized 块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的 synchronized 同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。本文所展现的锁在内部是用 synchronized 同步块实现的,因此它们也不保证公平性。

在 finally 语句中调用 unlock()

如果用 Lock 来保护临界区,并且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:

lock.lock();
try{
  //do critical section code,
  //which may throw exception
} finally {
  lock.unlock();
}

这个简单的结构可以保证当临界区抛出异常时 Lock 对象可以被解锁。如果不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会导致其它所有在该 Lock 对象上调用 lock()的线程一直阻塞。

以上就是关于 java 多线程锁的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!

(0)

相关推荐

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

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

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

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

  • 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分布式锁的三种实现方案

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(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 Socket编程详解及示例代码

    Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序. 餐前甜点 Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本.当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限.一旦IO操作对象被打开,那么这个用户进程可以对这个

  • Java Lambda 表达式详解及示例代码

    Java Lambda 表达式是 Java 8 引入的一个新的功能,可以说是模拟函数式编程的一个语法糖,类似于 Javascript 中的闭包,但又有些不同,主要目的是提供一个函数化的语法来简化我们的编码. Lambda 基本语法 Lambda 的基本结构为 (arguments) -> body,有如下几种情况: 参数类型可推导时,不需要指定类型,如 (a) -> System.out.println(a) 当只有一个参数且类型可推导时,不强制写 (), 如 a -> System.o

  • java Signleton模式详解及示例代码

    Singleton模式是创建模式. 这种模式只涉及一个类是负责创建自己的对象. 该类确保只有一个对象获得创建. 这个类提供了一种方法来访问它的唯一对象. 例如,当设计一个用户界面,我们只能有一个主应用程序的窗口.我们可以使用Singleton模式,以确保有是MainApplicationWindow对象的一个​​实例. 下面的代码将创建一个主窗口类. MainWindow类有其私有的构造,并有其自身的静态实例. 主窗口类提供了一个静态方法来获取其静态实例外面的世界. 我们的演示类将使用主窗口类来

  • Java 多线程实例详解(三)

    本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 1000); DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); Thr

  • AngularJs bootstrap详解及示例代码

    AngularJs学习笔记系列第一篇,希望我可以坚持写下去.本文内容主要来自 http://docs.angularjs.org/guide/ 文档的内容,但也加入些许自己的理解与尝试结果. 一.总括 本文用于解释Angular初始化的过程,以及如何在你有需要的时候对Angular进行手工初始化. 二.Angular <script> 标签 本例用于展示如何通过推荐的路径整合Angular,实现自动初始化. <!doctype html> <html xmlns:ng=&qu

  • Java多线程编程实现socket通信示例代码

    流传于网络上有关Java多线程通信的编程实例有很多,这一篇还算比较不错,代码可用.下面看看具体内容. TCP是Tranfer Control Protocol的 简称,是一种面向连接的保证可靠传输的协议.通过TCP协议传输,得到的是一个顺序的无差错的数据流.发送方和接收方的成对的两个socket之间必须建 立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以

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

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

  • Java 多线程实例详解(二)

    本文承接上一篇文章<Java多线程实例详解(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法. 1.join() join -- 让一个线程等待另一个线程完成才继续执行.如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行. public class ThreadTest { public static void main(String[] args) {

  • Java多线程ThreadPoolExecutor详解

    目录 1 newFixedThreadPool 2 newCachedThreadPool 3 newSingleThreadExecutor 4 提交任务 5 关闭线程池 前言: 根据ThreadPoolExecutor的构造方法,JDK提供了很多工厂方法来创建各种用途的线程池. 1 newFixedThreadPool public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolE

随机推荐

其他