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锁对象时,会出现阻塞。