Java中volatile、synchronized和lock解析

一、概述

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


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

(1)每一个线程都有本身的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程本身的本地内存空间,而后对变量进行操做。 编程

(2)对该变量操做完成后,在某个时间再把变量刷新回主内存。缓存


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


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


(2)可见性(visibility):简单来讲就是一个线程修改了变量,其余线程能够当即知道。保证可见性的方法:volatile,synchronized,final(一旦初始化完成其余线程就可见)。并发

二、volatile

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


上面的话有些拗口,简单归纳volatile,它可以使变量在值发生改变时能尽快地让其余线程知道。jvm


(1)问题来源ide

首先咱们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操做会先在寄存器或者是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的做用是被其修饰的变量,每次更新时,都会刷新上述步骤。


三、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)

当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程获得执行。另外一个线程必须等待当前线程执行完这个代码块之后才能执行该代码块。 

  

当一个线程访问object的一个synchronized(this)同步代码块时,其余线程对object中全部其它synchronized(this)同步代码块的访问将被阻塞。  


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


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


以上规则对其它对象锁一样适用。

第三点举例说明:

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()则是唤醒全部等待的线程。


四、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都具有可重入性,因此不会发生上述现象。


五、volatile和synchronized区别

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


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


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


  《Java编程思想》上说,定义long或double变量时,若是使用volatile关键字,就会得到(简单的赋值与返回操做)原子性。 

   

4)volatile不会形成线程的阻塞,而synchronized可能会形成线程的阻塞.


五、当一个域的值依赖于它以前的值时,volatile就没法工做了,如n=n+1,n++等。若是某个域的值受到其余域的值的限制,那么volatile也没法工做,如Range类的lower和upper边界,必须遵循lower<=upper的限制。


六、使用volatile而不是synchronized的惟一安全的状况是类中只有一个可变的域。


六、synchronized和lock区别

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


2)synchronized在发生异常时,会自动释放线程占有的锁,所以不会致使死锁现象发生;而Lock在发生异常时,若是没有主动经过unLock()去释放锁,则极可能形成死锁现象,所以使用Lock时须要在finally块中释放锁;


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


4)经过Lock能够知道有没有成功获取锁,而synchronized却没法办到。


5)Lock能够提升多个线程进行读操做的效率。


在性能上来讲,若是竞争资源不激烈,二者的性能是差很少的,而当竞争资源很是激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。因此说,在具体使用时要根据适当状况选择。

来源:java一日一条

微信图片_20171210074204.jpg

公众号:IT哈哈

相关文章
相关标签/搜索