sychronized关键字与监视器
文章目录
在前两节的学习中理解到,多线程在处理程序显著调高系统效能的背后,带来的更多是并发的问题,而这些并发的问题引起的核心原因是:
- 线程具有共享的存储空间
- 线程在运行的过程中可能在代码运行的任意一个地方被中断
而并发问题引起的表象原因是:
- 线程运行的次序无法确定,且不能依赖于优先级来保证程序的逻辑性
因为上述三个原因针对并发问题的解决点就位于:保证程序会受到并发操作影响部分的“原子性”
通过保证原子性进而保证某块代码上线程运行的次序问题,进而解决并发问题,所以通过Java中已经提供的机制锁对象和条件对象来保证原子性:
- 锁对象:
- 通过持有锁保证当有一个线程在访问临界区的过程中,其他的线程无法访问临界区(一块代码块)
- 在使用完锁之后,将锁释放以保证其他的线程能去持有锁
- 条件对象
- 通过创建条件对象保证当线程持有锁,但是不具备运行条件时,可以释放锁给其他线程
- 线程可以检查条件是否满足,并在条件满足时可以尝试持有锁完成运行
通过锁对象和条件对象解决并发的过程会有一些繁琐,所以Java又提供了一种机制,也就是sychronized 关键字来解决并发同步问题
1.sychronized关键字
1.1总结锁和条件的关键作用:
- 锁可以用来保护代码片段,任何时刻只能有一个线程执行被保护(被上锁)的代码
- 锁可以管理试图进入被保护代码段的线程(互斥访问,拒绝其他线程的访问)
- 锁可以拥有一个或多个相关的条件对象
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
1.2sychronized三种使用形式
1.2.1作用于实例方法
public sychronized void method(){
method body
}
既然已经定义成了关键字,那么这段代码在语法上并没有什么可以多说的,而真正让人关心的应该是这里sychronized关键字实现的原理:
从1.0版本开始,Java中的每一个对象就提供一个内部锁,这是什么意思,就和之前我们使用的Lock锁一样,我们之前会创建一个Lock对象,然后对代码进行上锁,而在Java的对象内部本身就提供了一个锁对象来进行一系列锁相关的操作,所以上述这段代码就等价于:
private Lock intrinsicLock = new ReentrantLock();
public void method(){
this.intrinsicLock.lock();
try{
method body
}finally{
this.intrinsicLock.unlock();
}
}
同样的对于条件对象来说,对象内部本身也内置了一个条件对象: 可以通过调用:
wait();
notifyAll();
等价于:
Condition intrinsicCondition = this.intrinsicLock.creatCondition();
intrinsicCondition.await();
intrisicCondition.signalAll();
来实现对锁的条件对象的管理。
需要注意的是wait,notifyAll和notify 方法都是Object类的final 方法也就是无法被重写的,而这很自然的就会提出一个疑问:在Java中锁是如何实现的?在操作系统中的锁通常是如何实现的?
查看Object类的源代码可以发现:
这几个方法都是通过C++进行实现的,所以留到下一篇博客去分享,这里还是继续
1.2.2作用于静态方法
将静态方法声明为sychronized也是合法的:
静态方法是属于类的方法?又没有实例?那是如何进行上锁的呢?
将方法声明为sychronized 会利用方法所在类的类对象(Class 对象)的内部锁,来对方法提供锁的服务。
如果一个静态方法被声明为同步,则这个方法被调用时,这个方法所在类的Class对象会被锁住,变为临界资源,而此时其他线程都无法调用这个类的这个或者任何其他的同步静态方法。
1.2.3作用于对象(同步阻塞)
通过上面的两种方法的介绍我们都可以了解到在Java中每一个对象都具有一个锁,了解这件事情的本质,那么针对临界资源是对象而不是方法时,是不是也可以使用这种方式:
sychronized(obj){
critical section
}
有时也会通过:
private Object lock = new Object();
public void method(int amount){
sychronized(lock){
amount ++;
}
}
这样的方式来创建一个临界区,通过这样的方式来实现临界区,而这种方式其实叫做**客户端锁定**。更常用更常用的一种形式是:
示例:
比如说我们此时有一段代码:
vector是java.util 包下提供一种数据结构,其实本质上就是一个List,它的get和set方法都是被sychronized修饰的,也就是都是同步方法:
/**
* 用于银行系统的转账逻辑
* @param accounts 银行账户列表
* @param from 转账来源账户index
* @param to 转账目的账户index
* @param amount 转账数额
*/
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);
}
因为get和set方法是同步的,我们可以很清楚的了解到,在set方法和get方法执行的过程中是不会被中断的,但是完全有可能在get方法刚执行完还没有进入set方法的过程中被中断剥夺运行权,那么如果一个线程在相同的存储位置存入不同的值,显然就会出现讹误,所以可以通过下面这段代码来实现:
public void transfer(Vector<Double> accounts,int from,int to,int amount){
sychronized(accounts){
accounts.set(from,accounts.get(from) - amount);
accounts.set(to,accounts.get(to) + amount);
}
}
这个方法可以工作,但是它完全依赖于一个事实,Vector对自己的所有可修改方法都使用内部锁,及锁定对象前提还有一个就是访问这个对象的方法都是临界区。
2.监视器
摘自:《Java 核心技术卷一》
注:这里的监视器是一种线程安全的设计思想,而并不是实际的技术
锁和条件是线程同步的强大工具,但是严格上讲,他们不是面向对象的,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。
最成功的解决方案之一就是**监视器**,用Java的术语来说监视器具有如下的特性:
- 监视器是只包含私有域的类
- 每个监视器类的对象有一个相关的锁
- 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用obj.method(),那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象进行操作时,没有其他线程能访问该域
Java的设计者以不是很精确的方式采用了监视器概念,Java中的每个对象有一个内部的锁和内部的条件对象,如果一个方法用sychronized 关键字声明,那么,它的表现就像是一个监视器方法。通过调用wait/notifyAll/notify 来访问条件变量。
然而下面的三个方面Java对象不同于监视器,从而使得线程的安全性降低:
- 域不要求必须是private
- 方法不要求必须是sychronized
- 内部锁对客户是可用的
将监听器应用到Java中就是Object类的实现,而保证安全性本质其实就是:将所有的域设为私有,那么访问域的操作就只能通过getter和setter方法进行,那么显然,因为方法都是用sychronized 修饰的,所以作为临界区不同线程之间访问是互斥的,自然也就能保证线程安全。