Java并发——线程同步Volatile与Synchronized详解
大家好,又见面了,我是你们的朋友全栈君。
0. 前言
转载请注明出处:Java并发——线程同步Volatile与Synchronized详解_SEU_Calvin的博客-CSDN博客_javasynchronized和volatile
面试时很可能遇到这样一个问题:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现线程安全吗?提到线程安全、线程同步,我们经常会想到两个关键字:volatile和synchronized,那么这两者有什么区别呢?
1. volatile修饰的变量具有可见性
volatile是变量修饰符,其修饰的变量具有可见性。
可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。
在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。
例子请查看下面的3.1,帮助理解。
2. volatile禁止指令重排
volatile可以禁止进行指令重排。
指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
程序执行到volatile修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
例子请查看下面3.2,帮助理解。
3. synchronized
synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。
可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。
原子性表现在:要么不执行,要么执行到底。
例子请查看下面3.3,帮助理解。
2. 总结 (1)从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。
(2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。
但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
3. volatile与synchronized的使用场景举例(结合第1部分进行理解学习)
3.1 volatile的使用举例
class MyThread extends Thread {
private volatile boolean isStop = false;
public void run() {
while (!isStop) {
System.out.println("do something");
}
}
public void setStop() {
isStop = true;
}
}
线程执行run()的时候我们需要在线程中不停的做一些事情,比如while循环,那么这时候该如何停止线程呢?如果线程做的事情不是耗时的,那么只需要使用一个标志即可。如果需要退出时,调用setStop()即可。这里就使用了关键字volatile,这个关键字的目的是如果修改了isStop的值,那么在while循环中可以立即读取到修改后的值。
如果线程做的事情是耗时的,那么可以使用interrupt方法终止线程 。如果在子线程“睡觉”时被interrupt,那么子线程可以catch到InterruptExpection异常,处理异常后继续往下执行。
3.2 volatile的使用举例
//线程1:
context = loadContext(); //语句1 context初始化操作
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
因为指令重排序,有可能语句2会在语句1之前执行,可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了。
3.3 必须使用synchronized而不能使用volatile的场景
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
例子中用new了10个线程,分别去调用1000次increase()方法,每次运行结果都不一致,都是一个小于10000的数字。自增操作不是原子操作,volatile 是不能保证原子性的。回到文章一开始的例子,使用volatile修饰int型变量i,多个线程同时进行i++操作。比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了。同理可以解释为什么每次运行结果都是小于10000的数字。 但是使用synchronized对部分代码进行如下修改,就能保证同一时刻只有一个线程获取锁然后执行同步代码。运行结果必然是10000。
public int inc = 0;
public synchronized void increase() {
inc++;
}
本文整理参考自: Java并发编程:volatile关键字解析 – Matrix海子 – 博客园以及warmor的博客。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/157314.html原文链接:https://javaforall.cn
相关文章
- Java数组及二维数组初始化与赋值方法总结
- 菜鸟教程java_JAVA笔记(菜鸟教程)[通俗易懂]
- Java中&、|、&&、||详解
- java的栈内存和堆内存_Java本地方法栈
- uint32 java_关于Java的int和C的uint32之间的转换
- Java安全基础(二)Servlet核心技术
- java解释器虚拟机-Java代码如何运行在Java虚拟机中
- Elastic Stack 实战教程 5:Elasticsearch Java API Client 开发
- java并发编程(1):Java多线程-基本线程类-基础知识复习笔记
- 干货:Java并发编程系列之volatile(二)详解编程语言
- Java Collection或Map的同步详解编程语言
- eeOracle放弃Java EE:梦想的终结(oracle放弃java)
- 之间的交互Redis与Java实现交互的探索(redis和java)
- 时间解决Java程序中Redis设置过期时间的问题(redisjava过期)
- 处理Java实现Redis缓存过期管理(redisjava过期)
- Linux下安装Java 开发环境指南(linux装java环境)
- 清理Redis中Java版本的过期键的清理方法(redisjava过期)
- 时间处理Redis Java中数据过期时间的方法(redisjava过期)
- 数据处理使用Java处理Redis过期数据(redisjava过期)
- 时间Java环境下使用Redis设置过期时间(redisjava过期)
- 实现Java实现Redis锁的研究与应用(redis锁java)
- 实现高并发:Java利用Redis秒杀成功(java秒杀redis)
- 语句Java自动生成Oracle数据库查询语句(java生成oracle)
- 构建基于Java和Oracle的强大技术栈(java架构oracle)
- Java程序建立Oracle数据库表的实现方式(java建oracle表)
- 并发Redis锁保障Java并发性(redis锁实现java)
- java多线程编程之使用Synchronized块同步变量