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

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

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的方式来处理。

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

一步一步掌握线程机制(三)---synchronized和volatile的使用的更多相关文章

  1. 一步一步掌握java的线程机制(一)----创建线程

    现在将1年前写的有关线程的文章再重新看了一遍,发现过去的自己还是照本宣科,毕竟是刚学java的人,就想将java的精髓之一---线程进制掌握到手,还是有点难度.等到自己已经是编程一年级生了,还是无法将 ...

  2. 一步一步掌握java的线程机制(二)----Thread的生命周期

    之前讲到Thread的创建,那是Thread生命周期的第一步,其后就是通过start()方法来启动Thread,它会执行一些内部的管理工作然后调用Thread的run()方法,此时该Thread就是a ...

  3. Java 线程 — synchronized、volatile、锁

    线程同步基础 synchronized 和volatile是Java线程同步的基础. synchronized 将临界区的内容上锁,同一时刻只有一个进程能访问该临界区代码 使用的是内置锁,锁一个时刻只 ...

  4. 一步一步掌握线程机制(六)---Atomic变量和Thread局部变量

    前面我们已经讲过如何让对象具有Thread安全性,让它们能够在同一时间在两个或以上的Thread中使用.Thread的安全性在多线程设计中非常重要,因为race condition是非常难以重现和修正 ...

  5. 一步一步创建JAVA线程

    (一)创建线程 要想明白线程机制,我们先从一些基本内容的概念下手. 线程和进程是两个完全不同的概念,进程是运行在自己的地址空间内的自包容的程序,而线程是在进程中的一个单一的顺序控制流,因此,单个进程可 ...

  6. Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)

    Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...

  7. 一步一步开发Game服务器(三)加载脚本和服务器热更新(二)完整版

    上一篇文章我介绍了如果动态加载dll文件来更新程序 一步一步开发Game服务器(三)加载脚本和服务器热更新 可是在使用过程中,也许有很多会发现,动态加载dll其实不方便,应为需要预先编译代码为dll文 ...

  8. 如何一步一步用DDD设计一个电商网站(三)—— 初涉核心域

    一.前言 结合我们本次系列的第一篇博文中提到的上下文映射图(传送门:如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念),得知我们这个电商网站的核心域就是销售子域.因为电子商务是以信息网络 ...

  9. 一步一步开发Game服务器(四)地图线程

    时隔这么久 才再一次的回归正题继续讲解游戏服务器开发. 开始讲解前有一个问题需要修正.之前讲的线程和定时器线程的时候是分开的. 但是真正地图线程与之前的线程模型是有区别的. 为什么会有区别呢?一个地图 ...

随机推荐

  1. python 几种常用测试框架

    测试的常用规则 一个测试单元必须关注一个很小的功能函数,证明它是正确的: 每个测试单元必须是完全独立的,必须能单独运行.这样意味着每一个测试方法必须重新加载数据,执行完毕后做一些清理工作.通常通过se ...

  2. iOS 获取已安装 的APP

    -(void)getAppPlist { Class LSApplicationWorkspace_class = objc_getClass("LSApplicationWorkspace ...

  3. Selenium2(WebDriver)总结(五)---元素操作进阶(常用类)

    1.Alert类 Alert是指windows弹窗的一些操作,需要new一个Alert类 driver.switchTo().alert():切换到alert窗口 alert.getText():取得 ...

  4. Spring MVC中forward请求转发2种方式(带参数)

    Spring MVC中forward请求转发2种方式(带参数) http://www.51gjie.com/javaweb/956.html  

  5. SpringMVC+SPring+Maven+Mybaits+Shiro+Mybaits基础开发项目

    开源项目资料库:https://gitee.com/VCS/seezoon-framework-all Seezoon项目介绍 基于spring,mybatis,shiro面向接口开发的的一套后台管理 ...

  6. BFC特性 形成BFC

    1.示例代码 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <ti ...

  7. Hibernate学习笔记一:项目创建与基本配置文件

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6760773.html  一:ORM ORM:对象-关系 映射. 即:把Java中有关联关系的对象,转换成关系型 ...

  8. java 实现md5加密的三种方式与解密

      java 实现md5加密的三种方式 CreateTime--2018年5月31日15点04分 Author:Marydon 一.解密 说明:截止文章发布,Java没有实现解密,但是已有网站可以免费 ...

  9. 链接sql数据库并输出csv文件

    __author__ = 'chunyang.wu' #作者:SelectDB # -*- coding: utf-8 -*- import MySQLdb import os os.environ[ ...

  10. codevs 2010 求后序遍历

    时间限制: 1 s空间限制: 64000 KB题目描述 Description输入一棵二叉树的先序和中序遍历序列,输出其后序遍历序列.输入描述 Input Description共两行,第一行一个字符 ...