概述
当我们要对资源进行原子可见性和互斥同步的操作时,我们一般会采用synchronized和volatile关键字来修饰。至于这两个关键字的用法我们可能有些混乱,接下来我们就来捋一捋这两个关键字的用法。
synchronized和volatile之用法对比
- volatile修饰变量,保证了不同线程对这个变量操作的可见性,即一个线程修改了这个变量,这新值对于其他线程来说是立即可见的。
- volatile禁止进行指令重排序。
- volatile本质是告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问变量,其他线程则被阻塞。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以早证变量的修改可见性。
- volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞;
- volatile标记的变量不会被编译器优化;synchronized标记的变量会被编译器优化。
用法和Demo
volatile修饰变量,保证了不同线程对这个变量操作的可见性,即一个线程修改了这个变量,这新值对于其他线程来说是立即可见的。我们来看一下下面这个例子
普通的多线程修改变量的例子:
public class MyClass {
public static int count = 0;
public static void inc() {
//这里延迟2毫秒,使得结果明显
try {
Thread.sleep(2);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同时启动1000个线程,去进行count++计算
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
MyClass.inc();
}
}).start();
}
//期望輸出1000
System.out.println("运行结果:MyClass.count=" + MyClass.count);
}
}
多次执行上面代码片输出的结果是
运行结果:MyClass.count=857
运行结果:MyClass.count=849
···············
发现输出结果基本上都是小于1000。这是每次开启一个线程去执行加法操作的时候,可能上一个线程还没有执行完,拿到的值还是未修改之前的值,这样就会导致最后输出的结果不是我们期望的1000.
用volatile修饰变量之后的多线程问题
public class MyClass {
public static volatile int count = 0;
public static void inc() {
//这里延迟2毫秒,使得结果明显
try {
Thread.sleep(2);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同时启动1000个线程,去进行count++计算
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
MyClass.inc();
}
}).start();
}
//期望輸出1000
System.out.println("运行结果:MyClass.count=" + MyClass.count);
}
}
多次执行上面代码片输出的结果是
运行结果:MyClass.count=775
运行结果:MyClass.count=814
···············
发现输出结果基本上没有1000。这样的结果,是不是让你很意外,volatile修饰了count,这样的话,如果线程A修改了count的值,那对于线程B而言应该是可见的,为什么结果不是1000呢?因为volatile只是保证了变量修改的可见性,却没有保证原子性。在java的内存模型中每一个线程运行时都会有一个线程栈,线程栈保存了线程运行时变量的信息。当线程访问的时候,首先通过对象的引用找到对应在堆内存中变量的值,然后把堆内存变量具体的值load到线程本地内存中,建立一个变量副本,之后线程就不会再和堆内存变量的值有关系,而是直接修改变量副本的值,在修改完之后的某一个时刻(线程退出前),自动把线程变量副本的值写对象所在堆内存中,这样堆内存中的对象就变化了。所以,上面开启了1000个线程,修改的只是修改了自己的副本,并没有直接修改堆内存的值,这样结果一般就会小于1000。如果需要同步操作,则需要要其他的来控制。
附上面解释图(来源于网络),便于理解,给两种不同的图,实质上是一样的。
2.synchronized的用法
1. synchronized修饰变量(count一定是引用类型)
public static Integer count = 0;
public void inc() {
synchronized (count) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
2.synchronized修饰方法
public synchronized void inc() {
try {
Thread.sleep(10);
count++ ;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.synchronized同步代码块
public void inc() {
synchronized(this) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++ ;
}
}
synchronized的用法不在这里过多的赘述,自己敲几个demo熟悉就知道了。
附加
这里的程序我都是用Android Studio来编写和调试的,附上设置让Android Studio也能运行java程序的方法。
File->New->New Module .选择java library 点击next, Finish,记住,这个时候一定要在运行的类里面写一个主函数。
- 主面板中选择Run->Edit configurations,点击左上角的+。选择Application。右边可以重新命名你运行的工程名字,Main name 就是主函数所在的类,Module就是你一开始New的Module,Jre选择Android Studio的jre既可以了。点击Apply->OK。
- 主面板中选择Run->Edit configurations,点击左上角的+。选择Application。右边可以重新命名你运行的工程名字,Main name 就是主函数所在的类,Module就是你一开始New的Module,Jre选择Android Studio的jre既可以了。点击Apply->OK。
控制台输出的时候可能会有乱码,第一个是Setting里的编码,File->Setting,确保都是UTF-8;第二个是在Java工程目录下的build.gradle添加如下代码,根据自己gradle的版本添加一个就可以了,然后重新运行一遍。,就会没有乱码了。
//新版本的gradle
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
//旧版本的gradle
tasks.withType(Compile) {
options.encoding = "UTF-8"
}
转载请标明出处,如有错误之处,请批评指正,谢谢!