详解Java同步—线程锁和条件对象

线程锁和条件对象

在大多数多线程应用中,都是两个及以上线程需要共享对同一数据的存取,所以有可能出现两个线程同时访问同一个资源的情况,这种情况叫做:竞争条件。

在Java中为了解决并发的数据访问问题,一般使用锁这个概念来解决。

有几种机制防止代码收到并发访问的干扰:

1.synchronized关键字(自动创建一个锁及相关的条件)

2.ReentrantLock类+Java.util.concurrent包中的lock接口(在Java5.0的时候引入)

ReentrantLock的使用

public void Method() {
    boolean flag = false;//标识条件
    ReentrantLock locker = new ReentrantLock();
    locker.lock();//开启线程锁
    try {
      //do some work...
    } catch (Exception ex) {

    } finally {
      locker.unlock();//解锁线程
    }
  }

locker.lock();确保只有一个线程进入临界区,一旦一个线程进入之后,会获得锁对象,其他线程无法通过lock语句。当其他线程调用lock时,它们会被阻塞,知道第一个线程释放锁对象。

locker.unlock();解锁操作,一定要放到finally里,因为如果try语句里出了问题,锁必须被释放,否则其他线程将永远被阻塞

因为系统会随机为线程分配资源,所以在线程获得锁对象之后,可能被系统剥夺运行权,这时候其他线程来访问,但是发现有锁,进不去,只能等拿到锁对象的线程把里面的代码执行完毕后,释放锁,第二个线程才能运行。

假设说做一个银行转账的功能,线程锁操作应该定义在银行类的转账方法里,因为这样每个银行对象都有一个锁对象,两个线程访问一个银行对象的时候,那么锁以串行方式提供服务。但是,如果每个线程访问不同的银行对象,每个线程都会得到不同的锁对象,彼此之间不会冲突,所以就不会造成不必要的线程阻塞。

锁是可重入的,线程可以重复获得已经持有的锁,锁通过一个持有数量计数来跟踪对lock方法的嵌套使用。

假设说,一个线程获得锁之后,要执行A方法,但是A方法里面又调用了B方法,这时候这个线程获得了两个锁对象,当线程执行B方法的时候,也会被锁死,防止其他线程乱入,当B方法执行完毕后,锁对象变成了一个,当A方法也执行完毕的时候,锁对象变成了0个,线程释放锁。

synchronized关键字

前面我们讲了ReentrantLock锁对象的使用,但是在系统里面我们不一定要使用ReentrantLock锁,Java中还提供了一个内部的隐式锁,关键字是synchronized.

举个例子:

public synchronized void Method() {
  //do some work...
}

只需要在返回值前面加上synchronized锁,就会实现上面ReentrantLock锁同样的效果.

Conditional条件对象

通常,线程拿到锁对象之后,却发现需要满足某一条件才能继续向下执行。

拿银行程序来举例子,我们需要转账方账户有足够的资金才能转出到目标账户,这时候需要用到ReentrantLock对象,因为如果我们已经完成转账方账户有足够的资金的判断之后,线程被其他线程中断,等其他线程执行完之后,转账方的钱又没有了足够的资金,这时候因为系统已经完成了判断,所以会继续向下执行,然后银行系统就会出现问题。

举例:

public void Transfer(int from, int to, double amount) {
  if (Accounts[from] > amount)//系统在结束判断之后被剥夺运行权,然后账户通过网银转出所有钱,银行凉凉
    DoTransfer(from, to, amount);
}

这时候我们就需要使用ReentrantLock对象了,我们修改一下代码:

public void Transfer(int from, int to, double amount) {
  ReentrantLock locker = new ReentrantLock();
  locker.lock();
  try {
    while (Accounts[from] < amount) {
      //等待有足够的钱
    }
    DoTransfer(from, to, amount);
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
}

但是这样又有了问题,当前线程获取了锁对象之后,开始执行代码,发现钱不够,进入等待状态,然后其他线程又因为锁的原因无法给该账户转账,就会一直进入等待状态。

这个问题如何解决呢?

条件对象登场!

public void Transfer(int from, int to, double amount) {
  ReentrantLock locker = new ReentrantLock();
  Condition sufficientFunds = locker.newCondition();//条件对象,
  lock.lock();
  try {
    while (Accounts[from] < amount) {
      sufficientFunds.await();
      //等待有足够的钱
    }
    DoTransfer(from, to, amount);
    sufficientFunds.signalAll();
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
}

条件对象的关键字是:Condition,一个锁对象可以有一个或多个相关的条件对象。可以通过锁对象.newCondition方法获得一个条件对象.

一般关于条件对象的命名需要能够反映它表达的条件的名字,所以在这里我们叫他sufficientFund,表示余额充足的意思。

在进入锁之前,我们创建一个条件,然后如果金额不足,在这里调用条件对象的await方法,通知系统当前线程进入挂起状态,让其他线程执行。这样你这次调用会被锁定,然后系统可以再次调用该方法给其他账户转账,当每一次转账完成后,执行转账操作的线程在底部调用signalAll通知所有线程可以继续运行了,因为我们有可能是转足够的钱给当前账户,这时候有可能该线程会继续执行(不一定是你,是通知所有线程,如果通知的线程还是不符合条件,会继续调用await方法,并完成转账操作,然后通知其他挂起的线程。

你说为啥不直接通知当前线程?不行,可以调用signal方法只通知一个线程,但是如果这个线程操作的账户还是没钱(不是转账给这个账户的情况),那这个线程又进入等待了,这时候已经没有线程能通知其他线程了,程序死锁,所以还是用signal比较保险。

以上是使用ReentrantLock+Condition对象,那你说我要是使用synchronized隐式锁怎么办?

也可以,而且不需要

public void Transfer(int from, int to, double amount) {
   while (Accounts[from] < amount) {
      wait();//这个wait方法是定义在Object类里面的,可以直接用,和条件对象的await一样,挂起线程
      //等待有足够的钱
    }
    DoTransfer(from, to, amount);
    notifyAll();//通知其他挂起的线程
}

Object类里面定义了wait、notifyAll、notify方法,对应await、signalAll和signal方法,用来操作隐式锁,synchronized只能有一个条件,而ReentrantLock显式声明的锁可以用绑定多个Condition条件.

同步块

除了我们上面讲的两种获取线程锁的方式,还有另外一种机制获得锁,这种方式比较特殊,叫做同步块:

Object locker = new Object();
synchronized (locker) {
  //do some work
}

//也可以直接锁当前类的对象
sychronized(this){
  //do some work
}

以上代码会获得Object类型locker对象的锁,这种锁是一个特殊的锁,在上面的代码中,创建这个Object类对象只是单纯用来使用其持有的锁.

这种机制叫做同步块,应用场景也很广:有的时候,我们并不是整个一个方法都需要同步,只是方法里的部分代码块需要同步,这种情况下,我们如果将这个方法声明为synchronized,尤其是方法很大的时候,会造成很大的资源浪费。所以在这种情况下我们可以使用synchronized关键字来声明同步块:

public void Method() {
  //do some work without synchronized
  synchronized (this) {
    //do some synchronized operation
  }
}

监视器的概念

锁和条件是同步中一个很重要的工具,但是它们并不是面向对象的。多年来,Java的研究人员努力寻找一种方法,可以在不需要考虑如何加锁的情况下,就能保证多线程的安全性。最成功的的一个解决方案叫做monitor监视器,这个对象内置于每一个Object变量中,相当于一个许可证。拿到许可证就可以进行操作,没有拿到则需要阻塞等待。

监视器具有以下特性:

1.监视器是只包含私有域的类

2.每个监视器对象都有一个相关的锁

3.使用监视器对象的锁对所有的方法进行加锁(举个例子:如果调用obj.Method方法,obj对象的锁会在方法调用的时候自动获得,当方法结束或返回之后会自动释放该锁。因为所有的域都是私有的,这样可以确保一个线程在操作类对象的时候,没有其他线程可以访问里面的域)

4.该锁对象可以有任意多个相关条件

你也可以自己创建一个监视器类,只要符合以上的要求即可。

其实我们使用的synchronized关键字就是使用了monitor来实现加锁解锁,所以又被称为内部锁。因为Object类实现了监视器,所以对象又被内置于任何一个对象之中。这就是我们为什么可以使用synchronized(locker)的方式锁定一个代码块了,其实只是用到了locker对象中内置的monitor而已。每一个对象的monitor类又是唯一的,所以就是唯一的许可证,拿到许可证的线程才可以执行,执行完后释放对象的monitor才可以被其他线程获取。

举个例子:

synchronized (this) {
  //do some synchronized operation
}

它在字节码文件中会被编译为:

monitorenter;//get monitor,enter the synchronized block
      //do some synchronized operation
monitorexit;//leavel the synchronized block,release the monitor

死锁

虽然有了线程可以保证原子性,但是锁和条件不能解决多线程中的所有问题,举个例子:

账户1余额:200

账户2余额:300

线程1:账户1→账户2(300)

线程2:账户2→账户1(400)

因为线程1和线程2的金额都不足以进行转账,所以两个线程都阻塞了,这种状态就叫死锁(deadlock),如果所有线程死锁,程序就卡死了。

为什么倾向于使用signalAll和notifyAll方式,如果假设使用signal和notify,

锁测试和超时

线程在调用lock方法获得另一个线程持有的锁的时候,很可能发生阻塞。应该更加谨慎的申请锁,tryLock方法试图申请一个锁,如果申请成功,返回true,否则,立刻返回false,线程就会离开去做别的事,而不是被阻塞等待锁对象。

语法:

ReentrantLock locker = new ReentrantLock();
if (locker.tryLock()) {
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
} else {
  //do other work
}

也可以给其指定超时参数,单位有SECONDS、MILLISECONDS、MICROSEONDS和MANOSECONDS.

ReentrantLock locker = new ReentrantLock();
if (locker.tryLock(1000, TimeUnit.MILLISECONDS)) {
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    locker.unlock();
  }
} else {
  //do other work
}

lock方法不能被中断,如果一个线程在调用了lock方法后等待锁的时候被中断,中断线程在获得锁之前一直处于阻塞状态。

如果带有超时参数的tryLock方法,那么如果等待期间线程被中断,会抛出InterruptedException异常,这是一个很好的特性,允许程序打破死锁。

读/写锁

ReentrantLock类属于java.util.concurrent.locks包,这个包底下还有一个ReentrantReaderWriterLock类,如果使用多线程对数据读的操作很多,但是写的操作很少的话,可以使用这个类。

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():

public void Read() {
  Lock readLocker = rwl.readLock();//创建读取锁对象
  readLocker.lock();//使用读取锁对象加锁
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    readLocker.unlock();
  }
}

public void Write() {
  Lock writeLocker = rwl.writeLock();//创建写入锁对象
  writeLocker.lock();//使用写入锁对象加锁
  try {
    //do some work
  } catch (Exception ex) {
    ex.printStackTrace();
  } finally {
    writeLocker.unlock();
  }
}
(0)

相关推荐

  • java 线程锁详细介绍及实例代码

    java 线程锁 在Java线程中运用synchronized关键字来达到同步的 synchronized可以锁方法,锁类,锁对象,锁代码块 方法锁 // 加在方法上面的同步锁是this public synchronized void print() { System.out.println("同步方法"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } 类锁

  • 深入解析Java并发程序中线程的同步与线程锁的使用

    synchronized关键字 synchronized,我们谓之锁,主要用来给方法.代码块加锁.当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码.当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段.但是,其余线程是可以访问该对象中的非加锁代码块的. synchronized主要包括两种方法:synchronized 方法.synchronized 块. synchron

  • Java多线程编程中线程锁与读写锁的使用示例

    线程锁Lock Lock  相当于 当前对象的 Synchronized import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /* * Lock lock = new ReentrantLock(); * lock.lock(); lock.unLock(); * 类似于 synchronized,但不能与synchronized 混用 */ public class L

  • 详解Java同步—线程锁和条件对象

    线程锁和条件对象 在大多数多线程应用中,都是两个及以上线程需要共享对同一数据的存取,所以有可能出现两个线程同时访问同一个资源的情况,这种情况叫做:竞争条件. 在Java中为了解决并发的数据访问问题,一般使用锁这个概念来解决. 有几种机制防止代码收到并发访问的干扰: 1.synchronized关键字(自动创建一个锁及相关的条件) 2.ReentrantLock类+Java.util.concurrent包中的lock接口(在Java5.0的时候引入) ReentrantLock的使用 publi

  • 详解Java中的锁Lock和synchronized

    一.Lock接口 1.Lock接口和synchronized内置锁 a)synchronized:Java提供的内置锁机制,Java中的每个对象都可以用作一个实现同步的锁(内置锁或者监视器Monitor),线程在进入同步代码块之前需要或者这把锁,在退出同步代码块会释放锁.而synchronized这种内置锁实际上是互斥的,即没把锁最多只能由一个线程持有. b)Lock接口:Lock接口提供了与synchronized相似的同步功能,和synchronized(隐式的获取和释放锁,主要体现在线程进

  • 详解java中各类锁的机制

    目录 前言 1. 乐观锁与悲观锁 2. 公平锁与非公平锁 3. 可重入锁 4. 读写锁(共享锁与独占锁) 6. 自旋锁 7. 无锁 / 偏向锁 / 轻量级锁 / 重量级锁 前言 总结java常见的锁 区分各个锁机制以及如何使用 使用方法 锁名 考察线程是否要锁住同步资源 乐观锁和悲观锁 锁住同步资源后,要不要阻塞 不阻塞可以使用自旋锁 一个线程多个流程获取同一把锁 可重入锁 多个线程公用一把锁 读写锁(写的共享锁) 多个线程竞争要不要排队 公平锁与非公平锁 1. 乐观锁与悲观锁 悲观锁:不能同时

  • 详解Java的线程状态

    Java的每个线程都具有自己的状态,Thread类中成员变量threadStatus存储了线程的状态: private volatile int threadStatus = 0; 在Thread类中也定义了状态的枚举,共六种,如下: public enum State { NEW, // 新建状态 RUNNABLE, // 执行状态 BLOCKED, // 阻塞状态 WAITING, // 无限期等待状态 TIMED_WAITING, // 有限期等待状态 TERMINATED; // 退出状

  • 详解Java停止线程的四种方法

    一.线程停止基础知识 interrupted(): 测试当前线程是否已经中断.该方法为静态方法,调用后会返回boolean值.不过调用之后会改变线程的状态,如果是中断状态调用的,调用之后会清除线程的中断状态. isInterrupted(): 测试线程是否已经中断.该方法由对象调用 interrupt(): 标记线程为中断状态,不过不会中断正在运行的线程. stop(): 暴力停止线程.已弃用. 二.停止线程方法1:异常法停止 线程调用interrupt()方法后,在线程的run方法中判断当前对

  • 详解Java创建线程的五种常见方式

    目录 Java中如何创建线程呢? 1.显示继承Thread,重写run来指定现成的执行代码. 2.匿名内部类继承Thread,重写run来执行线程执行的代码. 3.显示实现Runnable接口,重写run方法. 4.匿名内部类实现Runnable接口,重写run方法 5.通过lambda表达式来描述线程执行的代码 [面试题]:Thread的run和start之间的区别? Thread类的具体用法 Thread类常见的一些属性 中断一个线程 1.方法一:让线程run完 2.方法二:调用interr

  • 详解Java子线程异常时主线程事务如何回滚

    一.提出问题 最近有一位朋友问了我这样一个问题,问题的截图如下: 这个问题问的相对比较笼统,我来稍微详细的描述下:主线程向线程池提交了一个任务,如果执行这个任务过程中发生了异常,如何让主线程捕获到该异常并且进行事务的回滚. 二.主线程与子线程 先来看看基础,下图体现了两种线程的运行方式, 左侧的图,体现了主线程启动一个子线程之后,二者互不干扰独立运行,生死有命,从此你我是路人! 右侧的图,体现了主线程启动一个子线程之后继续执行主线程程序逻辑,在某一节点通过阻塞的方式来获取子线程的执行结果. 对于

  • 详解java创建一个女朋友类(对象啥的new一个就是)==建造者模式,一键重写

    创建一个女朋友,她有很多的属性,比如:性别,年龄,身高,体重,类型等等,虽然每个女朋友都有这些属性,但是每个人找女朋友的要求都是不一样的,有的人喜欢男的,有的人喜欢女的,有的喜欢胖的,不同的人可以根据自己的喜好去建造不同的女朋友,我们不需要关心她是怎么建造的,我们只需要去指定她的属性就行了 相比如文字解释,我更习惯撸代码来解释,下面来一步步实现怎么用java来为你创建一个女朋友 首先定义一个女朋友类: package nuoyanli; /** * Created by ${nuoyanli}

  • 详解Java的线程的优先级以及死锁

    Java线程优先级 需要避免的与多任务处理有关的特殊错误类型是死锁(deadlock).死锁发生在当两个线程对一对同步对象有循环依赖关系时.例如,假定一个线程进入了对象X的管程而另一个线程进入了对象Y的管程.如果X的线程试图调用Y的同步方法,它将像预料的一样被锁定.而Y的线程同样希望调用X的一些同步方法,线程永远等待,因为为到达X,必须释放自己的Y的锁定以使第一个线程可以完成.死锁是很难调试的错误,因为: 通常,它极少发生,只有到两线程的时间段刚好符合时才能发生. 它可能包含多于两个的线程和同步

  • 详解C++11中的线程锁和条件变量

    线程 std::thread类, 位于<thread>头文件,实现了线程操作.std::thread可以和普通函数和 lambda 表达式搭配使用.它还允许向线程的执行函数传递任意多参数. #include <thread> void func() { // do some work } int main() { std::thread t(func); t.join(); return 0; } 上面的例子中,t是一个线程实例,函数func()在该线程运行.调用join()函数是

随机推荐