Java常见面试题汇总-----------Java多线程(多线程同步机制)

39、Synchronized的底层原理

  synchronized是JAVA中解决并发编程中最常用的方法。
  synchronized的作用如下:
  1、确保线程互斥访问同步代码;
  2、保证共享变量的修改能够及时可见;
  3、有效解决指令重排序问题。
  synrhronized关键字简洁、清晰、语义明确,因此即使有了Lock接口,使用的还是非常广泛。其应用层的语义是可以把任何一个非null对象 作为"锁",当synchronized作用在方法上时,锁住的便是对象实例(this);当作用在静态方法时锁住的便是对象对应的Class实例,因为 Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;当synchronized作用于某一个对象实例时,锁住的便是对应的代码块。在 HotSpot JVM实现中,锁有个专门的名字:对象监视器。
  每个对象都有一个监视器锁(monitor),同步语句块的实现使用的是monitorenter和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功;如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个monitor,重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor。
  值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。
  总结这两条指令的作用:
  monitorenter:
  每个对象都有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。线程执行monitorenter命令获取monitor锁的过程如下:
  1、如果monitor的进入数为0,则线程获取锁,并设置monitor的进入数为1;
  2、如果该线程已经占有该monitor,则进入数+1;
  3、如果其他线程占有该monitor,monitor的进入数不为0,则该线程进入阻塞状态,直到monitor为0,重新获取monitor的所有权。
  monitorexit:
  执行monitorexit的线程必须是monitor的所有者。
  当执行该命令时,monitor的进入数-1,当monitor的进入数为0,该线程已经不再是该monitor的所有者,其他被这个monitor阻塞的线程可以尝试获取monitor的所有权。

线程状态及状态转换

  当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
  Contention List:所有请求锁的线程将被首先放置到该竞争队列;
  Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List;
  Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set;
  OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck;
  Owner:获得锁的线程称为Owner;
  !Owner:释放锁的线程。
  下图反映了个状态转换关系:


Java常见面试题汇总-----------Java多线程(多线程同步机制)_第1张图片

  新请求锁的线程将首先被加入到Conetention List中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从Contention List中移动线程到EntryList,下面说明下ContentionList和EntryList 的实现方式:

ContentionList 虚拟队列

  ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个先进先出(FIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock-Free的队列。


Java常见面试题汇总-----------Java多线程(多线程同步机制)_第2张图片

  因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。

EntryList

  EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到 EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给 OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。
  OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

自旋锁

  那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。
  缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋), 在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。
  还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。显然,自旋的周期选择显得非常重要,但这与操作系统、硬件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性。
  自旋优化策略
  对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其他的自旋优化策略。



40、同步方法与同步代码块的区别

  为何使用同步?
  java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(增删改查),将会导致数据的不准确,相互之间产生冲突。类似于在atm取钱,银行数据却没有变,这是不行的,要存在于一个事务中。因此加入了同步锁,以避免在该线程没有结束前,调用其他线程。从而保证了变量的唯一性,准确性。

  1、同步方法
  即有synchronized (同步) 修饰符修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,要获取内置锁,否则处于阻塞状态。
  同步方法(粗粒度锁):
  A、修饰一般方法: public synchronized void method(){...},获取的是当前调用对象 this 上的锁;
  B、修饰静态方法: public static synchronized void method (){...},获取当前类的字节码对象上的锁。

  2、同步代码块
  即有synchronized修饰符修饰的语句块,被该关键词修饰的语句块,将加上内置锁。实现同步。
  同步代码块(细粒度锁):
  synchronized( obj ){...},同步代码块可以指定获取哪个对象上的锁,obj 任意。
  同步是高开销的操作,因此尽量减少同步的内容。通常没有必要同步整个方法,同步部分代码块即可。同步方法默认用this或者当前类class对象作为锁。同步代码块可以选择以什么来加锁,比同步方法要更细粒度化,我们可以选择只同步会发生问题的部分代码而不是整个方法。
  同步方法锁的是当前对象,当一个线程使用该对象的同步方法时,会获得该对象的锁,其他线程不能访问该对象的同步方法(只有获得该对象的锁才可以访问同步方法,但可以访问该对象的非同步方法),如果并发量大的话,效率很低,因为如果要访问没有冲突的方法时本来不会和之前的操作产生冲突,但因为没有该对象的锁,所以要等待获得该对象的锁,白白地浪费时间。而同步代码块可以选择要同步的代码块,粒度更小,可以避免上面出现的问题。



41、Synchronized和Lock的区别

  1)、Lock是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
  2)、synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  3)、Lock 可以让等待锁的线程响应中断(可中断锁),而 synchronized却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断(不可中断锁);
  4)、通过 Lock 可以知道有没有成功获取锁(tryLock()方法:如果获取了锁,则返回true;否则返回false,也就说这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。),而 synchronized 却无法办到。
  5)、Lock 可以提高多个线程进行读操作的效率(读写锁)。
  6)、Lock 可以实现公平锁,synchronized不保证公平性。
  在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。所以说,在具体使用时要根据适当情况选择。

  锁的一些种类:
  可重入锁,如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法时也同样持有该锁,即同一个线程调用同锁的多个代码块和方法不会重复加锁,synchronized、ReentrantLock和ReentrantReadWriteLock都是可重入锁。
  可重入锁最大的作用是避免死锁。当锁不具有可重入性,那么在持有当前对象锁的代码块或者方法中调用其他同锁的方法,则另一个方法就会等待当前对象锁的释放,实际上该对象锁已经被当前线程所持有,不可能再次获得,因此就发生死锁。
  可中断锁,在等待的过程中可中断,synchronized 不是可中断锁,lockInterruptibly()获取的就是可中断锁,中断会抛出异常。
  如果某一线程 A 正在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
  公平锁,ReentrantLock和ReentrantReadWriteLock两个锁都可以加入Boolean型的构造参数设置成公平锁,但默认是非公平锁,synchronized是非公平锁。
  多个线程同时等待,当前线程执行完毕,随机执行下个线程即非公平的,公平锁即先到先执行,尽量以请求锁的顺序来获取锁。
  读写锁,即读锁和写锁是分开的,ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。  



42、Volatile与Synchronized的对比

  1、volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。
  2、从内存可见性角度看,volatile读相当于加锁,volatile写相当于解锁。
  3、synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
  4、关键字volatile解决的是变量在多个线程之间的可见性问题,而synchronized关键字解决的是多个线程之间访问资源的同步性。



43、乐观锁和悲观锁的区别

  悲观锁:悲观锁是认为肯定有其他线程来争夺资源,因此不管到底会不会发生争夺,悲观锁总是会先去锁住资源,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。Synchronized 和 Lock 都是悲观锁。
  乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。就是当去做某个修改或其他操作的时候它认为不会有其他线程来做同样的操作(竞争),这是一种乐观的态度,通常是基于 CAS 原子指令来实现的。CAS 通常不会将线程挂起,因此有时性能会好一些。乐观锁的一种实现方式——CAS。
  在数据库系统中,悲观锁从数据开始更改时就将数据锁住,直到更改完成才释放。乐观锁直到修改完成准备提交所做的修改到数据库的时候才会将数据锁住。乐观锁不能解决脏读,乐观锁一般使用数据版本或者时间戳来实现。



44、sleep和wait的区别

  sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
  注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程。
  wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()或notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生InterruptedException异常。
  其实两者都可以让线程暂停一段时间,但是本质的区别是sleep是线程的运行状态控制,wait是线程之间的通讯问题。
  sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定。好比如说,我要做的事情是"点火->烧水->煮面",而当我点完火之后我不立即烧水,我要休息一段时间再烧。对于运行的主动权是由我的流程来控制。
  而wait(),首先,这是由某个确定的对象来调用的,将这个对象理解成一个传话的人,当这个人在某个线程里面说"暂停!",也是 thisOBJ.wait(),这里的暂停是阻塞。还是"点火->烧水->煮饭",thisOBJ就好比一个监督我的人站在我旁边,本来该线程应该执行1后执行2,再执行3,而在2处被那个对象喊暂停,那么我就会一直等在这里而不执行3,但正个流程并没有结束,我一直想去煮饭,但还没被允许,直到那个对象在某个地方说"通知暂停的线程启动!",也就是thisOBJ.notify()的时候,那么我就可以煮饭了,这个被暂停的线程就会从暂停处继续执行。
  1、sleep是Thread的静态方法、wait是Object的方法;
  2、sleep不释放锁对象,wait放弃锁对象;
  3、sleep暂停线程,但监控状态仍然保持,结束后自动恢复;
  4、wait、notify和notifyAll只能在同步控制方法控制块里面使用,而sleep可以在任意地方使用;
  5、wait方法导致线程放弃对象锁,只有针对此对象发出notify(或notifyAll)后才进入对象锁定池准备重新获得对象锁,然后进入就绪状态,准备运行。

你可能感兴趣的:(Java常见面试题汇总-----------Java多线程(多线程同步机制))