最近看到有很多粉丝私信我说,能不能给整理出一份面试的要点出来,说本身复习的时候思绪很乱,总是找不到重点。那么今天就先给你们分享一个面试几乎必问的点,并发!在面试中问的频率很高的一个是分布式,一个就是并发,具体干货都在下方了。java
我:synchronized能够保证方法或者代码在运行时,同一时刻只有一个方法能够进入到临界区,同时还能够保证共享变量的内存可见性。面试
我:咱们能够把它理解为一个同步工具,也能够描述为一种同步机制,它一般被描述为一个对象。与一切皆对象同样,全部的java对象是天生的Monitor, 每个java对象都有成为Monitor的潜质,由于在Java的设计中,每个java对象自打娘胎出来就带了一把看不见的锁,它被叫作内部锁或者Monitor锁。数据库
public static void main(String [] args) { Vector<String> vector = new Vector<>(); for (int i=0; i<10; i++) { vector.add(i+""); } System.out.println(vector); }
public static void test() { List<String> list = new ArrayList<>(); for (int i=0; i<10; i++) { synchronized (Demo.class) { list.add(i + ""); } } System.out.println(list); }
我:轻量级锁提高程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内是不存在竞争的(区别于偏向锁),这是一个经验数据。若是没有竞争,轻量级锁使用CAS操做避免了使用互斥 量的开销,但若是存在竞争,除了互斥量的开销,还额外发生了CAS操做,所以在有竞争的状况下,轻量级锁比传统的重量级锁更慢。编程
二、拷贝对象头中的Mark Word复制到锁记录(Lock Record)中。
三、拷贝成功后,虚拟机将使用CAS操做尝试将锁对象的Mark Word更新为指向Lock Record的指针,并将线程栈帧中的Lock Record里的owner指针指向Object的Mark Word。若是这个更新 动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。缓存
四、若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明 多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志位的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。安全
我:偏向锁的目的是消除数据在无竞争状况下的同步原语,进一步提升程序的运行性能。偏向锁会偏向于第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其余线程获取,那持有 偏向锁的线程将永远不须要同步。数据结构
我:偏向锁、轻量级锁都是乐观锁,重量级锁是悲观锁。一个对象刚开始实例化的时候,没有任何线程来访问它时,它是可偏向的,意味着它认为只可能有一个线程来访问它,因此当第一个线程 访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操做,并将对象头中的ThreadID改为本身的Id,以后再访问这个对象只须要对比ID。一旦有第二个线程访问这个对象,由于偏向锁不会释放,因此第二个线程看到对象是偏向状态,代表在这个对象上存在竞争了,检查原来持有该对象的线程是否依然存活,若是挂了,则能够将对象变为无锁状态,而后从新偏向新的线程。若是原来的线程依然存活,则立刻执行那个线程的操做栈,检查该对象的使用状况,若是仍然须要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是此时升级为轻量级锁)。若是不存在使用了,则能够将对象恢复成无锁状态,而后从新偏向。多线程
我:在JSR113标准中有有一段对JMM的简单介绍:Java虚拟机支持多线程执行。在Java中Thread类表明线程,建立一个线程的惟一方法就是建立一个Thread类的实例对象,当调用了对象的start方法后,相应的线程将会执行。线程的行为有时会与咱们的直觉相左,特别是在线程没有正确同步的状况下。本规范描述了JMM平台上多线程程序的语义,具体包含一个线程对共享变量的写入什么时候能被其余线程看到。这是官方的接单介绍。并发
我:Java内存模型是内存模型在JVM中的体现。这个模型的主要目标是定义程序中各个共享变量的访问规则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量这类的底层细节。经过这些规则来规范对内存的读写操做,保证了并发场景下的可见性、原子性和有序性。 JMM规定了多有的变量都存储在主内存中,每条线程都有本身的工做内存,线程的工做内存保存了该线程中用到的主内存副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不是直接读写主内存。不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量的传递均须要本身的工做内存和主存之间 进行数据同步。而JMM就做用于工做内存和主存之间数据同步过程。他规定了如何作数据同步以及何时作数据同步。也就是说Java线程之间的通讯由Java内存模型控制,JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。app
我:不是的,它须要知足如下两个条件:
一、在单线程环境下不能改变程序运行的结果。
二、存在数据依赖关系的不容许重排序。
其实这两点能够归结为一点:没法经过happens-before原则推导出来的,JMM容许任意的排序。
int a=1; //A int b=2; //B int c=a+b; //C
A,B,C三个操做存在以下关系:A和B不存在数据依赖,A和C,B和C存在数据依赖,所以在重排序的时候:A和B能够随意排序,可是必须位于C的前面,但不管何种顺序,最终结果C都是3.
public class RecordExample2 { int a = 0; boolean flag = false; /** * A线程执行 */ public void writer(){ a = 1; // 1 flag = true; // 2 } /** * B线程执行 */ public void read(){ if(flag){ // 3 int i = a + a; // 4 } }}
假如操做1和操做2之间重排序,可能会变成下面这种执行顺序:
一、线程A执行flag=true;
二、线程B执行if(flag);
三、线程B执行int i = a+a;
四、线程A执行a=1。
按照这种执行顺序线程B确定读不到线程A设置的a值,在这里多线程的语义就已经被重排序破坏了。操做3和操做4之间也能够重排序,这里就不阐述了。可是他们之间存在一个控制依赖的关系,由于只有操做3成立操做4才会执行。当代码中存在控制依赖性时,会影响指令序列的执行的并行度,因此编译器和处理器会采用猜想执行来克服控制依赖对并行度的影响。假如操做3和操做4重排序了,操做4先执行,则先会把计算结果临时保存到重排序缓冲中,当操做3为真时才会将计算结果写入变量i中。
一、可见性:可见性是指线程之间的可见性,一个线程修改的状态对另外一个线程是可见的。也就是一个线程的修改的结果,另外一个线程可以立刻看到。好比:用volatile修饰的变量,就会具备可见性,volatile修饰的变量不容许线程内部缓存和重排序,即直接修改内存,因此对其余线程是可见的。但这里要注意一个问题,volatile只能让被他修饰的内容具备可见性,不能保证它具备原子性。好比 volatile int a=0; ++a;这个变量a具备可见性,可是a++是一个非原子操做,也就是这个操做一样存在线程安全问题。在Java中,volatile/synchronized/final实现了可见性。
二、原子性:即一个操做或者多个操做要么所有执行而且执行的过程不会被任何因素打断,要么都不执行。原子就像数据库里的事务同样,他们是一个团队,同生共死。看下面一个简单的栗子:
i=0; //1 j=i; //2 i++; //3 i=j+1; //4
上面的四个操做,只有1是原子操做,其余都不是原子操做。好比2包含了两个操做:读取i,将i值赋给j。在Java中synchronized/lock操做中保证原子性。
三、有序性:程序执行的顺序按照代码的前后顺序执行。 前面JMM中提到了重排序,在java内存模型中,为了效率是容许编译器和处理器对指令进行重排序,并且重排序不会影响单线程的运行结果,可是对多线程有影响。Java中提供了volatile和synchronized保证有序性。
那么volatile的内存语义是如何实现的呢?对于通常的变量会被重排序,而对于volatile则不能,这样会影响其内存语义,因此为了实现volatile的内存语义JMM会限制重排序。
volatile的重排序规则:
一、若是第一个操做为volatile读,则无论第二个操做是啥,都不能重排序。这个操做确保volatile读以后的操做不会被编译器重排序到volatile读以前。
二、当第二个操做为volatile写,则无论第一个操做是啥,都不能重排序。这个操做确保了volatile写以前的操做不会被编译器重排序到volatile写以后。
三、当第一个操做为volatile写,第二个操做为volatile读,不能重排序。
volatile的底层实现是经过插入内存屏障,可是对于编译器来讲,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,因此JMM采用了保守策略。 以下:
一、在每个volatile写操做前插入一个StoreStore屏障。
二、在每个volatile写操做后插入一个StoreLoad屏障。
三、在每个volatile读操做后插入一个LoadLoad屏障。
四、在每个volatile读操做后插入一个LoadStore屏障。
总结:StoreStore屏障->写操做->StoreLoad屏障->读操做->LoadLoad屏障->LoadStore屏障。 下面经过一个例子简单分析下: volatile原理分析
if (this.value == A) { this.value = B return true; } else { return false; }
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;
如上是AtomicInteger的源码: 一、Unsafe是CAS的核心类,Java没法直接访问底层操做系统,而是经过本地native方法访问。不过尽管如此,JVM仍是开了个后门:Unsafe,它提供了 硬件级别的原子操做。
二、valueOffset:为变量值在内存中的偏移地址,Unsafe就是经过偏移地址来获得数据的原值的。
三、value:当前值,使用volatile修饰,保证多线程环境下看见的是同一个。
// AtomicInteger.java public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; } // Unsafe.java public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
在方法compareAndSwapInt(var1, var2, var5, var5 + var4)中,有四个参数,分别表明:对象,对象的地址,预期值,修改值。
其实在面试里,多线程,并发这块问的仍是很是频繁的,你们看完以后有什么不懂的欢迎在评论区讨论,也能够私信问我,通常我看到以后都会回的!