上篇通过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么会产生这样的结果呢,因为建立的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:创建两个线程,执行同一个对象的输出方法。
public class TraditionalThreadSynchronized { public static void main(String[] args) { final Outputter output = new Outputter(); new Thread() { public void run() { output.output("zhangsan"); }; }.start(); new Thread() { public void run() { output.output("lisi"); }; }.start(); } } class Outputter { public void output(String name) { // TODO 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符 for(int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); // Thread.sleep(10); } } }
运行结果:
zhlainsigsan
显然输出的字符串被打乱了,我们期望的输出结果是zhangsanlisi,这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,Java中使用synchronized保证一段代码在多线程执行时是互斥的,有两种用法:
1. 使用synchronized将需要互斥的代码包含起来,并上一把锁。
{ synchronized (this) { for(int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); } } }
这把锁必须是需要互斥的多个线程间的共享对象,像下面的代码是没有意义的。
{ Object lock = new Object(); synchronized (lock) { for(int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); } } }
每次进入output方法都会创建一个新的lock,这个锁显然每个线程都会创建,没有意义。
2. 将synchronized加在需要互斥的方法上。
public synchronized void output(String name) { // TODO 线程输出方法 for(int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); } }
这种方式就相当于用this锁住整个方法内的代码块,如果用synchronized加在静态方法上,就相当于用××××.class锁住整个方法内的代码块。使用synchronized在某些情况下会造成死锁,死锁问题以后会说明。使用synchronized修饰的方法或者代码块可以看成是一个原子操作。
每个锁对(JLS中叫monitor)都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒(notify)后,才会进入到就绪队列,等待CPU的调度,反之,当一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒,这个涉及到线程间的通信,下一篇博文会说明。看我们的例子,当第一个线程执行输出方法时,获得同步锁,执行输出方法,恰好此时第二个线程也要执行输出方法,但发现同步锁没有被释放,第二个线程就会进入就绪队列,等待锁被释放。一个线程执行互斥代码过程如下:
1. 获得同步锁;
2. 清空工作内存;
3. 从主内存拷贝对象副本到工作内存;
4. 执行代码(计算或者输出等);
5. 刷新主内存数据;
6. 释放同步锁。
所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
volatile是第二种Java多线程同步的机制,根据JLS(Java LanguageSpecifications)的说法,一个变量可以被volatile修饰,在这种情况下内存模型(主内存和线程工作内存)确保所有线程可以看到一致的变量值,来看一段代码:
class Test { static int i = 0, j = 0; static void one() { i++; j++; } static void two() { System.out.println("i=" + i + " j=" + j); } }
一些线程执行one方法,另一些线程执行two方法,two方法有可能打印出j比i大的值,按照之前分析的线程执行过程分析一下:
1. 将变量i从主内存拷贝到工作内存;
2. 改变i的值;
3. 刷新主内存数据;
4. 将变量j从主内存拷贝到工作内存;
5. 改变j的值;
6. 刷新主内存数据;
这个时候执行two方法的线程先读取了主存i原来的值又读取了j改变后的值,这就导致了程序的输出不是我们预期的结果,要阻止这种不合理的行为的一种方式是在one方法和two方法前面加上synchronized修饰符:
class Test { static int i = 0, j = 0; static synchronized void one() { i++; j++; } static synchronized void two() { System.out.println("i=" + i + " j=" + j); } }
根据前面的分析,我们可以知道,这时one方法和two方法再也不会并发的执行了,i和j的值在主内存中会一直保持一致,并且two方法输出的也是一致的。另一种同步的机制是在共享变量之前加上volatile:
class Test { static volatile int i = 0, j = 0; static void one() { i++; j++; } static void two() { System.out.println("i=" + i + " j=" + j); } }
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™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。volatile 变量的同步性较差,但它有时更简单并且开销更低。volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized ...
Java线程(二):线程同步synchronized和volatile 详细讲解Java 同步的原理技术资料
3. **有序性**:JMM通过`volatile`关键字和`synchronized`块等机制来确保指令的执行顺序,防止编译器和处理器为了优化而进行的指令重排序。 #### 四、深入剖析volatile关键字 `volatile`关键字是Java语言提供的轻...
Java线程之线程同步synchronized和volatile详解 Java线程同步是Java多线程编程中非常重要的一个方面,主要涉及到线程安全、可见性和有序性三个方面。Java提供了多种同步机制来解决线程安全问题,包括synchronized和...
在 Java 中,还有其他线程同步机制,如volatile 变量、Atomic 变量、Lock 接口等。这些机制可以用于解决不同类型的线程同步问题。 线程同步机制的选择取决于具体的应用场景和需求。开发者需要根据实际情况选择合适...
Java中的`synchronized`关键字是多线程编程中的一个重要概念,用于控制并发访问共享资源时的同步机制。在Java中,当多个线程试图同时访问和修改同一块代码或数据时,可能会导致数据不一致的问题。为了解决这个问题,...
Java 多线程(Synchronized+Volatile+JUC 并发工具原理+线程状态+CAS+线程池) Java 多线程是 Java 语言中的一种并发编程机制,允许程序同时执行多个线程,以提高程序的执行效率和响应速度。 Java 多线程机制提供了...
- 锁是实现线程同步的一种机制,主要包括内置锁(`synchronized`)和显式锁(`java.util.concurrent.locks.Lock`)。 - 显式锁提供了更灵活的锁定机制,如可重入锁、读写锁等。 #### 六、Java线程:线程的交互 - ...
在多线程环境下,可能会出现数据竞争问题,为了解决这个问题,Java提供了多种同步机制,如synchronized关键字、wait/notify机制、Lock锁(ReentrantLock)等。synchronized用于控制对共享资源的访问,而wait/notify...
#### 二、Java线程:创建与启动 - **定义线程** 1. 扩展 `java.lang.Thread` 类。此类中有一个 `run()` 方法,该方法需要特别注意其使用方式: ```java public void run() ``` 如果线程是使用独立的 `Runnable...
Java线程间通信主要包括线程间的同步和线程间的协作两部分。常用的通信方法有`wait()`、`notify()`和`notifyAll()`等。 1. **wait()**:使当前线程暂停执行,并释放当前持有的锁。 2. **notify()**:唤醒正在等待该...
1. 线程优先级:Java定义了三个基本优先级(MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY),线程可以设置优先级,但实际调度受操作系统影响。 2. 守护线程与用户线程:守护线程不阻碍JVM退出,而用户线程则会阻止JVM...
Java提供了wait()、notify()和notifyAll()方法进行线程间的通信,但这需要在同步控制块(synchronized)中使用。Java 5引入了BlockingQueue阻塞队列,提供了一种更安全的线程间通信方式,如ArrayBlockingQueue、...
- `wait()`、`notify()`和`notifyAll()`等方法用于实现线程间的同步。 - `join()`方法允许一个线程等待另一个线程执行完毕。 #### 六、线程安全与同步 - **线程安全问题**: - 数据竞争: 多个线程同时修改同一个...
在Java中,`volatile`关键字也可以用于线程同步,但它的作用比`synchronized`轻量级,主要用于解决单个变量在多线程环境中的可见性问题,而不是防止数据竞争。 总结一下,Java线程同步的关键点: 1. **线程同步是...
在并发编程领域,正确理解和使用同步机制至关重要,其中`synchronized`和`volatile`是Java中两个关键的并发控制工具。本资源"并发编程一之synchronized和volatile.rar"提供了多个示例代码(demo1到demo12),帮助...
6. **线程池**:Java提供`ExecutorService`和`ThreadPoolExecutor`来管理和控制线程,线程池可以有效地复用线程,减少创建和销毁线程的开销,提高系统效率。常用的接口有`Executors`,可以创建固定大小、单线程、...
线程安全是Java多线程编程中的重要考虑因素,合理的线程管理和同步机制可以避免数据错误和程序崩溃。在实际应用中,如QQ聊天、网页访问等场景,多线程技术被广泛使用,以实现高并发和高效响应。 总的来说,理解和...