Java: volatile和synchronized的区别 (转)

内存模型与CPU缓存
本来CPU计算的数字都是从主从main memory中读取的,但是CPU运行的速度比计算机读取内存的速度快,为了补齐这个短板,所以出现了CPU缓从这种东西。
在多CPU系统(或多核处理器——一个芯片上有多个CPU),每个CPU有自己的缓存。两个线程A,B在不同的CPU上同时跑,A对主存的某个共享变量修改后会暂时存在CPU a的缓存中。线程B在CPU b上跑,B仍旧是从主存中读取该共享变量,此时B读到的就是旧值了。就出现了数据的不一致性。
这里出现不一致的条件:必须是多个线程并且访问共享变量,而不是普通变量。
 为了解决这个问题,有两种方式:
在总线上加LOCK#锁;
使用缓从一致性协议,比如MESI协议。
2. 并发环境下的可见性、原子性、有序性
原子性:一个操作,要么执行,要么不执行,在执行的过程中不会被打断。

JAVA原子性适用于除了long和double的原始数据类型的“简单操作”。从内存中读写除了long和double的原始数据类型是原子操作的。



简单操作指的是,简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)


i++            # java中不是原子的
i = i+1        # java中不是原子的
i = j          # java中不是原子的
i = 1          # java中是原子的
上面四个语句只有最后一个是原子的。这里注意:i=j,虽然不是原子的(有两步:先从内从读取 j 的值,再将 j 的值写入内从),但是这里的每一步是原子的(这两步都是简单的读写操作)。i++在C++里可能就是原子的,这个与C++本身的内存模型有关。

在32位上,对64bit的long和double变量的读写是分成两个32bit读写的,因此上下文切换可能发生在读(或写)进行到一半时,这叫做word tearing。

用violate定义long或者double变量时, 对 “简单的” 负值和return操作能保证原子性。

不同的JVM提供了对原子性不同程度的保证。。。



可见性:一个线程对共享变量的修改应该被应用内的其他线程立即可见。

有序性:程序执行的顺序按照代码的先后顺序执行。



这里只讨论原子性和有序性。



原子性、可见性

原子性与可见性是两个不同的概念!



Java中原子性保证:Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。



Java中可见性保证:synchronized和Lock、volatile三种。推荐synchronized方式,volatile有局限性,适合某个特定场合。



3. Java的volatile关键字
volatile 单词的意思:易变的,不稳定的,易挥发的。
下面的英文来自《Thinking in Java , edtion 4》

volatile 含义:

The volatile keyword also ensures visibility across the application. If you declare a field to be volatile, this means that as soon as a write occurs for that field, all reads will see the change. This is true even if local caches are involved—volatile fields are immediately written through to main memory, and reads occur from main memory.

大意:可见性和原子性是不同的两个概念。对一个非volatile变量的原子操作的结果不会立即刷新到主从。多线程环境下对某个共享变量访问时,要么把这个变量定义成volatile的,要么使用synchronization,这样就保证了该变量的可见性。但如果该变量的操作是在synchronized方法或代码块中的话,就不用将该变量定义为volatile了,因为synchronization同步机制会强迫刷新到内存,强迫一个线程对共享变量做的改变对整个应用可见。
It’s important to understand that atomicity and volatility are distinct concepts. An atomic operation on a non-volatile field will not necessarily be flushed to main memory, and so another task that reads that field will not necessarily see the new value. If multiple tasks are accessing a field, that field should be volatile; otherwise, the field should only be accessed via synchronization. Synchronization also causes flushing to main memory, so if a field is completely guarded by synchronized methods or blocks, it is not necessary to make it volatile. 


volatile关键字的局限:
volatile doesn’t work when the value of a field depends on its previous value (such as incrementing a counter), nor does it work onfields whose values are constrained by the values of other fields, such as the lower and upper bound of a Range class which must obey the constraint lower <= upper. 
It’s typically only safe to use volatile instead of synchronized if the class has only one mutable field. Again, your first choice should be to use the synchronized keyword—that’s the safest approach, and trying to do anything else is risky.


If you define a variable as volatile, it tells the compiler not to do any optimizations that would remove reads and writes that keep the field in exact synchronization with the local data in the threads. In effect, reads and writes go directly to memory, and are not cached, volatile also restricts compiler reordering of accesses during optimization. However, volatile doesn’t affect the fact that an increment isn’t an atomic operation.


Basically, you should make a field volatile if that field could be simultaneously accessed by multiple tasks, and at least one of those accesses is a write. For example, a field that is used as a flag to stop a task must be declared volatile.

首先需要理解线程安全的两个方面:执行控制内存可见

执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

在Java 5提供了原子数据类型atomic wrapper classes,对它们的increase之类的操作都是原子操作,不需要使用sychronized关键字。

对于volatile关键字,当且仅当满足以下所有条件时可使用:

1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2. 该变量没有包含在具有其他变量的不变式中。

volatile和synchronized的区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

 

==================================== 详细说明 ============================================

1. 概述

在研究并发程序时,我们需要了解java中关键字volatile和synchronized关键字的使用以及lock类的用法。

首先,了解下java的内存模型:

 

 

(1)每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
(2)对该变量操作完成后,在某个时间再把变量刷新回主内存。

那么我们再了解下锁提供的两种特性:互斥(mutual exclusion) 和可见性(visibility):

(1)互斥(mutual exclusion):互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;

(2)可见性(visibility):简单来说就是一个线程修改了变量,其他线程可以立即知道。保证可见性的方法:volatile,synchronized,final(一旦初始化完成其他线程就可见)。

2、volatile

volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

上面的话有些拗口,简单概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。

(1)问题来源

首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存。而在这个过程中,变量的新值对其他线程是不可见的。

public class RunThread extends Thread {
 
    private boolean isRunning = true;
 
    public boolean isRunning() {
        return isRunning;
    }
 
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
 
    @Override
    public void run() {
        System.out.println("进入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("线程执行完成了");
    }
}
 
public class Run {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在main线程中,thread.setRunning(false);将启动的线程RunThread中的共享变量设置为false,从而想让RunThread.java的while循环结束。如果使用JVM -server参数执行该程序时,RunThread线程并不会终止,从而出现了死循环。

(2)原因分析

现在有两个线程,一个是main线程,另一个是RunThread。它们都试图修改isRunning变量。按照JVM内存模型,main线程将isRunning读取到本地线程内存空间,修改后,再刷新回主内存。

而在JVM设置成 -server模式运行程序时,线程会一直在私有堆栈中读取isRunning变量。因此,RunThread线程无法读到main线程改变的isRunning变量。从而出现了死循环,导致RunThread无法终止。

(3)解决方法

volatile private boolean isRunning = true;

(4)原理 

当对volatile标记的变量进行修改时,会将其他缓存中存储的修改前的变量清除,然后重新读取。一般来说应该是先在进行修改的缓存A中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存B中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存A获取到消息,将新值穿给B。最后将新值写入内存。当变量需要更新时都是此步骤,volatile的作用是被其修饰的变量,每次更新时,都会刷新上述步骤。

3、synchronized

Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。

(1)synchronized 方法

方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前.这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

public synchronized void synMethod(){
      //方法体
}

如在线程t1中有语句obj.synMethod(); 那么由于synMethod被synchronized修饰,在执行该语句前, 需要先获得调用者obj的对象锁, 如果其他线程(如t2)已经锁定了obj (可能是通过obj.synMethod,也可能是通过其他被synchronized修饰的方法obj.otherSynMethod锁定的obj), t1需要等待直到其他线程(t2)释放obj, 然后t1锁定obj, 执行synMethod方法. 返回之前之前释放obj锁。

(2)synchronized 块

对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.此时,线程获得的是成员锁。

(3)synchronized (this)

1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  
2. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。  

3. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。 

4. 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。  

5. 以上规则对其它对象锁同样适用。

第三点举例说明:

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } catch (InterruptedException ie) {  
               }  
          }  
     }  
     public static void main(String[] args) {  
          final Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}

含有synchronized同步块的方法m4t1被访问时,线程中m4t2()依然可以被访问。

(4)wait() 与notify()/notifyAll() 

wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!

notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。

notifyAll()则是唤醒所有等待的线程。

4、lock

(1) synchronized的缺陷

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:

  如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

  因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

  另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

  总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象

(2) java.util.concurrent.locks包下常用的类

public interface Lock {
    //获取锁,如果锁被其他线程获取,则进行等待
    void lock(); 
 
    //当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
    void lockInterruptibly() throws InterruptedException;
 
    /**tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成
    *功,则返回true,如果获取失败(即锁已被其他线程获取),则返回
    *false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。*/
    boolean tryLock();
 
    //tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock(); //释放锁
    Condition newCondition();
}

通常使用lock进行同步:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
 
}finally{
    lock.unlock();   //释放锁
}

trylock使用方法:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
 
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意 :
当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程

而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

(3) ReentrantLock 

ReentrantLock,意思是“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
 
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
 
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
 
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

代码解释:

class MyClass {
    public synchronized void method1() {
        method2();
    }
 
    public synchronized void method2() {
 
    }
}

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

  而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

5、volatile和synchronized区别

  1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.

  2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.

  3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.

    《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。
  
  4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.

  5、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。

  6、使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

6、synchronized和lock区别

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

 

 本文转自:

1. volatile和synchronized的区别

2. CSDN: volatile和synchronized的区别

3. Java关键字volatile,原子性,变量可见性

 

posted @ 2021-04-06 10:21  夜行过客  阅读(478)  评论(0编辑  收藏  举报