一、 引入同步: 有一个很经典的案例,即银行取款问题。我们可以先看下银行取款的基本流程:

1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。

2)用户输入取款金额。

3)系统判断账户金额是否大于取款金额。

4)如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。

假设,此时有两个人,同时使用同一个账户并发取钱,我们模拟下取款流程:

public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// 此处省略了accountNo和balance两个Field的setter和getter方法 // accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
} // balance的setter和getter方法
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
} // 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

接下来,提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。

public class DrawThread extends Thread {
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount; public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
} // 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run() {
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount) {
// 吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
} else {
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}

输出:

---------- java ----------

乙取钱成功!吐出钞票:800.0

甲取钱成功!吐出钞票:800.0

余额为: 200.0

余额为: -600.0





输出完成 (耗时 0 秒) - 正常终止

之所以会出现这样的错误,是因为线程调度具有不确定性,在账户余额只有1000时,取出了1600,而且账户余额出现了负值。

要解决该问题,java引入了同步监视器,在线程开始执行同步代码块之前,必须先获得同步监视器的锁定。

同步监视器的目的: 阻止多个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

接下来,我们使用同步监视器锁定线程的执行体run()方法:

public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 使用account作为同步监视器,任何线程进入下面同步代码块之前,
// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
// 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
synchronized (account)
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
// 吐出钞票
System.out.println(getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}

除了使用同步代码块之外,我们还可以使用同步方法。同步方法无须显示指定同步监视器,同步方法的同步监视器是this,也就是对象本身。

通过通过方法可以非常方便的实现线程安全的类:

·该类的对象可以被多个线程安全的访问。

·每个线程调用该对象的任意方法之后都将得到正确的结果。

·每个线程调用该对象的任意方法之后,该对象的状态依然保持合理状态。

public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
} // accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
} // 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
} // 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

上面程序中增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰,该方法变为同步方法,同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程Account对象锁定,然后进入draw()方法执行取钱操作。

接下来,我们看下并发的线程类该如何写:

public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 直接调用account对象的draw方法来执行取钱
// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
account.draw(drawAmount);
}
}

线程类无须事前取钱操作,而是直接调用account的draw()方法来执行取钱操作。由于已经使用了synchronized关键字修饰了draw()方法,同步方法的同步监视器就是this,而this总代表调用该方法的对象——在上面的示例中,调用draw()方法的对象时account,因此多个线程并发修改一份account之前,必须先对account对象加锁。

二、 同步锁(Lock)

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构。Lock是控制多个线程对共享资源进行访问的工具。

某些锁可能允许对共享资源的并发访问,比如ReadWriteLock(读写锁)。比较常用的Lock有ReentrantLock(可重入锁),使用它可以显式的加锁、释放锁。

public class Account
{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
} // accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
} // 提供一个线程安全draw()方法来完成取钱操作
public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 修改完成,释放锁
lock.unlock();
}
} // 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来之宗lock()方法的嵌入调用,线程在每次调用lock()枷锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

三、死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施处理死锁情况,所以多线程编程时应该采取避免死锁出现。

死锁的举例:

class A
{
public synchronized void foo( B b )
{
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法" ); //①
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); //③
b.last();
}
public synchronized void last()
{
System.out.println("进入了A类的last方法内部");
}
}
class B
{
public synchronized void bar( A a )
{
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法" ); //②
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); //④
a.last();
}
public synchronized void last()
{
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}

深入Java线程管理(三):线程同步的更多相关文章

  1. Java多线程学习(三)---线程的生命周期

    线程生命周期 摘要: 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态.在线程的生命周期中,它要经过新建(New).就绪(Runnable).运行(Running).阻塞 ...

  2. Java并发之线程管理(线程基础知识)

    因为书中涵盖的知识点比较全,所以就以书中的目录来学习和记录.当然,学习书中知识的时候自己的思考和实践是最重要的.说到线程,脑子里大概知道是个什么东西,但很多东西都还是懵懵懂懂,这是最可怕的.所以想着细 ...

  3. C# 线程--第三线程池

    概述 线程池有那些优点: 1.在多线程中线程池可以减少我们创建线程,并合理的复用线程池中的线程.因为在线程池中有线程的线程处于等待分配任务状态. 2.不必管理和维护生存周期短暂的线程,不用在创建时为其 ...

  4. ACE线程管理机制-线程的创建与管理

    转载于:http://www.cnblogs.com/TianFang/archive/2006/12/04/581369.html 有过在不同的操作系统下用c++进行过多线程编程的朋友对那些线程处理 ...

  5. C11线程管理:线程创建

    1.线程的创建 C11创建线程非常简单,只需要提供线程函数就行,标准库提供线程库,并可以指定线程函数的参数. #include <iostream> #include <thread ...

  6. linux线程篇 (三) 线程的同步

    1 互斥量 pthreat_mutex_t mymutex; //1. 创建 初始化 int pthread_mutex_init(pthread_mutex_t *mutex, const pthr ...

  7. 温故知新-java多线程&深入理解线程池

    文章目录 摘要 java中的线程 java中的线程池 线程池技术 线程池的实现原理 简述 ThreadPoolExecutor是如何运行的? 线程池运行的状态和线程数量 任务执行机制 队列缓存 Wor ...

  8. Java 线程第三版 第一章Thread导论、 第二章Thread的创建与管理读书笔记

    第一章 Thread导论 为何要用Thread ? 非堵塞I/O      I/O多路技术      轮询(polling)      信号 警告(Alarm)和定时器(Timer) 独立的任务(Ta ...

  9. Java之线程安全中的三种同步方式

    一个程序在运行起来时,会转换为进程,通常含有多个线程. 通常情况下,一个进程中的比较耗时的操作(如长循环.文件上传下载.网络资源获取等),往往会采用多线程来解决. 比如,现实生活中,银行取钱问题.火车 ...

  10. java多线程二之线程同步的三种方法

          java多线程的难点是在:处理多个线程同步与并发运行时线程间的通信问题.java在处理线程同步时,常用方法有: 1.synchronized关键字. 2.Lock显示加锁. 3.信号量Se ...

随机推荐

  1. 使用Gradle构建项目

    使用gradle构建项目之前,咱们先聊聊maven,使用maven构建项目和配置主要 接下来正式使用gradle来构建项目的和步骤: 1.下载源码 2.下载gradle 1.下载地址:https:// ...

  2. 实现一个vue的图片预览插件

    vue-image-swipe 基于photoswipe实现的vue图片预览组件 安装 1 第一步 npm install vue-image-swipe -D 2 第二步 vue 入口文件引入 im ...

  3. Leetcode874.Walking Robot Simulation模拟行走的机器人

    机器人在一个无限大小的网格上行走,从点 (0, 0) 处开始出发,面向北方.该机器人可以接收以下三种类型的命令: -2:向左转 90 度 -1:向右转 90 度 1 <= x <= 9:向 ...

  4. vs2015卸载、vs2008安装Visual Assist x

    卸载2015 参考博文 1. 手动卸载VS2015的主要部分: win10系统: 控制面板---程序和功能--Microsoft Visual Studio 2015---更改卸载 2. 下载工具并解 ...

  5. Linux安装JDK和Tomcat

    步骤如下: 1)在/root用户下建立jdk和tomcat两个文件夹并上传jdk-7u80-linux-x64.rpm和apache-tomcat-7.0.82.zip   2)安装jdk #  rp ...

  6. Mysql主从安装配置

    Mysql主从安装配置   环境: 主从服务器上的MySQL数据库版本同为5.1.34 主机IP:192.168.0.1 从机IP:192.168.0.2  一. MySQL主服务器配置 1.编辑配置 ...

  7. poj2987 最大权闭合图

    基础题. 最小割后,与汇点相连的点都不要,然后从源点出发dfs一遍有多少相连的点即可. #include<stdio.h> #include<string.h> #includ ...

  8. Python2.7下,调用subprocess启动子进程,读取子进程标准输出若干问题

    1:如果调用的子进程也是一个python脚本,则subprocess.Popen中的bufsize=1无效果.也就是说,即使设置了bufsize=1表示进行行缓冲,子进程如果不显示调用sys.stdo ...

  9. (一)流--IO框架

    介绍: IO(Input/Output)是计算机输入/输出的接口.java的核心库java.io提供了全方面的IO接口,包括:文件系统的操作,文件读写,标准设备输出等等 File  文件和目录类 In ...

  10. 人生苦短,LET'S GO! GO语言目录

    1.Golang开山篇,GO就是NB! 1-1.go开发工具安装 2.go-人生第一个go程序和基本语法 3.go-流程控制 4.go-函数 5.go-流程控制 6.go-复合类型 7.go-面向对象 ...