专栏名称: java一日一条
主要是讲解编程语言java,并且每天都推送一条关于java编程语言的信息
目录
相关文章推荐
Java编程精选  ·  为什么不建议用 equals 判断对象相等? ·  昨天  
芋道源码  ·  Java 新魔法:利用 Function ... ·  19 小时前  
芋道源码  ·  某公司新招了个牛逼的架构师后... ·  2 天前  
芋道源码  ·  用 Spring AOP 优化 IN ... ·  3 天前  
51好读  ›  专栏  ›  java一日一条

JAVA中volatile、synchronized和lock解析

java一日一条  · 公众号  · Java  · 2018-10-29 18:58

正文

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)

当两个并发线程访问同一个对象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()则是唤醒所有等待的线程。

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();
    }  
}

注意:







请到「今天看啥」查看全文