上篇通过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么会产生这样的结果呢,因为建立的Count对象是线程共享的,一个线程改变了其成员变量num值,下一个线程正巧读到了修改后的num,所以会递增输出。

要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。拿上篇博文中的例子来说明,在多个线程之间共享了Count类的一个对象,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程栈),工作内存存储了主内存Count对象的一个副本,当线程操作Count对象时,首先从主内存复制Count对象到工作内存中,然后执行代码count.count(),改变了num值,最后用工作内存Count刷新主内存Count。当一个对象在多个内存中都存在副本时,如果一个内存修改了共享变量,其它线程也应该能够看到被修改后的值,此为可见性。多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程,一个最经典的例子就是银行汇款问题,一个银行账户存款100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操作,并将数据刷新到主内存,最后主内存数据100+10=110,显然这是一个严重的问题,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后取款,此为有序性。本文讲述了JDK5.0之前传统线程的同步方式,更高级的同步方式可参见Java线程(八):锁对象Lock-同步问题更完美的处理方式

下面同样用代码来展示一下线程同步问题。

TraditionalThreadSynchronized.java:创建两个线程,执行同一个对象的输出方法。

  1. public class TraditionalThreadSynchronized {
  2. public static void main(String[] args) {
  3. final Outputter output = new Outputter();
  4. new Thread() {
  5. public void run() {
  6. output.output("zhangsan");
  7. }
  8. }.start();
  9. new Thread() {
  10. public void run() {
  11. output.output("lisi");
  12. }
  13. }.start();
  14. }
  15. }
  16. class Outputter {
  17. public void output(String name) {
  18. // TODO 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符
  19. for(int i = 0; i < name.length(); i++) {
  20. System.out.print(name.charAt(i));
  21. // Thread.sleep(10);
  22. }
  23. }
  24. }

运行结果:

  1. zhlainsigsan

显然输出的字符串被打乱了,我们期望的输出结果是zhangsanlisi,这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,Java中使用synchronized保证一段代码在多线程执行时是互斥的,有两种用法:

1. 使用synchronized将需要互斥的代码包含起来,并上一把锁。

  1. {
  2. synchronized (this) {
  3. for(int i = 0; i < name.length(); i++) {
  4. System.out.print(name.charAt(i));
  5. }
  6. }
  7. }

这把锁必须是需要互斥的多个线程间的共享对象,像下面的代码是没有意义的。

  1. {
  2. Object lock = new Object();
  3. synchronized (lock) {
  4. for(int i = 0; i < name.length(); i++) {
  5. System.out.print(name.charAt(i));
  6. }
  7. }
  8. }

每次进入output方法都会创建一个新的lock,这个锁显然每个线程都会创建,没有意义。

2. 将synchronized加在需要互斥的方法上。

  1. public synchronized void output(String name) {
  2. // TODO 线程输出方法
  3. for(int i = 0; i < name.length(); i++) {
  4. System.out.print(name.charAt(i));
  5. }
  6. }

这种方式就相当于用this锁住整个方法内的代码块,如果用synchronized加在静态方法上,就相当于用××××.class锁住整个方法内的代码块。使用synchronized在某些情况下会造成死锁,死锁问题以后会说明。使用synchronized修饰的方法或者代码块可以看成是一个原子操作

每个锁对(JLS中叫monitor)都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒(notify)后,才会进入到就绪队列,等待CPU的调度,反之,当一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒,这个涉及到线程间的通信,下一篇博文会说明。看我们的例子,当第一个线程执行输出方法时,获得同步锁,执行输出方法,恰好此时第二个线程也要执行输出方法,但发现同步锁没有被释放,第二个线程就会进入就绪队列,等待锁被释放。一个线程执行互斥代码过程如下:

1. 获得同步锁;

2. 清空工作内存;

3. 从主内存拷贝对象副本到工作内存;

4. 执行代码(计算或者输出等);

5. 刷新主内存数据;

6. 释放同步锁。

所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

volatile是第二种Java多线程同步的机制,根据JLS(Java LanguageSpecifications)的说法,一个变量可以被volatile修饰,在这种情况下内存模型(主内存和线程工作内存)确保所有线程可以看到一致的变量值,来看一段代码:

  1. class Test {
  2. static int i = 0, j = 0;
  3. static void one() {
  4. i++;
  5. j++;
  6. }
  7. static void two() {
  8. System.out.println("i=" + i + " j=" + j);
  9. }
  10. }

一些线程执行one方法,另一些线程执行two方法,two方法有可能打印出j比i大的值,按照之前分析的线程执行过程分析一下:

1. 将变量i从主内存拷贝到工作内存;

2. 改变i的值;

3. 刷新主内存数据;

4. 将变量j从主内存拷贝到工作内存;

5. 改变j的值;

6. 刷新主内存数据;

这个时候执行two方法的线程先读取了主存i原来的值又读取了j改变后的值,这就导致了程序的输出不是我们预期的结果,要阻止这种不合理的行为的一种方式是在one方法和two方法前面加上synchronized修饰符:

  1. class Test {
  2. static int i = 0, j = 0;
  3. static synchronized void one() {
  4. i++;
  5. j++;
  6. }
  7. static synchronized void two() {
  8. System.out.println("i=" + i + " j=" + j);
  9. }
  10. }

根据前面的分析,我们可以知道,这时one方法和two方法再也不会并发的执行了,i和j的值在主内存中会一直保持一致,并且two方法输出的也是一致的。另一种同步的机制是在共享变量之前加上volatile:

  1. class Test {
  2. static volatile int i = 0, j = 0;
  3. static void one() {
  4. i++;
  5. j++;
  6. }
  7. static void two() {
  8. System.out.println("i=" + i + " j=" + j);
  9. }
  10. }

one方法和two方法还会并发的去执行,但是加上volatile可以将共享变量i和j的改变直接响应到主内存中,这样保证了主内存中i和j的值一致性,然而在执行two方法时,在two方法获取到i的值和获取到j的值中间的这段时间,one方法也许被执行了好多次,导致j的值会大于i的值。所以volatile可以保证内存可见性,不能保证并发有序性。

没有明白JLS中为什么使用两个变量来阐述volatile的工作原理,这样不是很好理解。volatile是一种弱的同步手段,相对于synchronized来说,某些情况下使用,可能效率更高,因为它不是阻塞的,尤其是读操作时,加与不加貌似没有影响,处理写操作的时候,可能消耗的性能更多些。但是volatile和synchronized性能的比较,我也说不太准,多线程本身就是比较玄的东西,依赖于CPU时间分片的调度,JVM更玄,还没有研究过虚拟机,从顶层往底层看往往是比较难看透的。在JDK5.0之前,如果没有参透volatile的使用场景,还是不要使用了,尽量用synchronized来处理同步问题,线程阻塞这玩意简单粗暴。另外volatile和final不能同时修饰一个字段,可以想想为什么。

本文来自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7424694

Java线程(二):线程同步synchronized和volatile的更多相关文章

  1. java 多线程:Thread 并发线程: 方法同步synchronized关键字,与static的结合

    1.方法内的变量是安全的 方法内定义的变量,每个变量对应单独的内存变量地址,多个线程之间相互不影响.多个线程之间的变量根本没有一毛钱关系 public class ThreadFuncVarSafe ...

  2. C#中的线程(二) 线程同步基础

    1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具:                       简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程 ...

  3. 第二十篇 .NET高级技术之C#中的线程(二) 线程同步基础

    1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具:                       简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程 ...

  4. (原创)JAVA多线程二线程池

    一,线程池的介绍 线程池包括一下三种: 线程池名称 创建方法 特点 其他 固定大小线程池 ExecutorService threadpool = Executors.newFixedThreadPo ...

  5. java多线程之内存可见性-synchronized、volatile

    1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享 ...

  6. java 语言多线程可见性(synchronized 和 volatile 学习)

    共享变量可见性实现的原理 java 语言层面支持的可见性实现方式: synchronized volatile 1. synchronized 的两条规定: 1 线程解锁前,必须把共享变量的最新值刷新 ...

  7. MFC线程(二):线程同步临界区CRITICAL SECTION

    当多个线程同时使用相同的资源时,由于是并发执行,不能保证先后顺序.所以假如时一个公共变量被几个线程同时使用会造成该变量值的混乱. 下面来举个简单例子. 假如有一个字符数组变量 char g_charA ...

  8. java多线程(二)——锁机制synchronized(同步方法)

    synchronized Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.当两个并发线程访问同一个对象object中 ...

  9. Java多线程之synchronized和volatile

    概述 用Java来开发多线程程序变得越来越常见,虽然Java提供了并发包来简化多线程程序的编写,但是我们有必要深入研究一下,才能更好的掌握这块知识. 本文主要对Java提供的底层原语synchroni ...

随机推荐

  1. 在Quartus II中分配管脚的两种常用方法

    在Quartus II中分配管脚的两种常用方法 示范程序 seg7_test.v 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* * ...

  2. 如何选择正确的DevOps工具

    坦白的讲:世界上没有哪种工具能够像DevOps这么神奇(或敏捷,或精益).DevOps在开发和运营团队之间建立了完美的合作与沟通,因此与其说这是一种神奇的工具,不如说是一种文化的转变. 然而,团队之间 ...

  3. freebsd镜像作用和vmware服务开启

    第一个是可以引导的光盘,只能引导系统,通常用于网络安装.基本没用.第二个是系统光盘的第一张.用这张就可以安装一个基本的系统.其他的软件,在系统安装完之后安装.第三个是系统盘的DVD版本.包括的软件比上 ...

  4. SELECT时为何要加WITH(NOLOCK)

    此文章非原创,仅为分享.学习!! 要提升SQL的查询效能,一般来说大家会以建立索引(index)为第一考虑.其实除了index的建立之外,当我们在下SQL Command时,在语法中加一段WITH ( ...

  5. python_way ,json(自学)

    python_way ,json 如果我们想将多行字典存放到文件中,并且还需要调出这些字典继续使用那么就要是用json. 首先将字典用json转换成字符串,存放到文件中. a = {"tel ...

  6. LINQ之路 2:C# 3.0的语言功能(上)

    在上一篇的LINQ介绍中,我们已经看到了隐式类型变量var,扩展方法(extension method)和lambda表达式的身影.没错,他们正是LINQ技术的基石,是他们让LINQ的实现成为可能,并 ...

  7. views中的公共代码放在一起

    在views中建立一个common.php文件,然后把views中的index.php和about.php公共代码放进去 <!doctype html> <html lang=&qu ...

  8. 解耦HTML、CSS和JavaScript

    当前在互联网上,任何一个稍微复杂的网站或者应用程序都会包含许多HTML.CSS和JavaScript.随着互联网运用的发展以及我们对它的依赖性日益增加,设定一个关于组织和维护你的前端代码的计划是绝对需 ...

  9. 关于Spring定时任务(定时器)用法

    Spring定时任务的几种实现 Spring定时任务的几种实现 一.分类 从实现的技术上来分类,目前主要有三种技术(或者说有三种产品): 从作业类的继承方式来讲,可以分为两类: 从任务调度的触发时机来 ...

  10. CentOS的包/库的找的地方

    1. http://pkgs.org/ 在这个网站里面 搜索相应的 包名,看能不能找到. 2. http://rpm.pbone.net/ 在这个网站里面 搜索相应的 包名,看能不能找到. 3.