面试必备:synchronized的底层原理?

最近更新的XX必备系列适合直接背答案,不深究,不喜勿喷。

你能说简单说一下synchronize吗?

可别真简单一句话就说完了呀~

参考回答:

synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性,原子性就是能够保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等该线程处理完数据后才能进行;可见性就是当一个线程在修改共享数据时,其他线程能够看到,保证可见性,volatile关键字也有这个功能;有序性就是,被synchronize锁住后的线程相当于单线程,在单线程环境jvm的重排序是不会改变程序运行结果的,可以防止重排序对多线程的影响。

补充:我们来看一下上边这个回答,其中有好几个部分可以继续延伸,这里指的延伸就是面试官可以再继续问你的问题。

延伸一:java内存模型的三大特性,或者是说一下java内存模型,或者是synchronize跟java内存模型有什么关系吗?

首先补充为何会问到java内存模型,因为Synchronize的三种作用其实就是java内存模型保证的,再就是这个问题可能单独就蹦到考察java内存模型(JMM)上了。

1、什么是java内存模型:java虚拟机规范中定义了java内存模型是用来屏蔽各种硬件和操作系统间内存的差异,来实现java程序在各平台下并发一致性,再就是,java内存模型并不是真实存在的,他只是一种抽象概念,定义了线程和主内存之间的抽象关系,也就是线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程共享变量的副本。

2、java内存模型的三大特性:java内存模型有三大特性,原子性、可见性、有序性。

原子性:要么执行,要么不执行,主要使用互斥锁Synchronize或者lock来保证操作的原子性;
可见性:在变量修改后将新值同步回主内存,主要有两种实现方式,一是volatile,被volatile修饰的变量发生修改后会立即刷新到主内存;二是使用Synchronize或者lock,当一个变量unlock之前会将变量的修改刷新到主内存中;
有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序不会影响单线程的执行结果,却会影响多线程并发执行的正确性。主要有两种方式确保有序性:volatile 和 Synchronize 关键字,volatile是通过添加内存屏障的方式来禁止指令重排序,也就是重排序是不能把后面的指令放到内存屏障之前执行;Synchronize是保证同一时刻有且只有一个线程执行同步代码,类似于串联顺序执行代码。

延伸二:你了解先行发生原则(happens-before)吗?

为什么会出现先行发生原则:从上边我们也能看到,如果java内存模型中所有的有序性都要靠volatile和Synchronize来实现的话,那么是非常繁琐的,所以j就出现这么一个《先行发生原则》,用来判断数据是否存在竞争、线程是否安全的重要依据。

参考回答:

先行发生原则是java内存模型用来定义两个操作之间的偏序关系。比如说A操作先发生于B操作,那么在B操作发生之前,A操作修改了内存中的共享变量,那么就会被B操作察觉到。

先行发生原则其中包含8种规则,比如:程序员次序规则,volatile变量规则,线程的启动、中止、中断等规则。

如果再问到能简单介绍一下你说的这几个规则吗?一般不会问这么细的,了解即可。

程序员次序规则:在一个线程内,在程序前面的操作先行发生于后面的操作。

volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

线程启动规则:Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

线程加入规则:Thread 对象的结束先行发生于 join() 方法返回。

线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

延伸三:volatile的作用,volatile跟Synchronize的区别

补充:如果上边不答到volatile,可能会跳过这个问题,但是多吹点说不定工资高点。

参考回答:

volatile的作用:volatile关键字主要作用是确保可见性跟有序性,当一个共享变量被volatile修饰,如果一个线程修改了这个共享变量,那么其他线程就会立马可知,强制刷新到主内存。

volatile跟Synchronize的区别:

  1. volatile只能作用域变量,Synchronize可作用域变量、方法、类、同步代码块等;
  2. volatile只能保证可见性和有序性,不能保证原子性,Synchronize三者都可以保证。
  3. volatile不会造成线程阻塞,Synchronize可能会造成线程阻塞。
  4. 在性能方面synchronized关键字是防止多个线程同时执行一段代码,会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。

延伸四:你能说说你刚刚提到的重排序吗?

你是否想过,重排序为什么会对多线程产生影响?

参考回答:

重排序是编译器和处理器为了优化程序性能而对指令进行重新排序的一种手段。重排序可以保证最终执行的结果是与程序顺序执行的结果一致,并且只会对不存在数据依赖性的指令进行重排序,重排序在单线程下对最终执行结果是没有影响的,但是在多线程下就会存在问题。

举个例子:

int a = 1;
int b = 2;
int c = a*b;

如上,a与c之间存在数据依赖关系,所以c不能排到A的前面,同时b与c之间也存在数据依赖关系,所以,c也不能排到B的前面,但是a与b之间是不存在数据依赖关系的,所以a与b之间是可以进行重排序的,但是无论怎么重排序都是不会影响到c的值。

但是在多线程中就不一样了,如下代码:

class Test{

    /** 我是变量a **/
    int a = 0;

    /** 我是用来标记变量a是否被写入 **/
    boolean flag = false;

    /** 我是写操作 **/
    public void writer(){
        a = 1/** 第1步 **/
        flag = true/** 第2步 **/
    }

    /** 我是读操作 **/
    public void reader(){
        if(flag){           /** 第3步 **/
            int i = a * a;  /** 第4步 **/
             ......
        }
    } 
}

flag是一个变量,用来表示变量a是否已被写入。这里假设有两个线程A和B ,A线程首先执行writer()方法,随后线程B执行reader()方法。线程B在执行操作第4步的时候,能否看到线程A在操作共享变量a的写入呢?

答案是:在多线程的情况下,不一定能看到;

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

具体细节可以看一下这篇文章,了解一下重排序对多线程的影响:https://blog.csdn.net/zhushuai1221/article/details/51491578

你能说一下Synchronize底层原理吗?

参考回答:

synchronized的底层原理是跟monitor有关,也就是视图器锁,每个对象都有一个关联的monitor,当Synchronize获得monitor对象的所有权后会进行两个指令:加锁指令monitorenter跟减锁指令monitorexit。

monitor里面有个计数器,初始值是从0开始的。如果一个线程想要获取monitor的所有权,就看看它的计数器是不是0,如果是0的话,那么就说明没人获取锁,那么它就可以获取锁了,然后将计数器+1,也就是执行monitorenter加锁指令;monitorexit减锁指令是跟在程序执行结束和异常里的,如果不是0的话,就会陷入一个堵塞等待的过程,直到为0等待结束。

最后

博客地址:https://www.cgblog.com/niceyoo

如果觉得这篇文章有丶东西,不放关注一下我,关注是对我最大的鼓励~

18年专科毕业后,期间一度迷茫,最近我创建了一个公众号用来记录自己的成长。

猜你喜欢

转载自www.cnblogs.com/niceyoo/p/12549327.html