您的位置:首页 > 编程语言 > Java开发

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

       


    

   
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息