文档章节

认识同步

如比如比
 如比如比
发布于 2015/10/02 19:22
字数 2706
阅读 79
收藏 3

package javay.test.java;


public class SyncTest {

    static Counter counter = new Counter();


       public static void main(String[] args) {


           MyThread[] threads = new MyThread[1000];

           for (int i = 0; i < 1000; i++) {

               threads[i] = new MyThread();

               threads[i].start();

           }


           for (int i = 0; i < 1000; i++) {

               try {

                   threads[i].join();

               } catch (InterruptedException e) {

                   System.out.println(e);

               }

           }


           System.out.println(SyncTest.counter.count);

       }

}

class MyThread extends Thread {

    public void run() {

        SyncTest.counter.countUp();

    }

}

class Counter {

    int count;

    void countUp() {

        System.out.print("[");

        int n = count;

        System.out.print(".");

        count = n + 1;

        System.out.print("]");

    }

}

------------------------------

class Counter {

    int count;

    void countUp() {

    synchronized (this) {

        System.out.print("[");

        int n = count;

        System.out.print(".");

        count = n + 1;

        System.out.print("]");

    }

    }

}


------------------------------

class Counter {

    int count;

    synchronized void countUp() {

//    synchronized (this) {

        System.out.print("[");

        int n = count;

        System.out.print(".");

        count = n + 1;

        System.out.print("]");

//    }

    }

}

------------------------------

class Global {

    static Object lock = new Object();

}

class Counter {

    int count;

//    synchronized void countUp() {

    void countUp() {

//    synchronized (this) {

    synchronized (Global.lock) {

        System.out.print("[");

        int n = count;

        System.out.print(".");

        count = n + 1;

        System.out.print("]");

    }

    }

}

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。 


多线程同时执行下面的代码可能会出错: 

Java代码

public class Counter {  

    protected long count = 0;  

    public void add(long value){  

        this.count = this.count + value;    

    }  

}  



想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序: 

从内存获取 this.count 的值放到寄存器 

将寄存器中的值增加value 

将寄存器中的值写回内存 

观察线程A和B交错执行会发生什么: 

this.count = 0; 

   A: 读取 this.count 到一个寄存器 (0) 

   B: 读取 this.count 到一个寄存器 (0) 

   B: 将寄存器的值加2 

   B: 回写寄存器值(2)到内存. this.count 现在等于 2 

   A: 将寄存器的值加3 

   A: 回写寄存器值(3)到内存. this.count 现在等于 3 

两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。 


竞态条件 & 临界区 

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。

允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。 


局部变量 

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子: 

Java代码

public void someMethod(){  

  long threadSafeInt = 0;  

  threadSafeInt++;  

}  



局部的对象引用 

对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(译者注:即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例: 

Java代码

public void someMethod(){  

  LocalObject localObject = new LocalObject();  

  localObject.callMethod();  

  method2(localObject);  

}  

  

public void method2(LocalObject localObject){  

  localObject.setValue("value");  

}  


样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。 


对象成员 

对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例: 

Java代码

public class NotThreadSafe{  

    StringBuilder builder = new StringBuilder();  

    public add(String text){  

        this.builder.append(text);  

    }    

}  


如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如: 

Java代码

NotThreadSafe sharedInstance = new NotThreadSafe();  

new Thread(new MyRunnable(sharedInstance)).start();  

new Thread(new MyRunnable(sharedInstance)).start();  

public class MyRunnable implements Runnable{  

  NotThreadSafe instance = null;  

  public MyRunnable(NotThreadSafe instance){  

    this.instance = instance;  

  }  

  public void run(){  

    this.instance.add("some text");  

  }  

}  


注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。 

当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子: 

Java代码

new Thread(new MyRunnable(new NotThreadSafe())).start();  

new Thread(new MyRunnable(new NotThreadSafe())).start();  


现在两个线程都有自己单独的NotThreadSafe对象,调用add()方法时就会互不干扰,再也不会有竞态条件问题了。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。 


线程控制逃逸规则 

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。 


引用

如果一个资源的创建,使用,销毁都在同一个线程内完成, 

且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。



资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。 


即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码: 


引用

检查记录X是否存在,如果不存在,插入X


如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录: 


引用

线程1检查记录X是否存在。检查结果:不存在 

线程2检查记录X是否存在。检查结果:不存在 

线程1插入记录X 

线程2插入记录X


同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。

当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。 


我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例: 

Java代码

public class ImmutableValue{  

    private int value = 0;  

    public ImmutableValue(int value){  

        this.value = value;  

    }  

    public int getValue(){  

        return this.value;  

    }  

}  


请注意ImmutableValue类的成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。 


(译者注:注意,“不变”(Immutable)和“只读”(Read Only)是不同的。当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。比如,一个人的出生年月日是“不变”属性,而一个人的年龄便是“只读”属性,但是不是“不变”属性。随着时间的变化,一个人的年龄会随之发生变化,而一个人的出生年月日则不会变化。这就是“不变”和“只读”的区别。(摘自《Java与模式》第34章)) 


如果你需要对ImmutableValue类的实例进行操作,可以通过得到value变量后创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例: 

Java代码

public class ImmutableValue{  

    private int value = 0;  

    public ImmutableValue(int value){  

        this.value = value;  

    }  

    public int getValue(){  

        return this.value;  

    }  

    public ImmutableValue add(int valueToAdd){  

        return new ImmutableValue(this.value + valueToAdd);  

    }  

}  


请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作。 


引用不是线程安全的! 

重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子: 

Java代码

public void Calculator{  

    private ImmutableValue currentValue = null;  

    public ImmutableValue getValue(){  

        return currentValue;  

    }  

    public void setValue(ImmutableValue newValue){  

        this.currentValue = newValue;  

    }  

    public void add(int newValue){  

        this.currentValue = this.currentValue.add(newValue);  

    }  

}  


Calculator类持有一个指向ImmutableValue实例的引用。注意,通过setValue()方法和add()方法可能会改变这个引用。因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,因此Calculator类不是线程安全的。换句话说:ImmutableValue类是线程安全的,但使用它的类不是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。 


要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可。


© 著作权归作者所有

上一篇: The list of characters
下一篇: 认识线程
如比如比
粉丝 127
博文 178
码字总数 286951
作品 0
日本
程序员
私信 提问
加载中

评论(2)

如比如比
如比如比 博主

引用来自“紫电清霜”的评论

犀利如茶壶兄,小弟甘拜下风
不敢不敢
紫电清霜
紫电清霜
犀利如茶壶兄,小弟甘拜下风
服务器文件双向同步的问题,双向同步变成了双向掐架

有谁做过服务器文件双向同步的,目前用的rsync+inotify ,感觉实现不了双向,谁有更好的思路? 现在的问题是,实践后觉得理论上是不可行的, A,B都即是服务端又是客户端,修改A,希望同步到B...

正厅级二奶
2014/11/21
719
2
DRBD的元数据实现:GI元组的原理

一、GI元组 DRBD使用代标识GI tuple来确定复制数据的“代”,通过GI tuple,DRBD可以确定两个节点是不是属于同一集群的事实(而不是意外连接的两个节点),确定重新同步时同步的方向(如果必...

dhb_oschina
2016/04/01
311
0
产品分析

1、杜绝跟进度的时候坐在QA身边玩手机,再看到会直接跟你说 2、每个人职责再同步一下,有些人还不认识,不了解做什么,提高曝光率 3、加强人员培养,不光是跟任务,还要有PM的思维

同程-陈老师
2016/04/20
3
0
将不确定变为确定~开发人员应该明确知道跨域Post的问题

注意:这里的跨域指不到同一域名下,包括一级与二级域名,这里也认为不在同域下 从A网站把信息以Post的方式发送到B网站,这种过程叫做跨域POST,相类的,A网站把B网站的信息获取回来,一般称...

mcy247
2017/12/05
0
0
Header First设计模式学习笔记——单例模式

问题引入 有时候对于有些对象,我们只需要一个,多了反而会出现很多问题。比如:线程池,缓存,处理器偏好设置,日志对象等等(可能导致程序异常,内存泄露)。 模式定义 确保一个类只有一个...

梦回雪夜观花
2014/05/25
135
0

没有更多内容

加载失败,请刷新页面

加载更多

java数据类型

基本类型: 整型:Byte,short,int,long 浮点型:float,double 字符型:char 布尔型:boolean 引用类型: 类类型: 接口类型: 数组类型: Byte 1字节 八位 -128 -------- 127 short 2字节...

audience_1
21分钟前
2
0
太全了|万字详解Docker架构原理、功能及使用

一、简介 1、了解Docker的前生LXC LXC为Linux Container的简写。可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。相当于C++中的NameSpa...

Java技术剑
22分钟前
2
0
Wifiphisher —— 非常非常非常流氓的 WIFI 网络钓鱼框架

编者注:这是一个非常流氓的 WIFI 网络钓鱼工具,甚至可能是非法的工具(取决于你的使用场景)。在没有事先获得许可的情况下使用 Wifiphisher 攻击基础网络设施将被视为非法活动。使用时请遵...

红薯
51分钟前
36
1
MongoDB 4 on CentOS 7安装指南

本教程为CentOS x86_64 7.x操作系统下,MongoDB Community x86_64 4.2(GA)安装指南。 安装方式一:yum repo在线安装 [此方式较为简单,官方推荐] Step1:新建MongDB社区版Yum镜像源。 # vim ...

王焱君
52分钟前
3
0
go-micro 入门教程1.搭建 go-micro环境

微服务的本质是让专业的人做专业的事情,做出更好的东西。 golang具备高并发,静态编译等特性,在性能、安全等方面具备非常大的优势。go-micro是基于golang的微服务编程框架,go-micro操作简单...

非正式解决方案
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部