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. 读书笔记—CLR via C#字符串及文本

    前言 这本书这几年零零散散读过两三遍了,作为经典书籍,应该重复读反复读,既然我现在开始写博了,我也准备把以前觉得经典的好书重读细读一遍,并且将笔记整理到博客中,好记性不如烂笔头,同时也在写的过程中也可 ...

  2. VS 文件自动定位功能

    在Visual Studio 中,当你在所有打开的文件中进行切换时,在Solution Explorer中也会自定定位到这个文件的目录下面,这个功能用来查找当前文件是非常有用.在Tools->O ...

  3. .net图片自动裁剪白边函数案例

    1.项目要求上传白底的图片要进行裁剪白边,于是同事谢了个函数感觉很好用. 2. #region 剪切白边 /// <summary> /// 剪切白边 /// </summary&g ...

  4. javascript call和apply

    每个函数都包含两个非继承而来的方法:call和apply. 我们可以通过这两个方法来间接调用函数.可以这样: f.call(o); f.apply(o); //o对象间接调用了f函数 这与下面的功能相 ...

  5. C#集合基础与运用

    C#集合基础与运用   C#集合基础与运用 1. 集合接口与集合类型............................................... 1 (1) 集合的命名空间..... ...

  6. elasticsearch文档-modules

    elasticsearch文档-modules modules 模块 cluster 原文 基本概念 cluster: 集群,一个集群通常由很多节点(node)组成 node: 节点,比如集群中的每台 ...

  7. 上传文件大小限制,webconfig和IIS配置大文件上传

    IIS6下上传大文件没有问题,但是迁移到IIS7下面,上传大文件时,出现HTTP 404错误. IIS配置上传大小,webconfig <!-- 配置允许上传大小 --><httpR ...

  8. 多模块分布式系统的简单服务访问 - OSGI原形(.NET)

    多模块分布式系统的简单服务访问 - OSGI原形(.NET) 先描述一下本篇描述的适用场景(3台server, 各个模块分布在各个Server上,分布式模块互相依赖.交互的场景): 多个OSIG引擎交 ...

  9. tmux tutorial

    This is a great tutorial about tmux quick start: http://www.youtube.com/watch?v=wKEGA8oEWXw&nore ...

  10. Python 3语法小记(五)字符串

    Python 3 的源码的默认编码方式为 UTF-8 在Python 3,所有的字符串都是使用Unicode编码的字符序列. utf-8 是一种将字符编码成字节序列的方式.字节即字节,并非字符.字符在 ...