简书 賈小強
转载请注明原创出处,谢谢!
在面试中你可能遇到过这样的问题:锁(lock)和监视器(monitor)有什么区别?
嗯,要回答这个问题,你必须深入理解Java的多线程底层是如何工作的。
简短的答案是,锁为实现监视器提供必要的支持。详细答案如下。
锁(lock)
逻辑上锁是对象内存堆中头部的一部分数据。JVM中的每个对象都有一个锁(或互斥锁),任何程序都可以使用它来协调对对象的多线程访问。如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(在锁内存区域设置一些标志)。所有其他的线程试图访问该对象的变量必须等到拥有该对象的锁有的线程释放锁(改变标记)。
一旦线程拥有一个锁,它可以多次请求相同的锁,但是在其他线程能够使用这个对象之前必须释放相同数量的锁。如果一个线程请求一个对象的锁三次,如果别的线程想拥有该对象的锁,那么之前线程需要 “释放”三次锁。
Java中显式锁的使用语法如下:
…
private Lock bankLock = new ReentrantLock();
…
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码的线程
- 锁可以拥有一个或者多个相关的条件对象
- 每个条件对象管理那些已经进入被保护的代码段,但还不能运行的线程
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然后大多数情况下,并不需要这样的控制,并且可以使用一种嵌入Java语言的内部机制。从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就说,要调用该方法,线程必须获得内部的对象锁。
内部锁的一般用法如下:
public synchronized void transfer(int from, int to, double amount) throws InterruptedException
{
while (accounts[from] < amount)
wait();
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());
notifyAll();
}
可以看到,用synchronized关键字来编写代码简洁得多。当然要理解这一代码,你必须了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程
然而,讲了这么多,实际上推荐最好优先使用BlockQueue
,Excutor
,同步集合等,然后再是synchronized关键字,最才是Lock
/Condition
监视器(Monitor)
监视器是一种同步结构,它允许线程同时互斥(使用锁)和协作,即使用等待集(wait-set)使线程等待某些条件为真的能力。
互斥
使用比较形象的说明,监视器就像一个包含一个特殊房间(对象实例)的建筑物,每次只能占用一个线程。这个房间通常包含一些需要防止并发访问的数据。从一个线程进入这个房间到它离开的时间,它可以独占访问房间中的任何数据。进入监控的建筑被称为“进入监控监视器。”进入建筑内部特殊的房间叫做“获取监视器”。房间占领被称为“拥有监视器”,离开房间被称为“释放监视器。”让整个建筑被称为“退出监视器。”
当一个线程访问受保护的数据(进入特殊的房间)时,它首先在建筑物接收(entry-set)中排队。如果没有其他线程在等待(拥有监视器),线程获取锁并继续执行受保护的代码。当线程完成执行时,它释放锁并退出大楼(退出监视器)。
如果当一个线程到达并且另一个线程已经拥有监视器时,它必须在接收队列中等待(entry-set)。当当前所有者退出监视器时,新到达的线程必须与在入口集中等待的其他线程竞争。只有一个线程能赢得竞争并拥有锁。
这里没有wait-set的事。
合作
一般来说,只有当多个线程共享数据或其他资源时,互斥才是重要的。如果两个线程不处理任何公共数据或资源,它们通常不能互相干扰,也不需要以互斥的方式执行。尽管互斥有助于防止线程在共享数据时互相干扰,但合作有助于线程共同努力实现一些共同目标。
合作在当一个线程需要的数据改变为在一个特定的状态时很重要,另一个线程负责将数据该入状态,如生产者/消费者的问题,读线程需要缓冲去在一个“不空”的状态才可以从缓冲区中读取任何数据。如果读线程发现缓冲区为空,则必须等待。写线程负责用数据填充缓冲区。一旦写入线程完成了更多的写入操作,读线程可以进行更多的读取。它有时也称为“Wait and Notify”或“Signal and Continue”监视器,因为它保留对监视器的所有权,并继续执行监视区域(如果需要的话继续)。在稍后的时间内,通知线程释放监视器,等待线程重新恢复拥有锁。
这种合作需要entry-set和wait-set.。下面的图表将有助于您理解这种合作。
上图显示监视器为三个矩形。在该中心,一个大矩形包含一个线程,即监视器的所有者。在左边,一个小矩形包含entry set。在右边,另一个小矩形包含wait set。
监视器是由Per Brich Hansen和Tony Hoare提出的概念,Java以不精确的方式采用了它,也就是Java中的每个对象有一个内部的锁和内部条件。如果一个方法用synchronized关键字声明,那么,它表现的就像一个监视器方法。通过wait/notifyAll/nofify来访问条件变量
我希望上面内容将有助于你更深入地了解Java多线程,欢迎提出任何问题。
Happy Learning !!
网友评论