浅谈Java volatile、synchronized的使用与线程安全
2018-03-12 17:27
344 查看
1.线程的安全性
线程的安全性包括(1)可见性、(2)原子性,下面会从这两个角度谈谈volatile与synchronized的使用。
2.volatile
(1)可见性
要理解volatile关键字,首先要了解java内存模型,如果有有不熟悉的朋友,可以参看这篇文章Java内存模型,通过内存模型,我们可以知道,变量是存在主内存中(也就是我们常说的堆内存),而每个线程会有自己的内存空间,即工作内存,在线程工作的时候,会先把变量从主内存读取到工作内存中,然后执行完相应的操作,再把变量写回主内存,因此,便会存在一个可见性的问题,我们先来举个栗子
public class GirlThread extends Thread {
private boolean isSayLove = false;
public boolean isSayLove() {
return isSayLove;
}
public void setIsSayLove(boolean isSayLove) {
this.isSayLove = isSayLove;
}
@Override
public void run() {
System.out.println("男孩暗恋女孩!");
while (isSayLove == false) {
}
System.out.println("男孩表白成功!");
}
}
public class BoyThread {
public static void main(String[] args) {
try {
GirlThread thread = new GirlThread();
thread.start();
Thread.sleep(1000);
thread.setIsSayLove(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
男孩准备给女孩表白,即男孩线程想将启动女孩线程中的共享变量设置为true,从而想让女孩线程循环结束,但是执行之后,并不会有预期的结果,因为现在有两个线程,一个是男孩线程,另一个是女孩线程,他们都试图修改isSayLove这个变量,按照JVM内存模型,男孩线程将isSayLove读取到本地的线程内存空间,修改后,在刷新回主内存,但是在运行时,线程会一直在私有内存中读取isSayLove变量。因此,女孩线程无法读取到男孩线程改变的isSayLove变量,从而出现了死循环,导致女孩线程无法终止,即女孩听不到男孩的表白。这种情况,在《Effective
JAVA》中,称之为“活性失败”。感兴趣的朋友可以阅读《Effective JAVA》第十章并发。解决方法在isSayLove前加上volatile关键即可,这里,它强制线程从主内存中取 volatile修饰的变量。
扩展一下,当多个线程之间需要根据某个条件确定 哪个线程可以执行时,要确保这个条件在 线程 之间是可见的。因此,可以用volatile修饰。
综上,volatile关键字的作用是:使变量在多个线程间可见(可见性)。
(2)原子性
原子性,就是某系列的操作步骤要么全部执行,要么都不执行。比如,变量的自增操作 i++,分三个步骤:
①从内存中读取出变量 i 的值
②将 i 的值加1
③将 加1 后的值写回内存
这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。
我们再来举个栗子:
public class GrilThraed {
public static void main(String[] args) throws InterruptedException {
SendFlowerThread[] mythreadArray = new SendFlowerThread[100];
for (int i = 0; i < 100; i++) {
mythreadArray[i] = new SendFlowerThread();
}
for (int i = 0; i < 100; i++) {
mythreadArray[i].start();
}
}
}
public class SendFlowerThread extends Thread {
public volatile static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count=" + count);
}
@Override
public void run() {
addCount();
}
}
男孩打算送花给女孩,送花线程中用for循环创建了100个线程,每个线程把数量增加到100,期望的正确结果是女孩线程可以收到100 * 100 = 10000朵花,但是count可能并没有达到10000,是因为volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增)比如,假设 i 自增到 5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量
i 值还是5相当于线程B读取的是已经过时的数据了,从而导致线程不安全性。这种情况,在《Effective JAVA》中,称之为“安全性失败”。
所以,volatile修饰的变量是不具有线程安全性的。
3.synchronized
使用synchronized关键字同步一个明显的特点是:类中定义有多个synchronized修饰的实例方法时,若多个线程拥有同一个MyObject类的对象,则这些方法只能以同步的方式执行。即,执行完一个synchronized修饰的方法后,才能执行另一个synchronized修饰的方法。
所以,synchronized是线程安全的。
4.volatile和synchronized的比较
volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中读取变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。
比较:
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
线程的安全性包括(1)可见性、(2)原子性,下面会从这两个角度谈谈volatile与synchronized的使用。
2.volatile
(1)可见性
要理解volatile关键字,首先要了解java内存模型,如果有有不熟悉的朋友,可以参看这篇文章Java内存模型,通过内存模型,我们可以知道,变量是存在主内存中(也就是我们常说的堆内存),而每个线程会有自己的内存空间,即工作内存,在线程工作的时候,会先把变量从主内存读取到工作内存中,然后执行完相应的操作,再把变量写回主内存,因此,便会存在一个可见性的问题,我们先来举个栗子
public class GirlThread extends Thread {
private boolean isSayLove = false;
public boolean isSayLove() {
return isSayLove;
}
public void setIsSayLove(boolean isSayLove) {
this.isSayLove = isSayLove;
}
@Override
public void run() {
System.out.println("男孩暗恋女孩!");
while (isSayLove == false) {
}
System.out.println("男孩表白成功!");
}
}
public class BoyThread {
public static void main(String[] args) {
try {
GirlThread thread = new GirlThread();
thread.start();
Thread.sleep(1000);
thread.setIsSayLove(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
男孩准备给女孩表白,即男孩线程想将启动女孩线程中的共享变量设置为true,从而想让女孩线程循环结束,但是执行之后,并不会有预期的结果,因为现在有两个线程,一个是男孩线程,另一个是女孩线程,他们都试图修改isSayLove这个变量,按照JVM内存模型,男孩线程将isSayLove读取到本地的线程内存空间,修改后,在刷新回主内存,但是在运行时,线程会一直在私有内存中读取isSayLove变量。因此,女孩线程无法读取到男孩线程改变的isSayLove变量,从而出现了死循环,导致女孩线程无法终止,即女孩听不到男孩的表白。这种情况,在《Effective
JAVA》中,称之为“活性失败”。感兴趣的朋友可以阅读《Effective JAVA》第十章并发。解决方法在isSayLove前加上volatile关键即可,这里,它强制线程从主内存中取 volatile修饰的变量。
扩展一下,当多个线程之间需要根据某个条件确定 哪个线程可以执行时,要确保这个条件在 线程 之间是可见的。因此,可以用volatile修饰。
综上,volatile关键字的作用是:使变量在多个线程间可见(可见性)。
(2)原子性
原子性,就是某系列的操作步骤要么全部执行,要么都不执行。比如,变量的自增操作 i++,分三个步骤:
①从内存中读取出变量 i 的值
②将 i 的值加1
③将 加1 后的值写回内存
这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。
我们再来举个栗子:
public class GrilThraed {
public static void main(String[] args) throws InterruptedException {
SendFlowerThread[] mythreadArray = new SendFlowerThread[100];
for (int i = 0; i < 100; i++) {
mythreadArray[i] = new SendFlowerThread();
}
for (int i = 0; i < 100; i++) {
mythreadArray[i].start();
}
}
}
public class SendFlowerThread extends Thread {
public volatile static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count=" + count);
}
@Override
public void run() {
addCount();
}
}
男孩打算送花给女孩,送花线程中用for循环创建了100个线程,每个线程把数量增加到100,期望的正确结果是女孩线程可以收到100 * 100 = 10000朵花,但是count可能并没有达到10000,是因为volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增)比如,假设 i 自增到 5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量
i 值还是5相当于线程B读取的是已经过时的数据了,从而导致线程不安全性。这种情况,在《Effective JAVA》中,称之为“安全性失败”。
所以,volatile修饰的变量是不具有线程安全性的。
3.synchronized
使用synchronized关键字同步一个明显的特点是:类中定义有多个synchronized修饰的实例方法时,若多个线程拥有同一个MyObject类的对象,则这些方法只能以同步的方式执行。即,执行完一个synchronized修饰的方法后,才能执行另一个synchronized修饰的方法。
所以,synchronized是线程安全的。
4.volatile和synchronized的比较
volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中读取变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。
比较:
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
相关文章推荐
- 并发容器ConcurrentHashMap与synchronized联合使用达到线程安全
- 关于使用synchronized实现线程安全的原理分析
- 线程安全,可以使用synchronized在对象及方法上加锁。
- java并发编程第六章(6)使用线程安全可遍历映射
- Java中Synchronized和Lock的使用和区别
- Java关于Synchronized关键字在不同位置使用的理解
- synchronized 使用 的 理解(来自一个讨论)
- 使用synchronized实现同步代码块
- Java使用synchronized实现多线程操作list<1>
- iOS- Swift:使用FMDB进行数据库操作(线程安全:增删改查)
- 并发编程学习总结(八) :java中synchronized关键字使用详解 对象锁的相关条件的使用(2)
- 详解synchronized与Lock的区别与使用
- synchronized与ReentrantLock的介绍、使用、适合场景及比较
- 使用synchronized获取互斥锁的几点说明
- .NET(C#):线程安全集合的阻塞BlockingCollection的使用
- java 中wait和notify 线程等待和线程唤醒的使用方式 需要借助synchronized
- Java同步机制总结--synchronized关键字的使用
- Java中线程安全集合的使用小结
- synchronized在多线程情况下的使用
- 如何线程安全的使用 HashMap