Java并发编程中构建自定义同步工具

当Java类库没有提供适合的同步工具时,就需要构建自定义同步工具。

可阻塞状态依赖操作的结构


复制代码 代码如下:

acquir lock on object state;//请求获取锁
while(precondition does not hold){//没有满足前提条件
   release lock;//先释放锁
   wait until precondition might hold;//等待满足前提条件
   optionlly fail if interrupted or timeout expires;//因为中断或者超时执行失败
   reacquire lock;//重新尝试获取锁
}
perform action//执行
   release lock;//释放锁

有界缓存实现基类示例

复制代码 代码如下:

public class BaseBoundBuffer<V> {
private final V[] buf;
private int tail;
private int head;
private int count;
@SuppressWarnings("unchecked")
public BaseBoundBuffer(int capacity) {
buf = (V[]) new Object[capacity];
}
public synchronized void doPut(V v) {
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
count++;
}
public synchronized V doTake() {
V v = buf[head];

if (++head == buf.length)
head = 0;
count--;
return v;
}
public final synchronized boolean isFull() {
return count == buf.length;
}
public final synchronized boolean isEmpty() {
return count == 0;
}
}

阻塞实现方式一:抛异常给调用者


复制代码 代码如下:

public synchronized void put1(V v)  throws Exception{
if(isFull())
throw new Exception("full error");
doPut(v);
}

分析:异常应该应用于发生异常情况中,在这里抛异常不合适;需要调用者是处理前提条件失败的情况,并没有解决根本问题。
阻塞实现方式二:通过轮询和休眠


复制代码 代码如下:

public void put2(V v) throws InterruptedException {
while (true) {//轮询
synchronized (this) {
if (!isFull()) {
doPut(v);
return;    
}
}
Thread.sleep(SLEEP_TIME);//休眠
}
}

分析:很难权衡休眠时间SLEEP_TIME设置。如果设置过小,CPU可能会轮询多次,消耗CPU资源也越高;如果设置过大,响应性就越低。

阻塞实现方式三:条件队列

条件队列中的元素是一个个等待相关条件的线程。每个Java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且Object中的wait、notify、notifyAll方法就构成了内部条件队列的API。Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其它线程能获得这个锁并修改对象的状态。Object.notify和Object.notifyAll能唤醒正在等待线程,从条件队列中选取一个线程唤醒并尝试重新获取锁。

复制代码 代码如下:

public synchronized void put3(V v) throws InterruptedException {
while(isFull())
wait();
doput(v);
notifyAll();
}

分析:获得较好响应,简单易用。

使用条件队列​
1.条件谓词

1).定义:条件谓词是使某个操作成为状态依赖操作的前提条件。条件谓词是由类中各个状态变量构成的表达式。例如,对于put方法的条件谓词就是“缓存不为空”。
2).关系:在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而每个状态变量必须由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(及调用wait和notify等方法所在的对象)必须是同一个对象。
3).约束:每次调用wait都会隐式地和特定的条件谓词相关联,当调用特定条件谓词时,调用者必须已经持有与条件队列相关的锁,这个锁必须还保护这组成条件谓词的状态变量

2.条件队列使用规则

1).通常都有一个条件谓词
2).永远在调用wait之前测试条件谓词,并且在wait中返回后再次测试;
3).永远在循环中调用wait;
4).确保构成条件谓词的状态变量被锁保护,而这个锁必须与这个条件队列相关联;
5).当调用wait、notify和notifyAll时,要持有与条件队列相关联的锁;
6).在检查条件谓词之后,开始执行被保护的逻辑之前,不要释放锁;

3.通知

尽量使用notifyAll,而不是nofify.因为nofify会随机唤醒一个线程从休眠状态变为Blocked状态(Blocked状态是种线程一直处于尝试获取锁的状态,即一旦发现锁可用,马上持有锁),而notifyAll会唤醒条件队列中所有的线程从休眠状态变为Blocked状态.考虑这么种情况,假如线程A因为条件谓词Pa进入休眠状态,线程B因为条件谓词Pb进入休眠状态.这时Pb为真,线程C执行单一的notify.如果JVM随机选择了线程A进行唤醒,那么线程A检查条件谓词Pa不为真后又进入了休眠状态.从这以后再也没有其它线程能被唤醒,程序会一直处于休眠状态.如果使用notifyAll就不一样了,JVM会唤醒条件队列中所有等待线程从休眠状态变为Blocked状态,即使随机选出一个线程一因为条件谓词不为真进入休眠状态,其它线程也会去竞争锁从而继续执行下去.

4.状态依赖方法的标准形式

复制代码 代码如下:

void stateDependentMethod throwsInterruptedException{
synchronized(lock){
while(!conditionPredicate))
lock.wait();
}
//dosomething();
....

notifyAll();
}

显示Condition对象

显示的Condition对象是一种更灵活的选择,提供了更丰富的功能:在每个锁上可以存在多个等待,条件等待可以是中断的获不可中断的,基于时限的等待,以及公平的或非公平的队列操作。一个Condition可以和一个Lock关联起来,就像一个条件队列和一个内置锁关联起来一样。要创建一个Condition,可以在相关联的Lock上调用Lock.newCondition方法。以下用显示条件变量重新实现有界缓存

复制代码 代码如下:

public class ConditionBoundedBuffer<V> {
 private final V[] buf;
 private int tail;
 private int head;
 private int count;
 private Lock lock = new ReentrantLock();
 private Condition notFullCondition = lock.newCondition();
 private Condition notEmptyCondition = lock.newCondition();
 @SuppressWarnings("unchecked")
 public ConditionBoundedBuffer(int capacity) {
  buf = (V[]) new Object[capacity];
 }

public void doPut(V v) throws InterruptedException {
  try {
   lock.lock();
   while (count == buf.length)
    notFullCondition.await();
   buf[tail] = v;
   if (++tail == buf.length)
    tail = 0;
   count++;
   notEmptyCondition.signal();
  } finally {
   lock.unlock();
  }

}

public V doTake() throws InterruptedException {
  try {
   lock.lock();
   while (count == 0)
    notEmptyCondition.await();
   V v = buf[head];
   buf[head] = null;
   if (++head == buf.length)
    head = 0;
   count--;
   notFullCondition.signal();
   return v;
  } finally {
   lock.unlock();
  }
 }
}

时间: 2015-04-13

实例讲解Java并发编程之闭锁

闭锁相当于一扇门,在闭锁到达结束状态之前,这扇门一直是关闭着的,没有任何线程可以通过,当到达结束状态时,这扇门才会打开并容许所有线程通过.它可以使一个或多个线程等待一组事件发生.闭锁状态包括一个计数器,初始化为一个正式,正数表示需要等待的事件数量.countDown方法递减计数器,表示一个事件已经发生,而await方法等待计数器到达0,表示等待的事件已经发生.CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行. 场景应用: 10个运动员准备赛

JAVA线程同步实例教程

线程是Java程序设计里非常重要的概念,本文就以实例形式对此加以详细解读.具体分析如下: 首先,线程加锁有什么用处呢?举个例子:比如你现在有30000块大洋在银行存着,现在你到银行取钱,当你输入密码完成后,已经输入取款金额,比如你输入的是20000,就是在银行给你拿钱这个时刻,你老婆也去银行取这笔钱,你老婆同样取20000,因为此时你的账上仍然是30000,所以银行同样的操作在你老婆那端又进行了一遍,这样当你们两个完成各自操作后,银行记录的你账上还应该有10000块存款,这样是不是很爽.解决这个

Java并发程序入门介绍

今天看了看Java并发程序,写一写入门程序,并设置了线程的优先级. class Elem implements Runnable{ public static int id = 0; private int cutDown = 5; private int priority; public void setPriority(int priority){ this.priority = priority; } public int getPriority(){ return this.priori

Java中同步与并发用法分析

本文较为详细的分析了Java中同步与并发的用法.分享给大家供大家参考.具体分析如下: 1.同步容器类包括两部分:vector和hashtable 另一类是同步包装类,由Collections.synchronizedXXX创建.同步容器对容器的所有状态进行串行访问,从而实现线程安全. 它们存在如下问题: a) 对于符合操作,需要额外的锁保护.比如迭代,缺少则添加等条件运算. b) toString,hashCode,equals都会间接的调用迭代,都需要注意并发.   2.java5.0中的并发

实例讲解Java并发编程之变量

编写线程安全需要关心的: 1.共享的变量 2.可变的变量 共享意味着多个线程可以同时访问,可变意味着其值在生命周期可以改变. 例如以下count 变量: 复制代码 代码如下: //线程不安全的类 public class UnsafeCount {     private int count = 0;    //该变量是共享的     public void increase() {    //这里没有同步机制,多个线程可以同时访问         count++;    //该变量是可变的  

Java并发编程之原子变量与非阻塞同步机制

1.非阻塞算法 非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 -- 例如比较和交换.非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御.使用底层的原子化机器指令取代锁,比如比较并交换(CAS,compare-and-swap). 2.悲观技术 独占锁是一种悲观的技术.它假设最坏的情况发生(如果不加锁,其它线程会破坏对象状态),即使没有发生最坏的情况,仍然用锁保护对象状态.

Java中的5种同步辅助类介绍

当你使用synchronized关键字的时候,是通过互斥器来保障线程安全以及对共享资源的同步访问.线程间也经常需要更进一步的协调执行,来完成复杂的并发任务,比如wait/notify模式就是一种在多线程环境下的协调执行机制. 通过API来获取和释放锁(使用互斥器)或者调用wait/notify等方法都是底层调用的方式.进一步来说,有必要为线程同步创建更高层次的抽象.通常用到的同步辅助类,就是对2个或多个线程间的同步活动机制做进一步封装,其内部原理是通过使用现有的底层API来实现复杂的线程间的协调

Java中的两种for循环介绍

复制代码 代码如下: package com.zxd.test; import java.util.List; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import com.zxd.bea

Java中CountDownLatch进行多线程同步详解及实例代码

Java中CountDownLatch进行多线程同步详解 CountDownLatch介绍 在前面的Java学习笔记中,总结了Java中进行多线程同步的几个方法: 1.synchronized关键字进行同步. 2.Lock锁接口及其实现类ReentrantLock.ReadWriteLock锁实现同步. 3.信号量Semaphore实现同步. 其中,synchronized关键字和Lock锁解决的是多个线程对同一资源的并发访问问题.信号量Semaphore解决的是多副本资源的共享访问问题. 今天

详解java中的6种单例写法及优缺点

在java中,单例有很多种写法,面试时,手写代码环节,除了写算法题,有时候也会让手写单例模式,这里记录一下单例的几种写法和优缺点. 1.初级写法 2.加锁 3.饿汉式 4.懒汉式 5.双锁检验 6.内部类 1.初级写法 package com.java4all.test6; /** * Author: yunqing * Date: 2018/8/13 * Description:单例模式 -- 初级 */ public class Singleton { private static Sing

Java中的静态绑定和动态绑定详细介绍

一个Java程序的执行要经过编译和执行(解释)这两个步骤,同时Java又是面向对象的编程语言.当子类和父类存在同一个方法,子类重写了父类的方法,程序在运行时调用方法是调用父类的方法还是子类的重写方法呢,这应该是我们在初学Java时遇到的问题.这里首先我们将确定这种调用何种方法实现或者变量的操作叫做绑定. 在Java中存在两种绑定方式,一种为静态绑定,又称作早期绑定.另一种就是动态绑定,亦称为后期绑定. 区别对比 1.静态绑定发生在编译时期,动态绑定发生在运行时 2.使用private或stati

浅谈Java中的n种随机数产生办法

我们从书本上学到什么? 最明显的,也是直观的方式,在Java中生成随机数只要简单的调用: java.lang.Math.random() 在所有其他语言中,生成随机数就像是使用Math工具类,如abs, pow, floor, sqrt和其他数学函数.大多数人通过书籍.教程和课程来了解这个类.一个简单的例子:从0.0到1.0之间可以生成一个双精度浮点数.那么通过上面的信息,开发人员要产生0.0和10.0之间的双精度浮点数会这样来写: Math.random() * 10 而产生0和10之间的整数

Java中的main函数的详细介绍

Java中的main函数的详细介绍 JAVA中的主函数是我们再熟悉不过的了,相信每个学习过JAVA语言的人都能够熟练地写出这个程序的入口函数,但对于主函数为什么这么写,其中的每个关键字分别是什么意思,可能就不是所有人都能轻松地答出来的了.我也是在学习中碰到了这个问题,通过在网上搜索资料,并加上自己的实践终于有了一点心得,不敢保留,写出来与大家分享. 主函数的一般写法如下: public static void main(String[] args){-} 下面分别解释这些关键字的作用: (1)p

详解java中的四种代码块

在java中用{}括起来的称为代码块,代码块可分为以下四种: 一.简介 1.普通代码块: 类中方法的方法体 2.构造代码块: 构造块会在创建对象时被调用,每次创建时都会被调用,优先于类构造函数执行. 3.静态代码块: 用static{}包裹起来的代码片段,只会执行一次.静态代码块优先于构造块执行. 4.同步代码块: 使用synchronized(){}包裹起来的代码块,在多线程环境下,对共享数据的读写操作是需要互斥进行的,否则会导致数据的不一致性.同步代码块需要写在方法中. 二.静态代码块和构造

详解Java 中的三种代理模式

代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法. 举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理

Java中的3种输入方式实现解析

这篇文章主要介绍了Java中的3种输入方式实现解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.从键盘读取char类型数据 char ch = (char)System.in.read(); System.in 提供的 read() 方法每次只能读取一个字节的数据,所以用的频率比较低. 2.BufferedReader 实现从键盘读取String类型数据 使用BufferedReader 对象的 readLine() 方法必须处理 jav