java的monitor对象

1. 什么是Monitor?

    Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:

  • 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
  • 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。

    Monitor对象可以被多线程安全地访问。关于“互斥”与“为什么要互斥”,我就不傻X兮兮解释了;而关于Monitor的singal机制,历史上曾经出现过两大门派,分别是Hoare派和Mesa派(上过海波老师OS课的SS同学应该对这个有印象),我还是用我的理解通俗地庸俗地解释一下:

  • Hoare派的singal机制江湖又称“Blocking condition variable”,特点是,当“发通知”的线程发出通知后,立即失去许可,并“亲手”交给等待者,等待者运行完毕后再将许可交还通知者。在这种机制里,可以等待者拿到许可后,谓词肯定为真——也就是说等待者不必再次检查条件成立与否,所以对条件的判断可以使用“if”,不必“while”
  • Mesa派的signal机制又称“Non-Blocking condition variable”, 与Hoare不同,通知者发出通知后,并不立即失去许可,而是把闻风前来等待者安排在ready queue里,等到schedule时有机会去拿到“许可”。这种机制里,等待者拿到许可后不能确定在这个时间差里是否有别的等待者进入过Monitor,因此不能保证谓词一定为真,所以对条件的判断必须使用“while”

    这两种方案可以说各有利弊,但Mesa派在后来的盟主争夺中渐渐占了上风,被大多数实现所采用,有人给这种signal另外起了个别名叫“notify”,想必你也知道,Java采取的就是这个机制。

2. Monitor与Java不得不说的故事

    子曰:“Java对象是天生的Monitor。”每一个Java对象都有成为Monitor的“潜质”。这是为什么?因为在Java的设计中,每一个对象自打娘胎里出来,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor锁”,或者“Intrinsic lock”。为了装逼起见,我们就叫它Intrinsic lock吧。有了这个锁的帮助,只要把类的所有对象方法都用synchronized关键字修饰,并且所有域都为私有(也就是只能通过方法访问对象状态),就是一个货真价实的Monitor了。比如,我们举一个大俗例吧:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
	private int balance;
	
	public Account(int balance) {
		this.balance = balance;
	}
	
	synchronized public boolean withdraw(int amount){
		if(balance<amount)
			return false;
		balance -= amount;
		return true;
	}
	
	synchronized public void deposit(int amount){
		balance +=amount;
	}
	
}

3. synchronized关键字

    上面我们已经看到synchronized的一种用法,用来修饰方法,表示进入该方法需要对Intrinsic lock加锁,离开时放锁。synchronized可以用在程序块中,显示说明对“哪个对象的Intrinsic lock加锁”,比如

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
synchronized public void deposit(int amount){
	balance +=amount;
}
// 等价于
public void deposit(int amount){
	synchronized(this){
		balance +=amount;
	}
}

 

    这时,你可能就要问了,你不是说任何对象都有intrinsic lock么?而synchronized关键字又可以显示指定去锁谁,那我们是不是可以这样做:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
	private int balance;
	private Object lock = new Object();
	
	public Account(int balance) {
		this.balance = balance;
	}
	
	public boolean withdraw(int amount){
		synchronized (lock) {
			if(balance<amount)
				return false;
			balance -= amount;
			return true;
		}	
	}
	
	public void deposit(int amount){
		synchronized (lock) {
			balance +=amount;
		}		
	}
}

 

    不用this的内部锁,而是用另外任意一个对象的内部锁来完成完全相同的任务?没错,完全可以。不过,需要注意的是,这时候,你实际上禁止了“客户代码加锁”的行为。前几天BBS上简哥有一贴提到的bug其实就是这个,这个时候使用这份代码的客户程序如果想当然地认为Account的同步是基于其内部锁的,并且傻X兮兮地写了类似下面的代码:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
	public static void main(String[] args) {
		Account account =new Account(1000);
		
		//some threads modifying account through Account’s methods...
		
		synchronized (account) {
			;//blabla
		}
	}

自认为后面的同步快对account加了锁,期间的操作不会被其余通过Account方法操作account对象的线程所干扰,那就太悲剧了。因为他们并不相干,锁住了不同的锁。

4. Java中的条件变量

    正如我们前面所说,Java采取了wait/notify机制来作为intrinsic lock 相关的条件变量,表示为等待某一条件成立的条件队列——说到这里顺带插一段,条件队列必然与某个锁相关,并且语义上关联某个谓词(条件队列、锁、条件谓词就是吉祥的一家)。所以,在使用wait/notify方法时,必然是已经获得相关锁了的,在进一步说,一个推论就是“wait/notify  方法只能出现在相应的同步块中”。如果不呢?就像下面一段(notify表示的谓词是“帐户里有钱啦~”):

 

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
	public void deposit(int amount){
		balance +=amount;
		notify();
	}

//或者这样:

	public void deposit(int amount){
		synchronized (lock) {
			balance +=amount;
			notify();
		}
	}

这两段都是错的,第一段没有在同步块里,而第二段拿到的是lock的内部锁,调用的却是this.notify(),让人遗憾。运行时他们都会抛IllegalMonitorStateException异常——唉,想前一阵我参加一次笔试的时候,有一道题就是这个,让你选所给代码会抛什么异常,我当时就傻了,想这考得也太偏了吧,现在看看,确实是很基本的概念,当初被虐是压根没有理解wait/notify机制的缘故。那怎么写是对的呢?

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
	public void deposit(int amount){
		synchronized (lock) {
			balance +=amount;
			lock.notify();
		}
	}
//或者(取决于你采用的锁):
	synchronized public void deposit(int amount){
		balance +=amount;
		notify();
	}

5.这就够了吗?

    看上去,Java的内部锁和wait/notify机制已经可以满足任何同步需求了,不是吗?em…可以这么说,但也可以说,不那么完美。有两个问题:

  • 锁不够用

    有时候,我们的类里不止有一个状态,这些状态是相互独立的,如果只用同一个内部锁来维护他们全部,未免显得过于笨拙,会严重影响吞吐量。你马上会说,你刚才不是演示了用任意一个Object来做锁吗?我们多整几个Object分别加锁不就行了吗?没错,是可行的。但这样可能显得有些丑陋,而且Object来做锁本身就有语义不明确的缺点。

  • 条件变量不够用

    Java用wait/notify机制实际上默认给一个内部锁绑定了一个条件队列,但是,有时候,针对一个状态(锁),我们的程序需要两个或以上的条件队列,比如,刚才的Account例子,如果某个2B银行有这样的规定“一个账户存款不得多于10000元”,这个时候,我们的存钱需要满足“余额+要存的数目不大于10000,否则等待,直到满足这个限制”,取钱需要满足“余额足够,否则等待,直到有钱为止”,这里需要两个条件队列,一个等待“存款不溢出”,一个等待“存款足够”,这时,一个默认的条件队列够用么?你可能又说,够用,我们可以模仿network里的“多路复用”,一个队列就能当多个来使,像这样:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
	public static final int BOUND = 10000;
	private int balance;
	
	public Account(int balance) {
		this.balance = balance;
	}
	
	synchronized public boolean withdraw(int amount) throws InterruptedException{
			while(balance<amount)
				wait();// no money, wait
			balance -= amount;
			notifyAll();// not full, notify
			return true;
	}
	
	synchronized public void deposit(int amount) throws InterruptedException{
			while(balance+amount >BOUND)
				wait();//full, wait
			balance +=amount;
			notifyAll();// has money, notify
	}
}

    不是挺好吗?恩,没错,是可以。但是,仍然存在性能上的缺陷:每次都有多个线程被唤醒,而实际只有一个会运行,频繁的上下文切换和锁请求是件很废的事情。我们能不能不要notifyAll,而每次只用notify(只唤醒一个)呢?不好意思,想要“多路复用”,就必须notifyAll,否则会有丢失信号之虞(不解释了)。只有满足下面两个条件,才能使用notify:

一,只有一个条件谓词与条件队列相关,每个线程从wait返回执行相同的逻辑。

二,一进一出:一个对条件变量的通知,语义上至多只激活一个线程。

 

    我又想插播一段:刚才写上面那段代码,IDE提示抛InterruptedException,我想提一下,这是因为wait是一个阻塞方法,几乎所有阻塞方法都会声明可能抛InterruptedException,这是和Java的interrupt机制有关的,以后我们有机会再说。

 

    既然这么做不优雅不高效不亚克西,那如之奈何?Java提供了其他工具吗?是的。这就是传说中的java.util.concurrency包里的故事。

 

5 java的并发编程工具包

java.util.concurrency是java的并发编程工具包,包下的类主要分为:

  • locks部分:显式锁(互斥锁和速写锁)相关;
  • atomic部分:原子变量类相关,是构建非阻塞算法的基础;
  • executor部分:线程池相关;
  • collections部分:并发容器相关;
  • tools部分:同步工具相关,如信号量、闭锁、栅栏等功能;

类图结构:

J.U.C

BlockingQueue

此接口是一个线程安全的 存取实例的队列。

使用场景

BlockingQueue通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。

BlockingQueue

注意事项:

  • 此队列是有限的,如果队列到达临界点,Thread1就会阻塞,直到Thread2从队列中拿走一个对象。
  • 若果队列是空的,Thread2会阻塞,直到Thread1把一个对象丢进队列。

相关方法

BlockingQueue中包含了如下操作方法:

 Throws ExceptionSpecial ValueBlocksTimes Out
Insert add(o) offer(o) put(o) offer(o, timeout, timeunit)
Remove remove(o) poll() take() poll(timeout, timeunit)
Examine element() peek()    

名词解释:

  • Throws Exception: 如果试图的操作无法立即执行,抛一个异常。
  • Special Value: 如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
  • Blocks: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
  • Times Out: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。

注意事项:

  • 无法插入 null,否则会抛出一个 NullPointerException。
  • 队列这种数据结构,导致除了获取开始和结尾位置的其他对象的效率都不高,虽然可通过remove(o)来移除任一对象。

实现类

因为是一个接口,所以我们必须使用一个实现类来使用它,有如下实现类:

  • ArrayBlockingQueue: 数组阻塞队列
  • DelayQueue: 延迟队列
  • LinkedBlockingQueue: 链阻塞队列
  • PriorityBlockingQueue: 具有优先级的阻塞队列
  • SynchronousQueue: 同步队列

使用示例:

见: BlockingQueue

ArrayBlockingQueue

ArrayBlockingQueue 是一个有界的阻塞队列

  • 内部实现是将对象放到一个数组里。数组有个特性:一旦初始化,大小就无法修改。因此无法修改ArrayBlockingQueue初始化时的上限。
  • ArrayBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。

DelayQueue

DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口:

public interface Delayed extends Comparable<Delayed< {  
 public long getDelay(TimeUnit timeUnit);  // 返回将要延迟的时间段
}
  • 1
  • 2
  • 3
  • 在每个元素的 getDelay() 方法返回的值的时间段之后才释放掉该元素。如果返回的是 0 或者负值,延迟将被认为过期,该元素将会在 DelayQueue 的下一次 take 被调用的时候被释放掉。
  • Delayed 接口也继承了 java.lang.Comparable 接口,Delayed对象之间可以进行对比。这对DelayQueue 队列中的元素进行排序时有用,因此它们可以根据过期时间进行有序释放。

LinkedBlockingQueue

内部以一个链式结构(链接节点)对其元素进行存储 。

  • 可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
  • 内部以 FIFO(先进先出)的顺序对元素进行存储。

PriorityBlockingQueue

一个无界的并发队列,它使用了和类 java.util.PriorityQueue 一样的排序规则。

  • 无法向这个队列中插入 null 值。
  • 插入到 其中的元素必须实现 java.lang.Comparable 接口。
  • 对于具有相等优先级(compare() == 0)的元素并不强制任何特定行为。
  • 从一个 PriorityBlockingQueue 获得一个 Iterator 的话,该 Iterator 并不能保证它对元素的遍历是以优先级为序的。

SynchronousQueue

一个特殊的队列,它的内部同时只能够容纳单个元素。

  • 如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。
  • 如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。

BlockingDeque

此接口表示一个线程安全放入和提取实例的双端队列

使用场景

通常用在一个线程既是生产者又是消费者的时候。
BlockingDeque

注意事项

  • 如果双端队列已满,插入线程将被阻塞,直到一个移除线程从该队列中移出了一个元素。
  • 如果双端队列为空,移除线程将被阻塞,直到一个插入线程向该队列插入了一个新元素。

相关方法

 Throws ExceptionSpecial ValueBlocksTimes Out
Insert addFirst(o) offerFirst(o) putFirst(o) offerFirst(o, timeout, timeunit)
Remove removeFirst(o) pollFirst(o) takeFirst(o) pollFirst(timeout, timeunit)
Examine getFirst(o) peekFirst(o)    
 Throws ExceptionSpecial ValueBlocksTimes Out
Insert addLast(o) offerLast(o) putLast(o) offerLast(o, timeout, timeunit)
Remove removeLast(o) pollLast(o) takeLast(o) pollLast(timeout, timeunit)
Examine getLast(o) peekLast(o)    

注意事项

  • 关于方法的处理方式和上节一样。
  • BlockingDeque 接口继承自 BlockingQueue 接口,可以用其中定义的方法。

实现类

  • LinkedBlockingDeque : 链阻塞双端队列

LinkedBlockingDeque

LinkedBlockingDeque 是一个双端队列,可以从任意一端插入或者抽取元素的队列。

  • 在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据。

ConcurrentMap

一个能够对别人的访问(插入和提取)进行并发处理的 java.util.Map接口。
ConcurrentMap 除了从其父接口 java.util.Map 继承来的方法之外还有一些额外的原子性方法。

实现类

因为是接口,必须用实现类来使用它,其实现类为

  • ConcurrentHashMap

ConcurrentHashMap与HashTable比较

  • 更好的并发性能,在你从中读取对象的时候 ConcurrentHashMap 并不会把整个 Map 锁住,只是把 Map 中正在被写入的部分进行锁定。
  • 在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛 ConcurrentModificationException。

ConcurrentNavigableMap

一个支持并发访问的 java.util.NavigableMap,它还能让它的子 map 具备并发访问的能力。

headMap

headMap(T toKey) 方法返回一个包含了小于给定 toKey 的 key 的子 map。

tailMap

tailMap(T fromKey) 方法返回一个包含了不小于给定 fromKey 的 key 的子 map。

subMap

subMap() 方法返回原始 map 中,键介于 from(包含) 和 to (不包含) 之间的子 map。

更多方法

  • descendingKeySet()
  • descendingMap()
  • navigableKeySet()

CountDownLatch

CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。

  • CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这一数量就减一。
  • 通过调用 await() 方法之一,线程可以阻塞等待这一数量到达零。

CyclicBarrier

CyclicBarrier 类是一种同步机制,它能够对处理一些算法的线程实现同步。

更多实例参考: CyclicBarrier

Exchanger

Exchanger 类表示一种两个线程可以进行互相交换对象的会和点。

更多实例参考: Exchanger

Semaphore

Semaphore 类是一个计数信号量。具备两个主要方法:

  • acquire()
  • release()
  • 每调用一次 acquire(),一个许可会被调用线程取走。
  • 每调用一次 release(),一个许可会被返还给信号量。

Semaphore 用法

  • 保护一个重要(代码)部分防止一次超过 N 个线程进入。
  • 在两个线程之间发送信号。

保护重要部分

如果你将信号量用于保护一个重要部分,试图进入这一部分的代码通常会首先尝试获得一个许可,然后才能进入重要部分(代码块),执行完之后,再把许可释放掉。

Semaphore semaphore = new Semaphore(1);  
//critical section  
semaphore.acquire();  
...  
semaphore.release();
  • 1
  • 2
  • 3
  • 4
  • 5

在线程之间发送信号

如果你将一个信号量用于在两个线程之间传送信号,通常你应该用一个线程调用 acquire() 方法,而另一个线程调用 release() 方法。

  • 如果没有可用的许可,acquire() 调用将会阻塞,直到一个许可被另一个线程释放出来。
  • 如果无法往信号量释放更多许可时,一个 release() 调用也会阻塞。

公平性

无法担保掉第一个调用 acquire() 的线程会是第一个获得一个许可的线程。

可以通过如下来强制公平:

Semaphore semaphore = new Semaphore(1, true); 
  • 1
  • 需要注意,强制公平会影响到并发性能,建议不使用。

ExecutorService

这里之前有过简单的总结: Java 中几种常用的线程池

存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。

实现类

此接口实现类包括:

  • ScheduledThreadPoolExecutor : 通过 Executors.newScheduledThreadPool(10)创建的
  • ThreadPoolExecutor: 除了第一种的其他三种方式创建的

相关方法

  • execute(Runnable):
    无法得知被执行的 Runnable 的执行结果
  • submit(Runnable):
    返回一个 Future 对象,可以知道Runnable 是否执行完毕。
  • submit(Callable):
    Callable 实例除了它的 call() 方法能够返回一个结果,通过Future可以获取。
  • invokeAny(…):
    传入一系列的 Callable 或者其子接口的实例对象,无法保证返回的是哪个 Callable 的结果 ,只能表明其中一个已执行结束。
    如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。
  • invokeAll(…):
    返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。

关闭ExecutorService

  • shutdown() : 不会立即关闭,但它将不再接受新的任务
  • shutdownNow(): 立即关闭

ThreadPoolExecutor

  • ThreadPoolExecutor 使用其内部池中的线程执行给定任务(Callable 或者 Runnable)。

ScheduledExecutorService(接口,其实现类为ScheduledThreadPoolExecutor)

  • ScheduledExecutorService能够将任务延后执行,或者间隔固定时间多次执行。
  • ScheduledExecutorService中的 任务由一个工作者线程异步执行,而不是由提交任务给 ScheduledExecutorService 的那个线程执行。

相关方法

  • schedule (Callable task, long delay, TimeUnit timeunit):
    Callable 在给定的延迟之后执行,并返回结果。
  • schedule (Runnable task, long delay, TimeUnit timeunit)
    除了 Runnable 无法返回一个结果之外,和第一个方法类似。
  • scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
    这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
    period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。
  • scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
    和上一个方法类似,只是period 则被解释为前一个执行的结束和下一个执行的结束之间的间隔。

ForkJoinPool

ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。

用法参考:Java Fork and Join using ForkJoinPool

Lock

Lock 是一个类似于 synchronized 块的线程同步机制。但是 Lock 比 synchronized 块更加灵活、精细。

实现类

Lock是一个接口,其实现类包括:

  • ReentrantLock

示例

Lock lock = new ReentrantLock();  
lock.lock();  
//critical section  
lock.unlock();
  • 1
  • 2
  • 3
  • 4
  • 调用lock() 方法之后,这个 lock 实例就被锁住啦。
  • 当lock示例被锁后,任何其他再过来调用 lock() 方法的线程将会被阻塞住,直到调用了unlock() 方法。
  • unlock() 被调用了,lock 对象解锁了,其他线程可以对它进行锁定了。

Lock 和 synchronized区别

  • synchronized 代码块不能够保证进入访问等待的线程的先后顺序。
  • 你不能够传递任何参数给一个 synchronized 代码块的入口。因此,对于 synchronized 代码块的访问等待设置超时时间是不可能的事情。
  • synchronized 块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的 lock() 和 unlock() 方法的调用放在不同的方法里。

ReadWriteLock

读写锁一种先进的线程锁机制。

  • 允许多个线程在同一时间对某特定资源进行读取,
  • 但同一时间内只能有一个线程对其进行写入。

实现类

  • ReentrantReadWriteLock

规则

  • 如果没有任何写操作锁定,那么可以有多个读操作锁定该锁
  • 如果没有任何读操作或者写操作,只能有一个写线程对该锁进行锁定。

示例:

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  
readWriteLock.readLock().lock();  
    // multiple readers can enter this section  
    // if not locked for writing, and not writers waiting  
    // to lock for writing.  
readWriteLock.readLock().unlock();  
readWriteLock.writeLock().lock();  
    // only one writer can enter this section,  
    // and only if no threads are currently reading.  
readWriteLock.writeLock().unlock();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

更多原子性包装类

位于 atomic包下,包含一系列原子性变量。

    • AtomicBoolean
    • AtomicInteger
    • AtomicLong
    • AtomicReference
posted @ 2020-04-02 16:27  BraveCode  阅读(9153)  评论(0编辑  收藏  举报