Java 并发编程——volatile与synchronized
一、Java并发基础
多线程的优点
- 资源利用率更好
- 程序设计在某些情况下更简单
- 程序响应更快
这一点可能对于做客户端开发的更加清楚,一般的UI操作都需要开启一个子线程去完成某个任务,否者会容易导致客户端卡死。例如一个网络请求,可以在回调接口中进行处理,而不是等待网络请求结束了再去做其它的UI操作(UI不会卡死)
多线程的代价
- 设计更加复杂
特别是共享数据的访问,多线程之间的数据通信等。
- 上下文切换开销
当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。
- 增加资源消耗
除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线程。我们可以尝试编写一个程序,让它创建100个线程,这些线程什么事情都不做,只是在等待,然后看看这个程序在运行的时候占用了多少内存。
内存可见性与原子性
- 内存可见性:
下面是一段简单的代码,在单线程环境下执行不会出现问题:
复制代码
public class TestVolatile {
boolean status = false; /**
* 状态切换为true
*/
public void changeStatus(){
status = true;
} /**
* 若状态为true,则running。
*/
public void run(){
if(status){
System.out.println("running....");
}
}
}
上面这个例子,在多线程环境里,假设线程A执行changeStatus()方法后,线程B运行run()方法,可以保证输出"running....."吗?
答案是NO!
这个结论会让人有些疑惑,可以理解。因为倘若在单线程模型里,先运行changeStatus方法,再执行run方法,自然是可以正确输出"running...."的;但是在多线程模型中,是没法做这种保证的。因为对于共享变量status来说,线程A的修改,对于线程B来讲,是"不可见"的。也就是说,线程B此时可能无法观测到status已被修改为true。那么什么是可见性呢?
所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。
Java内存模型
为什么出现这种情况呢,我们需要先了解一下JMM(java内存模型)
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下
需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存。当然如果是出于理解的目的,这样对应起来也无不可。
- 原子性
要么不执行,要么执行到底。原子性就是当某一个线程修改i的值的时候,从取出i到将新的i的值写给i之间不能有其他线程对i进行任何操作。也就是说保证某个线程对i的操作是原子性的,这样就可以避免数据脏读。 通过锁机制或者CAS(Compare And Set 需要硬件CPU的支持)操作可以保证操作的原子性。
指令重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。
但是重排序也需要遵守一定规则:
a) 重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
b)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。
重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a。
public class TestVolatile {
int a = 1;
boolean status = false; /**
* 状态切换为true
*/
public void changeStatus(){
a = 2;//1
status = true;//2
} /**
* 若状态为true,则running。
*/
public void run(){
if(status){//3
int b = a+1;//4
System.out.println(b);
}
}
}
假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?
答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
voaltile
a)当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;这个操作会导致其他线程中的缓存无效,从而保证变量对其它线程的可见性。
b)禁止指令重排序优化。
volatile禁止指令重排序也有一些规则,简单列举一下:
- 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
- 当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
- 当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
synchronized
synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
具体可参考:http://www.importnew.com/21866.html
下面的例子中,当调用get方法时需要等待set运行结束:
public class SyncTestEntity { private int flag = 0; public int getFlag() {
synchronized (this) {
return flag;
}
} public void setFlag(int a) {
synchronized (this) {
System.out.println("into lock");
try {
Thread.sleep(2000);
flag = a;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
final SyncTestEntity entity = new SyncTestEntity();
new Thread(new Runnable() {
@Override
public void run() {
entity.setFlag(10);
}
}).start(); try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result=" + entity.getFlag());
}
}
synchronized demo
运行结果:
into lock
result=10
原子类(atomic)
作用: java中自增/自减操作实际上是由多个原子操作构成(read i; inc; write i),当多个线程同时对一个共享变量进行复合操作时就会出现多线程问题。
java jdk中提供了原子类,提供的所有接口都能保证对基础元素操作的原子性。
基本类型
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
- int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
- boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
- int getAndIncrement(): 以原子的方式将当前值加 1,注意,这里返回的是自增前的值,也就是旧值。
- void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
- int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。
原子类中还有其它数组元素等,这里不再介绍。
使用实例: 三个线程对同一变量进行自增操作
1) 不做任何线程同步
public class AtomicTest { private static String TAG = "AtomicTest"; private static int m;
//private static volatile int m; public static void test() { m = 0;
long startTime = System.currentTimeMillis();
final CountDownLatch cdl = new CountDownLatch(3);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m++;
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m++;
}
cdl.countDown();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m++;
}
cdl.countDown();
}
});
t1.start();
t2.start();
t3.start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "test: result=" + m + " elapsedTime=" + (System.currentTimeMillis()-startTime));
}
}
无任何同步
2) 使用volatile
与1)中代码唯一区别是在定义m时增加volatile修饰符
3) 使用同步锁进行同步
public class AtomicTest { private static String TAG = "AtomicTest"; private static int m; public static void test() { m = 0;
long startTime = System.currentTimeMillis();
final CountDownLatch cdl = new CountDownLatch(3);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (AtomicTest.class) {
m++;
}
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (AtomicTest.class) {
m++;
}
}
cdl.countDown();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (AtomicTest.class) {
m++;
}
}
cdl.countDown();
}
});
t1.start();
t2.start();
t3.start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "test: result=" + m + " elapsedTime=" + (System.currentTimeMillis()-startTime));
}
}
synchronized
4)使用原子类进行控制
public class AtomicTest { private static String TAG = "AtomicTest"; private static AtomicInteger m; public static void test() { m = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
final CountDownLatch cdl = new CountDownLatch(3);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
cdl.countDown();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
cdl.countDown();
}
});
t1.start();
t2.start();
t3.start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Log.d(TAG, "test: result=" + m + " elapsedTime=" + (System.currentTimeMillis()-startTime));
}
}
AtomicInteger
四种方式执行结果如下:
结果 | 耗时 | |
int | 不正确(结果为25000-30000不等) | 26ms |
volatile | 不正确(结果为25000-30000不等) | 26ms |
synchronized | 正确(结果为30000) | 300ms |
AtomicInteger | 正确(结果为30000) | 35ms |
a) volatile 对性能基本没有影响
b) volatile不能保证自增操作的原子性
c) synchronized和AtomicInteger都能保证自增操作的原子性,但是AtomicInteger在性能上有绝对的优势。
参考:
http://blog.csdn.net/suifeng3051/article/details/52611233
https://www.jianshu.com/p/beb2c98003c4
https://blog.csdn.net/suifeng3051/article/details/52611310
http://www.cnblogs.com/chengxiao/p/6528109.html
Java 并发编程——volatile与synchronized的更多相关文章
- Java并发编程 Volatile关键字解析
volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了 ...
- 【Java并发编程实战】-----synchronized
在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者.写相同的数据,访问相同的文件等等.对于这种情况如果我们不加以控制,是非常容易导致错误的.在java中,为了解决这个问题,引入临界区概念. ...
- 干货:Java并发编程系列之synchronized(一)
1. 使用方法 synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面: 确保线程互斥的访问代码块,同一时刻只有一个方法可以进入到临界区 保 ...
- 【Java并发编程实战】—–synchronized
在我们的实际应用其中可能常常会遇到这样一个场景:多个线程读或者.写相同的数据,訪问相同的文件等等.对于这样的情况假设我们不加以控制,是非常easy导致错误的. 在java中,为了解决问题,引入临界区概 ...
- Java并发编程volatile关键字
volatile理解 Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和volatile 关键字机制.volatile具有synchronized关键字的“可见性”,vo ...
- Java并发编程-深入探讨synchronized实现原理
synchronized这个关键字对应Java程序猿来说是非常的熟悉,只要遇到要解决线程安全问题的地方都会使用这个关键字.接下来一起来探讨一下synchronized到底时怎么实现线程同步,使用syn ...
- java并发编程 volatile关键字 精准理解
1.volatile的作用 一个线程共享变量(类的成员变量.类的静态成员变量等)被volatile修饰之后,就具有以下作用: 1)并发中的变量可见性(不同线程对该变量进行操作时的可见性),即一个线程修 ...
- Java并发编程--Volatile详解
摘要 Volatile是Java提供的一种弱同步机制,当一个变量被声明成volatile类型后编译器不会将该变量的操作与其他内存操作进行重排序.在某些场景下使用volatile代替锁可以减少 ...
- java并发编程:深入了解synchronized
简介 synchronized是Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.同时它还保证了共享变量的内存可见性. ...
随机推荐
- 第六章 副词(Les adverbes )
副词属于不变词类,无性.数变化(tout除外),它的功能是修饰动词.形容词.副词或句子. ➡副词的构成 ⇨单一副词 bien tard hier mal vite tôt très souvent ...
- Jsp+servlet+mysql搭建套路
1.建立数据库根据需求建立相应的数据库确立数据库的字段.属性.主键等2.建立javaweb项目,搭建开发环境在开发环境的/WebRoot/WEB-INF下建立lib文件夹,存放需要使用的jar包常用的 ...
- ViewFlipper实现自动播放的图片库
作者实现的基础上,加上了文字的变换 public class MainActivity extends Activity { private ViewFlipper viewFlipper; priv ...
- shell 脚本,将/etc/目录下所有的软链接文件输出
#!/bin/bash # cd /etc for a in *;do if [ -L $a ];then #如果文件存在,为软链接文件且指向的是文件,则返回真 echo $a fi done 测试:
- Ansible Ad-Hoc命令
-a:传入模块的参数,不同的模块要传入的参数不同 -B SECOND:当任务放到后台执行异步任务,设置程序运行的超时时间,传入的是一个数值,单位秒 -C:测试该任务能否正常运行,不对被管理主机做出任何 ...
- Elasticsearch 健康状态处理
笔者在自己的 ubuntu 服务器上使用 GET /_cat/health?v 命令时,返回值如下所示 可以看到集群状态为 yellow,这是什么意思呢?原来在 es 的集群状态中,有三种情况,官网描 ...
- IocPerformance 常见IOC 功能、性能比较
IocPerformance IocPerformance 基本功能.高级功能.启动预热三方面比较各IOC,可以用作选型参考. Lamar: StructureMap的替代品 Lamar 文档 兼容S ...
- ASP.NET Core 2 学习笔记(六)MVC
ASP.NET Core MVC跟ASP.NET MVC观念是一致的,使用上也没有什么太大的变化.之前的ASP.NET MVC把MVC及Web API的套件分开,但在ASP.NET Core中MVC及 ...
- 【mysql】Windows环境搭建(适用5.7以上)
1 下载MySQL 登录 https://dev.mysql.com/downloads/mysql/ 2 配置 下载好了zip文件,解压至任意非中文目录,在根目录下新建my.ini: 输入以下内容( ...
- VS2017 无法使用"XXX"附加到应用程序
可能是启用了腾讯的网游,可以关闭游戏,再试一下,如果还是不行,重启一下就可以了.好像是游戏的什么防篡改的作用