volatile、synchronized和lock解析

/ Javavolatilesynchronizedlock / 没有评论 / 3518浏览

volatile、synchronized和lock解析

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

2020318185925-java-memory-model

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

volatile

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

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

问题来源

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

一个例子如下,这里定义了 isRunning 成员变量,来控制子线程结束:

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 static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在主线程中设置了thread.setRunning(false); 但是子线程并不会结束而是一直在循环,

解决方法

volatile private boolean isRunning = true;

原理

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

synchronized

Java 语言的关键字,可用来给对象方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。

当两个并发线程访问同一个对象 object 中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问 object的一个加锁代码块时,另一个线程仍然可以访问该 object中的非加锁代码块。

synchronized 方法

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

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

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

简单来说就是如果对象中有方法是使用 synchronized 来同步,必须先获得该方法对象的锁,才能调用该方法,否则只能等待锁释放。

synchronized 块

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

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()依然可以被访问。

wait() 与notify()/notifyAll()

lock

synchronized 的缺陷

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

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

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

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

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

再举个例子:

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

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

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

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修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

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

如果锁具备可重入性,则称作为可重入锁。像 synchronizedReentrantLock 都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个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一直等待永远不会获取到的锁。

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

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类的lowerupper边界,必须遵循lower<=upper的限制。
  6. 使用 volatile而不是 synchronized的唯一安全的情况是类中只有一个可变的域。

synchronized和lock区别

  1. Lock 是一个接口,而 synchronizedJava中的关键字,synchronized是内置的语言实现;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而 synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过 Lock可以知道有没有成功获取锁,而 synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。

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