Java 并发 线程同步

@author ixenos

同步


1.异步线程本身包含了执行时需要的数据和方法,不需要外部提供的资源和方法,在执行时也不关心与其并发执行的其他线程的状态和行为

2.然而,大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取,这将产生同步问题(可见性和同步性的丢失)

  比如两个线程同时执行指令account[to] += amount,这不是原子操作,可能被处理如下:

  a)将account[to]加载到寄存器

  b)增加amount

  c)将结果写回account[to]

  还可以通过javap -c -v Bank对Bank.class文件进行反编译,将得到以下字节码:

   aload_0

   getfield    #2; //Field accounts:[D

   iload_2

   dup2

   daload

   dload_3

   dadd

   dastore

  执行他们的线程可以在任何一条指令点上被中断,多线程执行就会产生同步问题

对象锁


0.在任何时刻,一个对象的对象锁至多只能被一个线程拥有

1.两种机制防止代码块受并发访问干扰

  a)synchronized关键字(synchronized关键字自动提供了一个锁和相关的条件)、ReentrantLock类

  b)java.util.concurrent 框架提供的独立的类

2.可重入锁

  1)Java运行系统允许一个线程重复获得已持有的对象锁,锁的可重入性可以防止一个线程的死锁

   2)synchronized和ReentrantLock都实现了可重入锁(ReentrantLock是Lock接口的实现类,但Lock接口本身不定义可重入锁!)

  3)锁保持一个持有计数(hold count)来跟踪锁的重入,当持有计数变为0的时候,线程才释放锁

    Q:那为什么要设定成对同一线程可重入锁,而不放锁呢?

    A:因为可能要保护某一片需若干个操作来更新的代码块,要确保这些操作完成后,另一个线程才能使用相同的对象

ReentrantLock保护代码块的基本结构如下:

 myLock.lock(); //myLock是一个ReentrantLock对象
try{
critical condition
}finally{
myLock.unlock(); //放在finally里即使抛出异常都释放锁
} ReentrantLock可重入锁情况:
 public class Bank{
private Lock banklock = new ReentrantLock(); //ReentrantLock实现了Lock接口
...
public void transfer(int from, int to, int amount){
bankLock.lock();
try{
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %10.2f%n", getTotalBalance()); //getTotalBalance()也是同步方法时,在同一线程内部锁可以重入
}finally{
bankLock.unlock();
}
}
}

  注意: 1)把解锁操作放在finally子句中是至关重要的,因为如果临界区的代码抛出异常,锁必须释放,否则其他线程将永远阻塞!

       2)注意不能使用try-with-resource语句,首先ReentrantLock类并没有实现Closeable接口,其次是因为解锁方法名不是close,即使改成close也不能工作,因为try-with-resource希望声明的是一个新变量,而显然我们使用锁时,是为了让多个线程交替持有锁。

synchronized保护代码块的基本结构如下:

 synchronized{
critical condition
}
//synchronized过了临界区自动释放锁 --------------------------------------------- public class Box{
private int value;
public synchronized void put(int value){ //方法锁定对象,该对象其他同步方法也被锁定!
this.value=value;
}
public synchronized int get(){
return this.value;
}
}

  synchronized可重入锁

 public class Reentrant{
public synchronized void a(){
b();
System.out.println("method a() is called");
}
public synchronized void b(){
System.out.println("method b() is called");
}
} ---------
输出:
method b() is called //说明该线程可以再次取得该对象锁(可重入锁)
method a() is called

接口 java.util.concurrent.locks.Lock 定义了:

  void lock();

  void unlock;

可重入锁类 java.util.concurrent.locks.ReentrantLock 定义了:

  ReentrantLock(); //构建一个可以被用来保护临界区的可重入锁

  ReentrantLock(boolean fair);  //构建一个带有公平策略的锁,优先放锁给等待时间最长的线程,公平锁因此降低程序性能

    注意:公平锁也无法确保线程调度器是公平的,调度器想忽略谁就忽略谁

条件对象


1.使用场景:线程进入临界区,却发现只有在某一条件满足之后它才能执行,要使用一个条件对象来管理那些已经获得一个锁却不能做有用工作的线程

2.代码示例:

  还是银行的存取问题

 if(bank.getBalance(from) >= amount){ //判断余额是否足够
//当前线程完全可能完成if条件测试后,且在调用transfer前就被中断了!
bank.transfer(from, to, amount);
}

  为此我们把余额判断和转账锁定成原子操作

 public void transfer(int from, int to, int amount){
bankLock.lock();
try{
while(account[from] < amount){ //检查余额是否足够取出
//wait
...
}
//transfer funds
...
}finally{
bankLock.unlock();
}
}

  但是问题来了:当账户中没有足够的金额时,就只能等待其它线程向账户注入资金,但是这线程刚取得了bankLock的排他性访问,因此别的线程没有进行存款操作的机会,此时就需要使用条件对象

  • 一个锁对象可以有一个或多个相关的条件对象
  • 使用锁对象的newCondition方法来生成一个Condition对象,一般使其命名为它所表达条件的名字
 class Bank{
private Condition sufficientFunds;
...
public Bank(){
...
sufficientFunds = bankLock.newCondition();
}
}

   1) 此时如果对象余额不足,就可调用sufficientFunds.await();使当前线程被阻塞,并放弃锁,等待其他线程完成任务并调用signalAll激活该线程(无法自我激活)

  2) sufficientFunds.signalAll(); 这一调用将重新激活因为这一条件而等待的所有线程为Runnable状态(这是抽象的条件,具体的条件是我们编写的配合await方法的判断语句)

  3) 如果没有人来激活,将导致死锁;如果所有其他线程都阻塞了,最后一个线程也先执行await,那么就全阻塞了,无药可救,程序就挂起了

  4) 另一个方法signal是随机接触某个线程的阻塞状态,这更高效也更危险,因为如果接触后还是不能运行,那么它将再次阻塞,没有其他人再执行signal时,下场就跟3)一样

综合示例:

 import java.util.concurrent.locks.*;

 /**
* A bank with a number of bank accounts that uses locks for serializing access.
* @version 1.30 2004-08-01
* @author Cay Horstmann
*/
public class Bank
{
private final double[] accounts;
private Lock bankLock; //使用接口类型,符合多态
private Condition sufficientFunds; /**
* Constructs the bank.
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
for (int i = 0; i < accounts.length; i++)
accounts[i] = initialBalance;
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
} /**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock(); //带有ReentrantLock
try
{
while (accounts[from] < amount)
sufficientFunds.await(); //条件对象阻塞放锁,让其他线程注入资金
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll(); //激活
}
finally
{
bankLock.unlock();
}
} /**
* Gets the sum of all account balances.
* @return the total balance
*/
public double getTotalBalance()
{
bankLock.lock(); //带有ReentrantLock
try
{
double sum = 0; for (double a : accounts)
sum += a; return sum;
}
finally
{
bankLock.unlock();
}
} /**
* Gets the number of accounts in the bank.
* @return the number of accounts
*/
public int size()
{
return accounts.length;
}
}

总结:

锁和条件的关键之处:

1.锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码

2.锁可以用来管理试图进入被保护代码段的线程

3.锁可以拥有一个或多个相关的条件对象

4.每个条件对象管理那些已经进入被保护的代码段但不能运行的线程

synchronized关键字


  • synchronized关键字利用了对象的内部锁内部条件来实现同步锁定和释放
  • synchronized两种形式:
    • 同步方法(synchronized method)
    • 同步块(synchronized block)
      • 以下示例以同步方法为主,本节最后再谈及同步块

  Lock和Condition接口提供了高度的锁定控制,但通常我们不需要

  • 从1.0版开始,Java中的对象都有一个内部锁(注意不是ReentrantLock),如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法,要调用该方法,线程必须获得对象的内部锁

所以

public synchronized void method(){
...
}

等价于

public void method(){
this.intrinsticLock.lock();
try{
...
}finally{
this.intrinsticLock.unlock();
}
}
  • 内部对象锁只持有一个相关内部条件,我们使用wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态,所以调用wait/notifyAll相当于
intrinsticLock.await();
intrinsticLock.signalAll();

  作用是相同的,但wait,notifyAll,notify是Object类的final方法,为了避免冲突,Condition命名时就是await,signalAll,signal,其实前者的名字更恰当!

例如

 class Bank{
private double[] accounts;
//改用synchronized关键字,调用内部锁
public synchronized void transfer(int from, int to, int amount){
while(account[from] < amount){
wait();
}
accounts[from] -= amount;
accounts[to] += amount;
notifyAll();
} //synchronized标注的方法执行完毕,内部锁自动释放 public synchronized double getTotalBalance(){...}
} /*
使用synchronized时必须要了解,
每一个对象都有一个内部锁,并且该锁有一个内部条件。
synchronized只是个关键字标记,实际上
由内部锁来管理那些试图进入synchronized方法的线程,
由内部条件来管理那些调用wait的线程 */
  • 静态方法声明为synchronized也是合法的,这样该方法将获得相关类对象内部锁(不要忘了类对象!!!)
  • 内部锁和内部条件的局限性:

    • 不能中断一个正在试图获得锁的线程
    • 试图获得锁时不能设定超时
    • 每个锁仅有单一条件
  • 那么该用外部锁和条件,还是内部锁和条件呢?

    • 首选java.util.concurrent包中的相关机制(阻塞队列等),会为你处理所有的加锁
    • 如果synchronized很适合,就使用它
    • 需要Lock/Condition的独有特性时,才使用它
      • 即concurrent > synchronized > Lock/Condition
  • 同步块(synchronized block)

  简单示例:

 synchronized(obj){     // obj作为该同步块的锁对象,只有持有该对象的锁才能进入代码块
critical section
}

  完整示例:

 public class Bank{
private double[] accounts;
private Object lock = new Object(); //专门取其内部锁辅助我们做同步操作
...
public void transfer(int from, int to, int amount){
synchronized(lock){ //取其对象锁,进入代码块
accounts[from] -= amount;
accounts[to] += amount;
} //释放对象锁
}
}

   客户端锁定:使用一个实际对象的锁来实现额外的原子操作,称为客户端锁定(clientside locking)

 /*
显然该方法不是原子操作,线程并发访问时将存在同步问题
*/
public void transfer(Vector<Double> accounts, int from, int to, int amount){
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
...
} -------------------------- /*
使用同步块使该方法关键操作变成原子操作
*/
public void transfer(Vector<Double> accounts, int from, int to, int amount){
synchronized(accounts){ //选定账户对象的锁来锁定,一石二鸟
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
...
}
}

  这个方法可以工作,但它却要完全依赖与这样一个事实,即accounts对象存在!这样具有耦合性,代码太脆弱

  因此,通常不推荐使用客户端锁定,要用同步块就直接新建一个Object对象来锁定就ok了~

监视器的概念


0.前言:锁和条件是线程同步的强大工具,但不是面向对象的,需要手动设置,于是就有了面向对象的监视器概念(monitor),使程序员不需要考虑如何加锁就可以保证多线程的安全性;

1.监视器标准定义(1970s提出的概念):

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

  2)每个监视器类的对象有一个相关的锁(对应Java:内部锁);

  3)使用该锁对所有的方法进行加锁(对应Java:所有方法是synchronized的),调用时自动获得对象锁,返回时自动释放该锁;

  4)该锁可以有任意多个相关条件

2.Java设计者以不精确的方式采用了监视器概念:

  1)Java中每一个对象都有一个内部锁和内部条件

  2)如果一个方法调用synchronized声明,那么该方法就如同一个监视器方法(自动加锁放锁)

  3)通过wait/notifyAll/notify来访问条件变量

3.然而Java对象以下三个方面使其背离了监视器的定义:

  1)域不要求是private的;

  2)方法不要求必须是synchronized的;

  3)内部锁对客户是可用的;

Java 并发 线程同步的更多相关文章

  1. Java并发——线程同步Volatile与Synchronized详解

    0. 前言 转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52370068 面试时很可能遇到这样一个问题:使用volatile修饰in ...

  2. Java 并发 线程的生命周期

    Java 并发 线程的生命周期 @author ixenos 线程的生命周期 线程状态: a)     New 新建 b)     Runnable 可运行 c)     Running 运行 (调用 ...

  3. java中线程同步的理解(非常通俗易懂)

    转载至:https://blog.csdn.net/u012179540/article/details/40685207 Java中线程同步的理解 我们可以在计算机上运行各种计算机软件程序.每一个运 ...

  4. Java 并发 线程的优先级

    Java 并发 线程的优先级 @author ixenos 低优先级线程的执行时刻 1.在任意时刻,当有多个线程处于可运行状态时,运行系统总是挑选一个优先级最高的线程执行,只有当线程停止.退出或者由于 ...

  5. Java 并发 线程属性

    Java 并发 线程属性 @author ixenos 线程优先级 1.每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程 2.默认情况下,一个线程继承它的父线程的优先级 当在一个运行的线 ...

  6. Java中线程同步的理解 - 其实应该叫做Java线程排队

    Java中线程同步的理解 我们可以在计算机上运行各种计算机软件程序.每一个运行的程序可能包括多个独立运行的线程(Thread). 线程(Thread)是一份独立运行的程序,有自己专用的运行栈.线程有可 ...

  7. Java并发——线程安全、线程同步、线程通信

    线程安全 进程间"共享"对象 多个“写”线程同时访问对象. 例:Timer实例的num成员,即add()方法是用的次数.即Timer实例是资源对象. class TestSync ...

  8. Java多线程与并发——线程同步

    1.多线程共享数据 在多线程的操作中,多个线程有可能同时处理同一个资源,这就是多线程中的共享数据. 2.线程同步 解决数据共享问题,必须使用同步,所谓同步就是指多个线程在同一时间段内只能有一个线程执行 ...

  9. java并发:同步容器&并发容器

    第一节 同步容器.并发容器 1.简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同 ...

随机推荐

  1. ASP.NET开发,简化与封装

    ASP.NET开发,简化与封装 微软的ASP.NET的开发,就是面向对象的编程,当然前端也能体验至面向对象的话,使用Web控件也必须的. 任一控件,我们均可以在后端.aspx.cs或.aspx.vb程 ...

  2. JavaScript原生数组函数

    有趣的JavaScript原生数组函数 在JavaScript中,可以通过两种方式创建数组,构造函数和数组直接量, 其中后者为首选方法.数组对象继承自Object.prototype,对数组执行typ ...

  3. cocos2d(背景图片循环滚动)

    背景图片循环滚动 使用action 实现的: 主要有两个背景图片交替循环滚动:我选的两个背景图片的宽度都是1024的 ,所以定义了#define BGIMG_WIDTH 1024 代码如下: 在Hel ...

  4. requestScope含义

    requestScope表名一个http请求的整个生命周期,它只是一个定义而已,不是一个对象. ${requestScope.info}就等价于request.getAttribute("i ...

  5. js正则验证邮箱格式

    首先总结一下邮箱的格式,邮箱由@分隔,左侧为用户名,右侧为邮箱域名,用户名可以由字母.数字._.-以及.组成,但是必须是以字母或数字开头,邮箱的域名是由字母.数字.-和.组成的,但是必须以.加上字母的 ...

  6. javascript中字符串常用操作整理

    javascript中字符串常用操作整理 字符串的操作在js中非常频繁,也非常重要.以往看完书之后都能记得非常清楚,但稍微隔一段时间不用,便会忘得差不多,记性不好是硬伤啊...今天就对字符串的一些常用 ...

  7. SQL拼接方法

    smark Beetle可靠.高性能的.Net Socket Tcp通讯组件 另类SQL拼接方法 在编写SQL的时候经常需要对SQL进行拼接,拼接的方式就是直接String+处理,但这种情况有个不好的 ...

  8. VS2012下使用Moq进行单元测试

    单元测试虽然是个很老的东西了,但平时写代码一般都不写测试,因为VS调试完全可以满足了,所以一直也就没有用过,刚好在<Pro.ASP.NET.MVC.3.Framework>中看到了Moq这 ...

  9. How to use USB 3G dongle/stick Huawei E169/E620/E800 ( Chip used Qualcomm e1750) in Linux (China and world)

    Using this 3G module in Linux is so great. I want it. So I made it. The 3G dongle of Huawei E169/E62 ...

  10. WPF4.5新特性(MSDN的翻译读不太懂)

    WPF4.5新特性(MSDN的翻译读不太懂) 1. 新的Doctype声明 XHTML的声明太长了,我相信很少会有前端开发人员能手写出这个Doctype声明. <!DOCTYPE html PU ...