在上篇文章(3.Java多线程总结系列:Java的线程同步实现)中,我们介绍了用synchronized关键字实现线程同步。但在Java中还有一种方式可以实现线程同步,那就是Lock锁。

一.同步锁

我们还是用同步锁来实现存取款的例子:

package com.chanshuyi.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class ThreadDemo93 { public static void main(String[] args) {
Account account = new Account(2300);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
}
} class DepositeThread extends Thread{ private Account account; public DepositeThread(Account account){
this.account = account;
} @Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
//模拟存钱的时间间隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class DrawMoneyThread extends Thread{ private Account account; public DrawMoneyThread(Account account){
this.account = account;
} @Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
//模拟取钱的时间间隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class Account{ private Lock lock = new ReentrantLock(); //存钱
public void deposit(double amount, int i){
lock.lock();
try {
Thread.sleep((long)Math.random()*10000); //模拟存钱的延迟
this.balance = this.balance + amount;
System.out.println("***第" + i + "次,存入钱:" + amount);
System.out.println("***第" + i + "次,存钱后账户余额:" + this.balance);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
} //取钱
public void withdraw(double amount, int i){
lock.lock();
try {
Thread.sleep((long)Math.random()*10000); //模拟取钱的延迟
if(this.balance >= amount){
this.balance = this.balance - amount;
System.out.println("第" + i + "次,取出钱:" + amount);
System.out.println("第" + i + "次,取钱后账户余额:" + this.balance);
}else{
System.out.println("第" + i + "次,余额不足");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
} public Account(){
} public Account(double balance){
this.balance = balance;
} private double balance;
}

当我们进入需要同步的代码时,我们调用lock.lock()方法获取锁对象。当退出同步代码块时,使用lock.unlock()释放锁对象。下面是其中的一个输出:

第1次,取出钱:100.0
第1次,取钱后账户余额:2200.0
***第1次,存入钱:200.0
***第1次,存钱后账户余额:2400.0
第2次,取出钱:100.0
第2次,取钱后账户余额:2300.0
***第2次,存入钱:200.0
***第2次,存钱后账户余额:2500.0
第3次,取出钱:100.0
第3次,取钱后账户余额:2400.0
***第3次,存入钱:200.0
***第3次,存钱后账户余额:2600.0
第4次,取出钱:100.0
第4次,取钱后账户余额:2500.0
***第4次,存入钱:200.0
***第4次,存钱后账户余额:2700.0
第5次,取出钱:100.0
第5次,取钱后账户余额:2600.0
***第5次,存入钱:200.0
***第5次,存钱后账户余额:2800.0
第6次,取出钱:100.0
第6次,取钱后账户余额:2700.0
第7次,取出钱:100.0
第7次,取钱后账户余额:2600.0
第8次,取出钱:100.0
第8次,取钱后账户余额:2500.0
第9次,取出钱:100.0
第9次,取钱后账户余额:2400.0
***第6次,存入钱:200.0
***第6次,存钱后账户余额:2600.0
***第7次,存入钱:200.0
***第7次,存钱后账户余额:2800.0
第10次,取出钱:100.0
第10次,取钱后账户余额:2700.0
***第8次,存入钱:200.0
***第8次,存钱后账户余额:2900.0
***第9次,存入钱:200.0
***第9次,存钱后账户余额:3100.0
***第10次,存入钱:200.0
***第10次,存钱后账户余额:3300.0

上面这个例子只是实现了存款和取款的隔离,使其不能同时进行存取款操作。但是没有考虑到余额不足的情况,所以当我们将初始账户的余额改为0时,其最后的余额就不是准确的数据了。那ReentrantLock锁能使用wait()/notify()进行线程通信么?答案是可以,但是不是使用wait()/notify(),而是使用Condition对象的await()和signal()方法。

二.用Condition进行线程间通信

package com.chanshuyi.thread.part3.part36;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* 银行存取款 - 使用Lock锁进行线程同步,用Condition进行线程通信
* 实现效果:不会出现余额不足的情况
* @author yurongchan
*
*/
public class ThreadDemo1 { public static void main(String[] args) {
Account account = new Account(0);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
} } class DepositeThread extends Thread{ private Account account; public DepositeThread(Account account){
this.account = account;
} @Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
//模拟存钱的时间间隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class DrawMoneyThread extends Thread{ private Account account; public DrawMoneyThread(Account account){
this.account = account;
} @Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
//模拟取钱的时间间隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} } class Account{ private Lock lock = new ReentrantLock();
private Condition sufficientFunds = lock.newCondition(); //存钱
public void deposit(double amount, int i){
System.out.println("***取款进程" + i + "准备存款.");
lock.lock();
try {
Thread.sleep((long)Math.random()*10000); //模拟存钱的延迟
this.balance = this.balance + amount;
System.out.println("***存款进程" + i + "存款" + amount);
System.out.println("***存款进程" + i + "存款后账户余额:" + this.balance);
sufficientFunds.signalAll(); //每次存入钱就唤醒其他存钱进程,通知其余额可能足够取款了
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
} //取钱
public void withdraw(double amount, int i){
System.out.println("---取款进程" + i + "准备取款.");
lock.lock();
while (this.balance < amount) {
try {
System.out.println("---取款进程" + i + "准备取款时发现余额不足.放弃对象锁,进入阻塞状态.");
sufficientFunds.await(); // 余额不足,等待
System.out.println("---取款进程" + i + "被唤醒,重新尝试取款.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("---账户余额充足.取款进程" + i + "开始取款.");
try {
Thread.sleep((long) Math.random() * 10000); // 模拟取钱的延迟
this.balance = this.balance - amount;
System.out.println("---取款进程" + i + "成功取款:" + amount);
System.out.println("---取款进程" + i + "成功取款后余额:" + this.balance);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public Account(){ } public Account(double balance){
this.balance = balance;
} private double balance;
}

上面的代码我们故意将账户余额设置为0,虚拟了账户余额不足的情况。之后我们通过lock.newCondition方法创建了一个sufficientFunds的条件,当我们判断得出余额不足的时候,我们调用sufficientFunds.await()方法让该线程放弃条件(Condition)对应的锁对象,让其他存款进程可以往账户中取款。当存款进程成功存款之后,存款进程调用sufficientFunds.signal()方法通知取款进程说:我存钱进去了,你再看看钱够不够。这时候取款进程继续运行,判断余额是否足够,如果不足则继续放弃锁对象并等待,否则就进行取款操作。下面是其中一次运行结果:

---取款进程1准备取款.
---取款进程1准备取款时发现余额不足.放弃对象锁,进入阻塞状态.
***取款进程1准备存款.
***存款进程1存款200.0
***存款进程1存款后账户余额:200.0
---取款进程1被唤醒,重新尝试取款.
---账户余额充足.取款进程1开始取款.
***取款进程2准备存款.
---取款进程1成功取款:100.0
---取款进程1成功取款后余额:100.0
---取款进程2准备取款.
***存款进程2存款200.0
***存款进程2存款后账户余额:300.0
---账户余额充足.取款进程2开始取款.
***取款进程3准备存款.
---取款进程2成功取款:100.0
---取款进程2成功取款后余额:200.0
---取款进程3准备取款.
***存款进程3存款200.0
***存款进程3存款后账户余额:400.0
---账户余额充足.取款进程3开始取款.
***取款进程4准备存款.
---取款进程3成功取款:100.0
---取款进程3成功取款后余额:300.0
---取款进程4准备取款.
***存款进程4存款200.0
***存款进程4存款后账户余额:500.0
---账户余额充足.取款进程4开始取款.
***取款进程5准备存款.
---取款进程4成功取款:100.0
---取款进程4成功取款后余额:400.0
---取款进程5准备取款.
***存款进程5存款200.0
***存款进程5存款后账户余额:600.0
---账户余额充足.取款进程5开始取款.
***取款进程6准备存款.
---取款进程5成功取款:100.0
---取款进程5成功取款后余额:500.0
---取款进程6准备取款.
***存款进程6存款200.0
***存款进程6存款后账户余额:700.0
---账户余额充足.取款进程6开始取款.
***取款进程7准备存款.
---取款进程6成功取款:100.0
---取款进程6成功取款后余额:600.0
---取款进程7准备取款.
***存款进程7存款200.0
***存款进程7存款后账户余额:800.0
---账户余额充足.取款进程7开始取款.
***取款进程8准备存款.
---取款进程7成功取款:100.0
---取款进程7成功取款后余额:700.0
---取款进程8准备取款.
***存款进程8存款200.0
***存款进程8存款后账户余额:900.0
---账户余额充足.取款进程8开始取款.
***取款进程9准备存款.
---取款进程8成功取款:100.0
---取款进程8成功取款后余额:800.0
---取款进程9准备取款.
***存款进程9存款200.0
***存款进程9存款后账户余额:1000.0
---账户余额充足.取款进程9开始取款.
***取款进程10准备存款.
---取款进程9成功取款:100.0
---取款进程9成功取款后余额:900.0
---取款进程10准备取款.
***存款进程10存款200.0
***存款进程10存款后账户余额:1100.0
---账户余额充足.取款进程10开始取款.
---取款进程10成功取款:100.0
---取款进程10成功取款后余额:1000.0

从上面的运行结果我们可以看出,一开始取款线程1准备取款,但是此时账户余额为0,无法取款,于是取款线程1让出对象锁,并等待。存款线程1因此获得了对象锁,并成功存进200,并唤醒了所有取款线程。此时取款线程1苏醒过来并判断账户余额满足了取款需求,于是取款线程1进行取款操作。

到这里我们学会了如何用synchronized和lock进行线程同步,并且利用其相对应的wait()/notify和Condition进行线程间的通信,以实现更高级的功能。

那既然synchronized能实现的功能,为什么还要有lock锁呢?他们之间究竟有什么异同呢?

三.同步线程的实现原理(共同点)

其实无论通过synchronized方法、synchronized代码块、还是Lock锁,他们的共同点都是传入一个唯一的对象,并以这个唯一的对象作为锁来实现线程同步的。虽然使用synchronized方法进行线程同步时并没有显示地传入一个锁对象,但是实际上它默认锁对象的就是synchronized方法所在类的对象(即例子中的Account对象)

三.使用synchronized的wait()/notify() 和 Lock的Condition进行线程通信有什么区别?(不同点)

使用synchronized和lock都能实现线程的通信,但是synchronized和wait()/notify()方法只能实现两个线程之间的通信,当有更多的线程需要互相通信时,wait()/notify()就无法做到了。而Lock对象能通过newCondition()方法创建出无数的"条件",通过这些条件,我们就能够成功地实现多线程(N>3)之间的数据通信,对它们进行控制。

比如我需要用线程实现这样的功能:有老大、老二、老三三个人,我们要这3个人进行报数,报数的顺序是这样的,首先是老大报数,报5次;然后是老二报数,报5次;之后是老三报数,报5次;就这样一直报两轮。

我们可以用三个方法分别代表三个人的报数,outputOne()方法代表老大报数,outputTwo()方法代表老二报数,outputThree()方法代表老三报数。接下来就是如果确保他们是按我们需要的顺序进行报数的。这时候如果我们用wait()和notify()方法的话,当老大报完数使用notifyAll()方法唤醒线程,这时候老二和老三都会抢着去报数,这时候我们是无法对其进行准确的顺序控制的。

但Lock锁的newCondition()方法允许有多个条件(Condition),我们可以创建三个条件,分别代表3个通知,比如:调用condition1.signal()时代表让老大报数,调用condition2.signal()时让老二报数,调用condition3.signal()时让老三报数,这样子就可以精准的控制他们的执行顺序了。实现后的代码如下:

package com.chanshuyi.class13;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* 用Condition实现多个操作的通信协作
* Condition不同于Object.wait()和Object.notify()的一个方面就是Condition能实现多个操作之间的相互协调通信,而Object.wait()和Object.notify()只能是两个操作之间的协调通信
* 这个实例实现3个线程轮流输出
* @author chenyr
* @time 2014-12-24 下午06:40:30
* All Rights Reserved.
*/
public class Condition3 { public static void main(String[] args)throws Exception{
final Outputer outputer = new Outputer();
new Thread(new Runnable(){
public void run(){
for(int i = 1; i <= 2; i++){
try {
outputer.outputOne(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start(); new Thread(new Runnable(){
public void run(){
for(int i = 1; i <= 2; i++){
try {
outputer.outputTwo(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start(); new Thread(new Runnable(){
public void run(){
for(int i = 1; i <= 2; i++){
try {
outputer.outputThree(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
} class Outputer{
private int index = 1; //1表示该老大输出,2表示该老二输出,3表示该老三输出 final Lock lock = new ReentrantLock();
final Condition condition1 = lock.newCondition();
final Condition condition2 = lock.newCondition();
final Condition condition3 = lock.newCondition(); //老大的输出
public void outputOne(int i) throws InterruptedException{
lock.lock();
System.out.println("outputOne-" + i + " has lock.");
try{
while(index != 1){
System.out.println("outputOne-" + i + " has release the lock and wait.");
condition1.await();
}
for(int j = 1; j <= 5; j++){
System.out.println("老大的第" + i + "次,第" + j + "小次");
}
index = 2;
condition2.signal();
}finally{
lock.unlock();
}
} //老二的输出
public void outputTwo(int i)throws InterruptedException{
lock.lock();
System.out.println("outputTwo-" + i + " has lock.");
try{
while(index != 2){
System.out.println("outputTwo-" + i + " has release the lock and wait.");
condition2.await();
}
for(int j = 1; j <= 5; j++){
System.out.println("老二的第" + i + "次,第" + j + "小次");
}
index = 3;
condition3.signal();
}finally{
lock.unlock();
}
} //老三的输出
public void outputThree(int i)throws InterruptedException{
lock.lock();
System.out.println("outputThree-" + i + " has lock.");
try{
while(index != 3){
System.out.println("outputThree-" + i + " has release the lock and Wait.");
condition3.await();
}
for(int j = 1; j <= 5; j++){
System.out.println("老三的第" + i + "次,第" + j + "小次");
}
index = 1;
condition1.signal();
}finally{
lock.unlock();
}
}
}

下面是输出的结果:

 outputOne-1 has lock.
老大的第1次,第1小次
老大的第1次,第2小次
老大的第1次,第3小次
老大的第1次,第4小次
老大的第1次,第5小次
outputThree-1 has lock.
outputThree-1 has release the lock and Wait.
outputTwo-1 has lock.
老二的第1次,第1小次
老二的第1次,第2小次
老二的第1次,第3小次
老二的第1次,第4小次
老二的第1次,第5小次
outputTwo-2 has lock.
outputTwo-2 has release the lock and wait.
outputOne-2 has lock.
outputOne-2 has release the lock and wait.
老三的第1次,第1小次
老三的第1次,第2小次
老三的第1次,第3小次
老三的第1次,第4小次
老三的第1次,第5小次
outputThree-2 has lock.
outputThree-2 has release the lock and Wait.
老大的第2次,第1小次
老大的第2次,第2小次
老大的第2次,第3小次
老大的第2次,第4小次
老大的第2次,第5小次
老二的第2次,第1小次
老二的第2次,第2小次
老二的第2次,第3小次
老二的第2次,第4小次
老二的第2次,第5小次
老三的第2次,第1小次
老三的第2次,第2小次
老三的第2次,第3小次
老三的第2次,第4小次
老三的第2次,第5小次

一开始老大报了5次数,之后老大还准备继续报数(第7行),但是index此时表明应该是老二报数了,所以老大只能时调用condition.await()方法暂时泛起锁(第8行)。之后老二获得了对象锁(第9行)进行了报数……

从线程同步上来看,无论synchronized还是Lock,他们的底层都是通过传入唯一的锁对象来实现线程同步的。

从线程通信来看,synchronized的线程只能实现两个线程之间的通信,但是Condition却可以实现更多线程之间的通信。

【MARK1 本篇的思维导图】

(删)Java线程同步实现二:Lock锁和Condition的更多相关文章

  1. JAVA线程同步 (二)notify()与notifyAll()-***

    编写多线程程序需要进行线程协作,前面介绍的利用互斥来防止线程竞速是来解决线程协作的衍生危害的.编写线程协作程序的关键是解决线程之间的协调问题,在这些任务中,某些可以并行执行,但是某些步骤需要所有的任务 ...

  2. Java线程知识:二、锁的简单使用

    锁的初步认识 说到锁,相信大家都不陌生,这是我们生活中非常常见的一种东西,它的形状也各式各样.在生活中,我们通常用锁来锁住房子的大门.装宠物的笼子.装衣服的衣柜.以及装着我们一些小秘密的小抽屉.... ...

  3. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  4. Java线程同步之一--AQS

    Java线程同步之一--AQS 线程同步是指两个并发执行的线程在同一时间不同时执行某一部分的程序.同步问题在生活中也很常见,就比如在麦当劳点餐,假设只有一个服务员能够提供点餐服务.每个服务员在同一时刻 ...

  5. Java 线程 — synchronized、volatile、锁

    线程同步基础 synchronized 和volatile是Java线程同步的基础. synchronized 将临界区的内容上锁,同一时刻只有一个进程能访问该临界区代码 使用的是内置锁,锁一个时刻只 ...

  6. Java线程同步_1

    Java线程同步_1 synchronized 该同步机制的的核心是同步监视器,任何对象都可以作为同步监视器,代码执行结束,或者程序调用了同步监视器的wait方法会导致释放同步监视器 synchron ...

  7. Java 线程池(二)

    简介 在上篇 Java 线程池(一) 我们介绍了线程池中一些的重要参数和具体含义,这篇我们看一看在 Java 中是如何去实现线程池的,要想用好线程池,只知其然是远远不够的,我们需要深入实现源码去了解线 ...

  8. JAVA - 线程同步和线程调度的相关方法

    JAVA - 线程同步和线程调度的相关方法 wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁:wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等 ...

  9. Java线程同步的四种方式详解(建议收藏)

    ​ Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...

随机推荐

  1. 【UWP】FFmpeg库的编译

    本文是关于windows8.1/windows10通用应用下编译ffmpeg的一些需要注意的地方,针对最新的msys2而写,都是我在实际操作中遇到的,但是网上没有提到的.如果大家遇到什么问题或是在之前 ...

  2. awk,sed文本处理案例

    #!/bin/bash ############################################################################# #针对一个多级目录下 ...

  3. React Native填坑之旅 -- 使用iOS原生视图(高德地图)

    在开发React Native的App的时候,你会遇到很多情况是原生的视图组件已经开发好了的.有的是系统的SDK提供的,有的是第三方试图组件,总之你的APP可以直接使用的原生视图是很多的.React ...

  4. 磁盘工作原理与IO性能分析

    最近,在研究如何优化产品设备的磁盘IO性能,需要深入研究磁盘及文件系统原理和工作机制,下面简要总结下关于磁盘方面的东西,下篇文章再分享文件系统的. 机械磁盘结构: 无论哪种机械硬盘,都主要由盘片.磁头 ...

  5. 文件上传----FTP部署

  6. Node.js~在linux上的部署~pm2管理工具的使用

    之前写了两篇关于在linux上部署nodejs的文章,大家如果没有基础可以先看前两篇<Node.js~在linux上的部署>,<Node.js~在linux上的部署~外网不能访问no ...

  7. JavaScript 图片轮播入门

    轮播要求:一个在页面居中的矩形框,图片依次从矩形框中经过 当图片完整占满矩形框时 停留一小段时间再向左边移动经过矩形框的图片自动跑到右边最后一个图的后面.核心原理:在一个for循环中利用offsetl ...

  8. JAVA反射原理解读

    一.什么是JAVA的反射 1.在运行状态中,对于任意一个类,都能够知道这个类的属性和方法. 2.对于任意一个对象,都能够调用它的任何方法和属性. 这种动态获取信息以及动态调用对象的方法的功能称为JAV ...

  9. Visual Studio 2017 ASP.NET Core开发

    Visual Studio 2017 ASP.NET Core开发,Visual Studio 2017 已经内置ASP.NET Core 开发工具. 在选择.NET Core 功能安装以后就可以进行 ...

  10. 深入理解ajax系列第四篇——FormData

    前面的话 现代Web应用中频繁使用的一项功能就是表单数据的序列化,XMLHttpRequest 2级为此定义了FormData类型.FormData为序列化表单以及创建与表单格式相同的数据提供了便利. ...