专栏名称: 互联网后端架构
主要介绍Java后端架构。其中也会掺杂一些前端、GO、Python、Linux,目标:全栈工程师!---好像很牛叉的样子 ^-^
目录
相关文章推荐
51好读  ›  专栏  ›  互联网后端架构

面试必备 - volatile、synchronized和lock解析

互联网后端架构  · 公众号  · 架构  · 2019-09-08 18:43

正文

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修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:







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