Java(8-3 01)多线程同步

来源:互联网 发布:摄像机内存卡数据恢复 编辑:程序博客网 时间:2024/06/07 13:01

上一次,我们说了关于同步和条件对象的使用原因和使用方法,这一次,我们接着上回的问题来说,这一节,我们将会讨论关于synchronized关键字的作用和用法,同步阻塞的用法,监视器的概念,以及Volatile域的作用,使用原因和用法。

Part 1 synchronized关键字

先回顾下上一节,我们知道了锁对象的使用,以及在线程进入锁对象后,如何用条件对象处理那些暂时还不能运行的代码。Java的这种机制为我们提供了很大的操作空间,但是实际情况中并不需要这样控制。来看看这个吧,从Java1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法!

也就是说每个对象都有个锁,声明方法前加个synchronized将会把这个对象的锁用来保护这个方法,这样我们就可以实现之前说的锁对象功能了 。

那条件对象的问题如何解决呢? 在内部对象锁的机制中,他将会并且只有一个相关条件(这就是就局限啦,之前的方法,我们可以设置很多条件对象),而且synchronized中的条件调用的并不是signaAll()
和await()方法,而是wait()和notify()方法

所以,我们之前的代码可以修改一下:

class Bank(){    private double[] accounts;    public synchronized void transfer(int form, int to, int amount)throws InterruptedException    {        while(accounts[from] < amount)            wait();  // 注意这里和我们条件对象的区别,调用的不是await()方法而是wait()方法        accounts[from] -= amount;        accounts[to] += amount;        notifyAll(); // 这里相当于我们条件对象的signalAll()方法    }    public synchronized double getTotalBalance(){...}}

(加粗)可以看到使用synchronized关键字来编写代码要简洁地多。我们要理解,每一个对象都有一个内部锁,并且该锁有一个内部条件;由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。

将静态方法设置为synchronized也是合法的。如果调用这种方法,该方法将会获得相关类对象的内部锁。

内部锁和条件虽然简便,易于阅读,但是也存在一个缺陷:
(1)不能中断一个正在试图获得锁的线程。
(2)试图获得锁时不能设定超时。
(3)每个锁仅有单一的条件,可能是不够的。

那我们应该如何选择使用那种机制好呢?最好Lock/Condition和synchronized都别使用,在许多情况下,我们可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁,例如如何使用阻塞队列来同步完成一个共同任务的线程。还应该看一些并行流的知识(二卷一章节)。
但是我们仍然提倡他,如果synchronized适合你的程序,请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。

Part 2 同步阻塞(学长说的那种方法)

正如我们刚刚讨论的那样,线程可以通过上面说的那两种方法实现同步,这里还有一种方法可以获得锁,通过进入一个同步阻塞。当线程表现如下形式的阻塞时:

public class Bank(){    private double[] acounts;    private Object lock = new Object();    ...    public void transfer(int from, int to, int amount)    {        synchronized(lock) //将Object对象上锁        {            accounts[from] -= amount;            accounts[to] += amount;        }        ...    }}

在这里Object的 lock对象的创建仅仅是用来使用每个Java对象持有的锁。

有时候程序员们使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定!
我们在这里举个例子,例如考虑Vector类,一个列表,它的方法是同步的。现在,假定在Vector中存储银行余额。这里有一个transfer方法的原始实现:
public void transfer(Vector accounts, int from,int to,int amount) // Error
{
accounts.set(from,accounts.get(from) - amount);
accounts.set(to,accounts.get(to) + amount);

}
Vector类的get和set方法是同步的,但是,这对于我们并没什么帮助。在第一次对get的调用一斤完成以后,一个线程完全可能在transfer方法中被剥夺运行权。于是,同一个存储位置,可能会被存入不同的值。但是我们可以截获这个锁:

pubilc void transfer(Vector accounts,int form,int to,int amount)
{
synchronized(accounts)
{
accounts.set(from,accounts.get(from) - amount);
accounts.set(to,accounts.get(to) + amount);
}

}
这个方法可以工作,但是他完全依赖与一个事实,Vector类对自己的所有可修改的方法都使用了内部锁(就是对与Vector类内部的各种操作也用了锁,否则很可能Vector类内部操作时,产生内存数据被擦除的情况)。然而,这是真的吗?Java并没给出承诺,必须仔细阅读源码才能有答案,所以说这种客户端锁定是非常脆弱的,使用时候要非常小心(尤其是对各种其他数据结构进行操作的时候)

Part 3 关于Volatile:

Volatile真正解决的问题是 JVM 在-server模式下(注意普通运行模式下没有此问题), 线程优先取用自己的线程私有stack中的变量值, 而不是公共堆中的值, 造成变量值老旧的问题.

换句话说, Volatile强制要求了所有线程在使用变量的时候要去公共内存堆中获取值, 不可以偷懒使用自己的.

Volatile绝对不保证原子性操作!
具体问题非常复杂! 请参考另一位朋友的博文 转自http://www.cnblogs.com/dolphin0520/p/3920373.html[http://www.cnblogs.com/dolphin0520/p/3920373.html]

Part4 关于监视器

用Java的术语来说 监视器是:(1)只包含私有域的类;(2)每个监视器的类对象都有一个相关的锁;(3)使用该锁对所有的方法进行加锁;(4)该锁可以有任意多个相关条件。

然而,Java对象不同于监视器,从而使得线程的安全性下降:
(1)域不要求必须是private
(2)方法不要求必须是synchronized
(3)内部锁对客户是可用的。
这些都是很大的安全漏洞了,引起了大神Per Brinch Hansen的严厉批评 !

原创粉丝点击