Java线程机制(三) synchronized和volatile的使用

现在开始进入线程编程中最重要的话题---数据同步,它是线程编程的核心,也是难点,就算我们理解了 数据同步的基本原理,但是我们也无法保证能够写出正确的同步代码,但基本原理是必须掌握的。

要 想理解数据同步的基本原理,首先就要明白,为什么我们要数据同步?

public class 

CharacterDisplayCanvas extends JComponent implements
        CharacterListener {
    protected FontMetrics fm;
    protected char[] tmpChar = new char[1];
    protected int fontHeight;

    public CharacterDisplayCanvas() {
        setFont(new Font("Monospaced", Font.BOLD, 18));
        fm = Toolkit.getDefaultToolkit().getFontMetrics(getFont());
        fontHeight = fm.getHeight();
    }

    public CharacterDisplayCanvas(CharacterSource cs) {
        this();
        setCharacterSource(cs);
    }

    public void setCharacterSource(CharacterSource cs) {
        cs.addCharacterListener(this);
    }

    public synchronized void newCharacter(CharacterEvent ce) {
        tmpChar[0] = (char) ce.character;
        repaint();
    }

    public Dimension preferredSize() {
        return new Dimension(fm.getMaxAscent() + 10, fm.getMaxAdvance() + 10);
    }

    protected synchronized void paintComponent(Graphics gc) {
        Dimension d = getSize();
        gc.clearRect(0, 0, d.width, d.height);
        if (tmpChar[0] == 0) {
            return;
        }
        int charWidth = fm.charWidth((int) tmpChar[0]);
        gc.drawChars(tmpChar, 0, 1, (d.width - charWidth) / 2, fontHeight);
    }
}

仔细查看上面的代码,我们就会发现,有两个方法的前面多了一个新的关键字:synchronized。让 我们看看这两个方法为什么要添加这个关键字。

newCharacter()用于显示新字母,而paintComponent()负 责调整和重画canvas。这两个方法存在着race condition,也就是竞争,因为它们访问的是同一份数据,最重 要的是它们是由不同的线程所调用的,这就导致我们无法保证它们的调用是按照正确的顺序来进行,可能在 newCharacter()方法未被调用前paintComponent()方法就已经重新绘制canvas。

之所以产生竞争,除 了这两个方法访问的是同一份数据之外,还和它们是非automic有关。我们在初中的时候都学过,原子曾经被 认为是最小单元,不可分的,哪怕现在已经证明这是不正确的,但原子不可分的概念在计算机这里保留了下来 。 一个程序如果被认为是automic,那么就表示它是无法被中断的,不会有中间状态。使用synchronized,就 能保证该方法无法被中断,那么其他线程就无法在该方法没有完成前调用它。

结合对象锁的知识,我 们可以简单的讲解一下synchronized的原理:一个线程如果想要调用另一个线程的synchronized方法,而且该 方法正在被其他线程调用,那么这个线程就必须等待,等待其他线程释放该方法所在的对象的锁,然后获得该 锁执行该方法。锁机制能够确保同一时间只有一个线程能够调用该方法,也就能保证只有一个线程能够访问数 据。

还记得我们之前通过使用标记来结束线程的时候,将该标记用volatile修饰?如果我们不用 volatile,又能使用什么方法呢?

如果单单只是上面的知识,我们可能会想到利用synchronized来同 步run()和setDone(),因为就是这两个方法在竞争done这个数据。但是这样存在很大的问题:run()会在done 没有被设置true前永远不会结束,但是done标记却要等到run()方法结束后才能由setDone()方法进行设置。

这就是一个死锁,永远解不开的锁。

产生死锁的原因有很多,像是上面这种情况就是一个典型 的代表,主要原因就是run()方法的scope(范围)太大。所谓的scope,指的是获取锁到释放锁的时间,而run() 方法的scope是一个循环,除非done设置为true。这种需要依赖其他线程的方法来结束执行的方法,如果将整 个方法设置为同步,就会出现死锁。

所以,最好的方法就是将scope缩小。

我们可以不用对整 个方法进行同步,而是对需要访问的数据进行同步,也就是对done使用volatile。

要想理解volatile 的工作原理,我们必须清楚变量的加载机制。java的内存模型允许线程能够在local memory中持有变量的值, 所以这也就导致某个线程改变该变量的值时,其他线程可能不会察觉到该变量的变化。这种情况只是一种可能 ,并不代表一定会出现,但像是循环执行这种操作,就增加了这种可能。

所以,我们要做的事情其实 很简单,就是让线程从同一个地方取出变量而不是自己维护一份。使用volatile,每次使用该变量都要从主存 储器中读取,每次改变该变量时,也要存入主存储器,而且加载和存储都是automic,无论是否是long或者 double变量(这两种类型的存储是非automic的)。

值得注意的,run()方法和setDone()方法本身就是 automic,因为setDone()方法仅有一个存储操作,而run()方法也只有一个读取操作,其余部分根本就需要该 值保持不变,也就是说,这两个方法其实本身就不存在竞争。

当然,如果还是坚持想要使用 synchronized的话,倒是有个比较丑陋的方法:对done提供setter和getter,然后synchronized这两个方法, 因为取得同步化的锁代表所有暂时存储于寄存器的值都会被清空到主存储器中,这样run()方法中要想取得 done就必须等到setDone()方法设置完毕。

多么丑陋的实现啊!!就为了同步一个变量,结果我们就要 平白对两个方法进行同步,增加无谓的线程开销!!但这也是没有办法的事,如果我们不知道还有volatile的 话,没准还会为自己的小聪明而开心不已!!

这就是多线程编程的现实,如果我们无法知道还有更加 优雅的实现,我们永远也只能写出这样的代码。

但让人更加困惑的是,volatile本身的存在现在也引 起人们的关注:它到底有没有必要?

volatile是以moot point(未决点)来实现的:变量永远都从主存 储器中读取,但这也只是JDK 1.2之前的情况,现在的虚拟机实现使得内存模式越来越复杂,而且也得到了极 大的优化,并且这种趋势只会一直持续下去。也就是说,基于内存模式的volatile可能会因为内存模式的不断 优化而逐渐变得没有意义。

volatile的使用是有局限的,它仅仅解决因内存模式而引发的问题,而且 只能用在对变量的automic操作上,也就是访问该变量的方法只可以有单一的加载或者存储。但很多方法都是 非automic,像是递增或者递减操作,就允许存在中间状态,因为它们本身就是载入,变更和存储的简化而已 ,也就是所谓的syntactic sugar(语法糖)。

我们大概可以这样理解volatile的使用条件:强迫虚拟机 不要临时复制变量,哪怕我们在许多情况下都不会使用它们。

volatile是否可以运用在数组上,让整 个数组中的所有元素都被同步呢?凡是使用java的人都会对这样的幻想嗤之以鼻,因为实际情况是只有数组的 引用才会被同步,数组中的元素不会是volatile的,虚拟机还是可以将个别元素存储于local的寄存器中,没 有任何方法可以指定数组的元素应该以volatile的方式来处理。

我们上面的同步问题是发生在展示随 机数字与字母的显示组件,现在我们继续将功能完善:玩家可以输入所显示的字母,并且正确就会得分。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索线程
, 变量
, volatile
, 方法
, synchronized
, paintcomponent
, 线程递减锁
, 一个
, volatile关键字
synchronized原理
java线程synchronized、java 多线程 volatile、java 线程 volatile、java volatile 机制、java多线程锁机制,以便于您获取更多的相关知识。

时间: 2024-10-25 07:55:34

Java线程机制(三) synchronized和volatile的使用的相关文章

Java线程机制(五) 等待与通知机制

在之前我们关于停止Thread的讨论中,曾经使用过设定标记done的做法,一旦done设置为true,线程就会 结束,一旦为false,线程就会永远运行下去.这样做法会消耗掉许多CPU循环,是一种对内存不友好的行为. java中的对象不仅拥有锁,而且它们本身就可以通过调用相关方法使自己成为等待者和通知者. Object对象本身有两个方法:wait()和notify().wait()会等待条件的发生,而notify()会通知正在 等待的线程此条件已经发生,它们都必须从synchronized方法或

Java线程机制(四) 同步方法和同步块

在之前例子的基础上,我们增加新的功能:根据正确与不正确的响应来显示玩家的分数. public class ScoreLabel extends JLabel implements CharacterListener { private volatile int score = 0; private int char2type = -1; private CharacterSource generator = null, typist = null; public ScoreLabel(Chara

JAVA线程的三种简单实现

JAVA并发编程的书有很多,对我胃口的就这一本:<Java并发编程从入门到精通>. 不厚,但从入门讲起. 今天实践了三种线程的简单实现. ThreadA package demo.thread; public class ThreadA extends Thread { public void run() { super.run(); try { Thread.sleep(500L); } catch (InterruptedException e) { e.printStackTrace()

Java 理论与实践: 正确使用 volatile 变量 线程同步

Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比. 这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化. 而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互. 使用建议:在两个或者更多的线程访问的成员变量上使用volatile.当要访问的变量已在synchronized代码块中,或者为常量时,不必使用. 由于使用vo

详解Java线程编程中的volatile关键字的作用_java

1.volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 先看一段代码,假如线程1先执行,线程2后执行: //线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;   这段代码是很

java的线程机制(二) Thread的生命周期

之前讲到Thread的创建,那是Thread生命周期的第一步,其后就是通过start()方法来启动Thread,它会 执行一些内部的管理工作然后调用Thread的run()方法,此时该Thread就是alive(活跃)的,而且我们还可以通 过isAlive()方法来确定该线程是否启动还是终结. 一旦启动Thread后,我们就只能执行一个方 法:run(),而run()方法就是负责执行Thread的任务,所以终结Thread的方法很简单,就是终结run()方法.仔 细查看文档,我们会发现里面有一个

Java 线程同步 synchronized

先来看一个不带线程同步的例子,这个例子很简单,只是让两个线程输出同样的内容,并不做其他的事, 所以,线程同步在这里体现的并不明显. import java.util.Date; public class ThreadTest extends Thread{ int pauseTime; String name; /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method s

Java线程:volatile关键字

谈及到volatile关键字,不得不提的一篇文章是:<Java 理论与实践: 正确使用 Volatile 变量>,这篇文章对volatile关键字的用法做了相当精辟的阐述. 之所以要单独提出volatile这个不常用的关键字原因是这个关键字在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题. 首先考虑一个问题,为什么变量需要volatile来修饰呢? 要搞清楚这个问题,首先应该明白计算机内部都做什么了.比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入. 同

java并发 多线程-关于java线程中的volatile关键字

问题描述 关于java线程中的volatile关键字 下面的代码中flag具有原子性,加上volatile修饰后具有可视性,能够保证setFlag和getFlag方法的同步,但是不明白与其他持有当前对象锁的同步方法是否能保持同步(就是otherMethod这样的方法) public class volatileTest { private volatile boolean flag; public void setFlag(boolean falg){ this.flag = flag;} pu