美文网首页程序员今日看点开源工具技巧
Java 线程并发之synchronized和Volatile

Java 线程并发之synchronized和Volatile

作者: 扈扈哈嘿 | 来源:发表于2016-12-29 16:33 被阅读146次

    每个线程都有自己的执行空间(即工作内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存。当多个线程同时读写某个内存数据时,就会涉及到线程并发的问题。涉及到三个特 性:原子性,有序性,可见性。
    简单说下这个三个特性的概念:
    switch(线程特性){
    case (可见性):
    一个数据在多个线程中都存在副本的时候,任何一个线程对共享变量的修改,其它线程都应该看到被修改之后的值。
    break;
    case(有序性):
    线程的有序性即按代码的先后顺序执行。很经典的就是银行的存钱取钱问题,比如A线程负责取钱,B线程负责取钱,账户里面有100块,这时候B和A都读取了账户余额,100块,这时B取出了10块,写入主内存后这时候账户还有90块,但A知道的是100块然后存了10块,再写入主内存就是110块,这显然是不对的,没有保存线程的有序性。
    break;
    case ( 原子性):
    原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。
    Java中的原子操作包括:
    1)除long和double之外的基本类型的赋值操作
    2)所有引用reference的赋值操作
    3)java.concurrent.Atomic.* 包中所有类的一切操作。
    线程之间的交互只能通过共享数据来实现,而不能直接传递数据。
    同步是为了解决多个线程对共享数据的访问和操作混乱达不到预期效果这种情况而引入的机制。
    break;
    }

    Synchronized

    看如下代码:在main方法中:

    for (int index = 0; index < 3; index++) {
        new Thread() {   
         @Override
    public void run() {  
    for (int i = 0; i < 1000; i++) { 
    try {                 
       Thread.sleep(1);       
             } catch (InterruptedException e) {      
                  e.printStackTrace();                }         
           incTestNum();   }    
        }    }.start();
    }
    new Thread() { 
       @Override   
     public void run() { 
           for (int i = 0; i < 300; i++) {    
            try {             
       Thread.sleep(10);        
        } catch (InterruptedException e) {    
                e.printStackTrace();            }    
            getTestNum();        } 
       }}.start();
    
    private static void incTestNum() {  
      i++;    j++;}
    
    private static void getTestNum() {   
     System.out.println("i===========" + i + ";j============" + j);}
    

    我们得到 的结果是:

    i===========63;j============63
    i===========93;j============93
    i===========121;j============122
    i===========151;j============152
    i===========180;j============182
    i===========210;j============212
    i===========240;j============242
    i===========267;j============270
    

    可以看到j有可能会比i大,这是在多线程并发操作i和j 的时候,并没有同步线程,此时同时操作i和j不具有原子性。并且i++本身也不是原子操作,先读取i,再执行i+1;然后再赋值给i;然后再写入内存。但直观来说应该i比j大,这是应该在读取i之后和读取j之前加操作又执行了多次。导致看到的j比i大。
    当我们加上在线程里面加上synchronized之后:

    private synchronized static void incTestNum() {   
     i++;    j++;}
    
    private synchronized static void getTestNum() {    
    System.out.println("i===========" + i + ";j============" + j);}
    

    结果:

    i===========92;j============92
    i===========121;j============121
    i===========150;j============150
    i===========182;j============182
    i===========209;j============209
    i===========240;j============240
    i===========269;j============269
    ......
    i===========3000;j============3000
    

    从结果可以看出,用synchronized锁住的方法是同步执行的,并且得到了正确的结果。
    使用synchronized修饰的方法或者代码块可以看成是一个原子操作。如果一个线程获取了锁,其它线程需要获取锁来执行的时候,其它线程就进入了等待锁的释放。这个过程是阻塞的。
    一个线程执行互斥代码过程如下:

    1. 获得同步锁;
    2. 清空工作内存;
    3. 从主内存拷贝对象副本到工作内存;
    4. 执行代码(计算或者输出等);
    5. 刷新主内存数据;
    6. 释放同步锁。
      所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

    如果在静态方法上加synchronized,那么作用等同于:

    void method{
       synchronized(Obl.class)
       }
    }
    

    既然要同步我们就要用线程之间的同享对象作为锁,所以下面方式是错误的使用:

    Object lock=new Object();
    synchronized(lock){
    
    }
    

    使用同步方法的时候:

    private synchronized  void test(){    }
    

    等价于:

    private void test(){
    synchronized(this){
    }
    }
    

    jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。

    volatile

    关于volatile的实现原理可以看看这篇文章:
    深入分析Volatile的实现原理
    volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的。这就保证了可见性
    我们在上面个例子的共享变量加上volatile关键:

    static volatile int i = 0, j = 0;
    

    再来看看运行结果:

    i===========241;j============241
    i===========272;j============271
    i===========301;j============301
    i===========329;j============330
    i===========359;j============360
    i===========390;j============390
    ......
    i===========2984;j============2993
    

    这看起来加了volatile和没有加是一样的效果,看起来线程都没有同步。原因是volatile不能保证操作的原子性。也就不能保证i++和j++的原子性,当A,B线程读取i的值假设此时i=10,然后A线程执行+1再写入,刷入主内存中,此时主内存i的值是11,然后现在B线程再执行+1写入主内存,此时主内存中i的值被还是11,但此时正常情况应该是12 的。这就是为什么最后的结果i和j都比3000小。
    声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用。如i++;i=i+1;
    当且仅当满足以下所有条件时,才应该使用volatile变量:

    • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    • 该变量没有包含在具有其他变量的不变式中。
    • 访问变量不需要加锁
      通常使用在如下情况:
    static class StopTester implements Runnable { 
       private  boolean stop = false;
    public void stopMe() {
    stop=true;
    }
    @Override
    public void run() {
    while(!stop){
    //TODO
    }
    }
    }
    

    volatile与synchronized的区别

    • volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。
    • volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
    • volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
    • volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
    • volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
    • volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.

    相关文章

      网友评论

        本文标题:Java 线程并发之synchronized和Volatile

        本文链接:https://www.haomeiwen.com/subject/owiqvttx.html