[Java复习05] 多线程&并发 知识点补充

Posted fyql

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java复习05] 多线程&并发 知识点补充相关的知识,希望对你有一定的参考价值。

0. wait/notify/notifyAll的理解?

  wait:让持有该对象锁的线程等待;

  notify: 唤醒任何一个持有该对象锁的线程;

  notifyAll: 唤醒所有持有该对象锁的线程;

  它们 3 个的关系是,调用对象的 wait 方法使线程暂停运行,通过 notify/ notifyAll 方法唤醒调用 wait 暂时的线程。

  它们并不是 Thread 类中的方法,而是 Object 类中的,为什么呢?

  因为每个对象都有监视锁,线程要操作某个对象当然是要获取某个对象的锁了,而不是线程的锁。

  注意点:

  1、调用对象的 wait, notify, notifyAll 方法需要拥有对象的监视器锁,即它们只能在同步方法(块)中使用;

  2、调用 wait 方法会使用线程暂停并让出 CPU 资源,同时释放持有的对象的锁;

  3、多线程使用 notify 容易发生死锁,一般使用 notifyAll;

 

1. sleep()和wait()的区别?

  1.1.使用限制(异常捕获)

  sleep()让线程休眠, 时间到继续执行,需要捕获InterruptedException。

  wait()必须放入synchronized同步代码块,同样需要捕获 InterruptedException 异常,并且需要获取对象的锁。

  wait 还需要额外的方法 notify/ notifyAll 进行唤醒,它们同样需要放在 synchronized 块里面,且获取对象的锁。

  notify/notifyAll不需要捕获异常, 但是wait仍然需要捕获InterruptedException。

   1.2 使用场景

  sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。

  1.3 所属类

  sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。

  为什么要这样设计呢?

   因为 sleep 是让当前线程休眠,不涉及到对象类,也不需要获得对象的锁,所以是线程类的方法。

   wait 是让获得对象锁的线程实现等待,前提是要楚获得对象的锁,所以是类的方法。

  1.4 释放锁(wait会释放锁,notify仅仅只是通知,不释放锁

  wait 可以释放当前线程对 lock 对象锁的持有,而 sleep 则不会。

  wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。

  当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞。

  这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁。

  调用某个对象的notify()/notifyAll()方法能够唤醒一个/多个正在等待这个对象的monitor的线程。

  notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁。

 

2. Join的使用和原理?

  join()是线程类Thread的方法,定义是指当前线程等待这个线程结束后再继续执行

  主线程里调用t.join()会阻塞主线程,直到 t线程 执行完毕。

  原理:利用wait方法来实现。当main方法主线程调用线程t的时候,main方法获取到了t的对象锁,

             而t调用自身wait方法进行阻塞,只要当t结束或者到时间后才会退出,接着唤醒主线程继续执行。

 

3. 线程sleep后,中途调用thread.interrupt()会出现什么情况?

  Interrupt会打断当前线程的sleep, 并且抛出InterruptedException。

 

4. synchronized有几种用法?

  1. 同步普通方法

     同一个实例只有一个线程能获取锁进入这个方法。

     只能作用在单实例上,多个实例,同步方法失效。

  2. 同步静态方法

     不管你有多少个类实例,同时只有一个线程能获取锁进入这个方法。

  3. 同步类

    锁住效果和同步静态方法一样,都是类级别的锁,同时只有一个线程能访问带有同步类锁的方法。

  4. 同步this实例

      这也是同步块的用法,表示锁住整个当前对象实例,只有获取到这个实例的锁才能进入这个方法。

     用法和同步普通方法锁一样,都是锁住整个当前实例。

  5. 同步XXX对象实例

  这也是同步块的用法,和上面的锁住当前实例一样,这里表示锁住整个XXX对象实例,只有获取到这个XXX实例的锁才能进入这个方法。

  注意点:

  类锁与实例锁不相互阻塞,但相同的类锁,相同的当前实例锁,相同的对象锁会相互阻塞。

 

5. 实现方法块同步和代码同步的原理?

  JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,可以使用monitorenter和monitorexit指令实现。

  monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit指令则插入到方法结束和异常处,

  JVM保证每个monitorenter都有一个monitorexit阈值相对应。线程执行到monitorenter的时候,会尝试获得对象所对应的monitor的锁,

  然后才能获得访问权限,synchronize使用的锁保存在Java对象头中。

 

6. synchronized实现原理?

  synchronized是基于Monitor(对象监视器)来实现同步的。

  Monitor从两个方面来支持线程之间的同步:互斥执行,协作

  1. Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。

  2. 使用 wait/notify/notifyAll 方法来协同不同线程之间的工作。

  3. Class和Object都关联了一个Monitor。

  Monitor 的工作机理:

  线程进入同步方法中后,

  为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。

  任一时刻内,监视者对象只属于一个活动线程(The Owner)。

  拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。

  其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。

  同步方法执行完毕了,线程退出临界区,并释放监视锁。

 

7. synchronized的具体实现?

  1、同步代码块采用monitorenter、monitorexit指令显式的实现。

  2、同步方法则使用ACC_SYNCHRONIZED标记符隐式的实现。

  monitorenter:

  每一个对象都有一个monitor,一个monitor只能被一个线程拥有。

  当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:

    如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。

    如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。

    如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。

  monitorexit:

    只有拥有相应对象的monitor的线程才能执行monitorexit指令。

    每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。

  锁存放位置:

  锁标记存放在Java对象头的Mark Word中。

  synchronized的锁优化:

  JDK1.6引入“偏向锁”和“轻量级锁”。锁4中状态:无锁->偏向锁->轻量级锁->重量级锁

 

8. JMM对共享变量的读写原理?

  每个线程都有自己的工作内存,每个线程需要对共享变量操作时必须先把共享变量从主内存 load 到自己的工作内存,等完成对共享变量的操作时再 save 到主内存。

 

9.内存不可见问题?

    如果一个线程运算完后还没刷到主内存,此时这个共享变量的值被另外一个线程从主内存读取到了,

    这个时候读取的数据就是脏数据了,它会覆盖其他线程计算完的值。

 

10. volatile的实现原理?

  1. JVM的Lock前缀的指令将使得CPU缓存写回到系统内存中去。

  2. 为了保证缓存一致性原则,在多CPU的情景下,一个CPU的缓存回写内存会导致其他的CPU上的缓存都失效,再次访问会重新从系统内存加载新的缓存内容。

 

11. HashMap是不是线程安全的?

    场景1: HashMap作为方法内局部变量,线程安全!

    在方法内部的局部变量时,局部变量属于当前线程级别变量,其他线程访问不了,所以安全。

    场景2: 对象成员变量,不安全!

    有哪几种线程安全的Map?  

    1. HashTable

        get/put方法都被synchronized关键字修饰,但同一个线程只能get或put,效率低。

    2. SynchronizedMap

private Map<String, Object> map = Collections.synchronizedMap(new HashMap<String, Object>());

     把传入的HashMap进行包装同步,实现方式是加mutex对象锁,性能也不好。

    3. ConcurrentHashMap – 推荐

       JDK1.8之前分段锁,分16个桶,每次加锁只加一个桶。

       JDK1.8加入CAS算法和红黑树,提高效率。

12. JDK1.7和JDK1.8关于ConcurrentHashMap的实现原理?

    在java 7中,ConcurrentHashMap的实现是基于分段锁协议的实现,本质上还是使用了锁。

    只是基于一种考虑,就是多个线程访问哈希桶具有随机性,基于这种考虑来将数据存储在不同的哈希段上面,然后每一个段配有一把锁,

    在需要写某个段的时候需要加锁,而在这个时候,其他访问其他段的线程是不需要阻塞的,

    但是对于该段的线程访问就需要等待,直到这个加锁的线程释放了锁,其他线程才能进行访问。 

    在java 8中,ConcurrentHashMap的实现抛弃了这种复杂的架构设计,但是继承了这种分散线程竞争压力的思想,

    其实就提高系统的并发度这一维度来说,分散竞争压力是一种最为直接明了的解决方案。

     而java 8在实现ConcurrentHashMap的时候大量使用了CAS操作,减少了使用锁的频度来提高系统的响应度,

     其实使用锁和使用CAS来做并发在复杂度上不是一个数量级的,使用锁在很大程度上假设了多个线程的排斥性,

     并且使用锁会将线程阻塞等待,也就是说使用锁来做线程同步的时候,线程的状态是会改变的,

     但是使用CAS是不会改变线程的状态的(不太严谨的说),所以使用CAS比起使用synchronized或者使用Lcok来说更为轻量级。

 

13. 如果程序中使用了线程池,如何才能在某个任务执行完成之后执行某些动作呢?

      Java线程池本身已经提供了任务执行前后的hook方法(beforeExecute和afterExecute),

      只需要自定义线程池继承ThreadPoolExecutor, 然后重写beforeExecute和afterExecute方法即可。

 

14. 什么是同步/并发队列(等待/阻塞队列)?

  技术图片

 简单的理解是同步队列存放着竞争同步资源的线程的引用(不是存放线程),而等待队列存放着待唤醒的线程的引用。

   常见操作:

  技术图片

    

   Throws Exception 类型的插入和取出在不能立即被执行的时候就会抛出异常。

   Special Value 类型的插入和取出在不能被立即执行的情况下会返回一个特殊的值(true 或者 false)。

   Blocked 类型的插入和取出操作在不能被立即执行的时候会阻塞线程直到可以操作的时候会被其他线程唤醒。

   Timed out 类型的插入和取出操作在不能立即执行的时候会被阻塞一定的时候,如果在指定的时间内没有被执行,那么会返回一个特殊值。

15. 什么是队列同步器AQS,底层原理? 

   队列同步器(AQS):是用来构建锁或者其他同步组件的基础框架。

  1. 用了一个int成员变量表示同步状态。

  getState(): 获取当前同步状态

  setState(int newState): 设置当前同步状态

  compareAndSetState(int expect, int update): CAS设置当前状态,保证原子性

  2. 通过内置的FIFO双向队列来完成获取锁线程的排队工作。

  2.1. 同步器包含两个节点类型的应用,一个指向头节点,一个指向尾节点,未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。

       同步队列遵循FIFO,首节点是获取同步状态成功的节点。

       技术图片

       未获取到锁的线程将创建一个节点,设置到尾节点。

       技术图片

       首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。

        技术图片

        3. 独占式/共享式锁获取

        独占式:有且只有一个线程能获取到锁,如:ReentrantLock

        独占锁获取流程:

        技术图片

       共享式:可以多个线程同时获取到锁,如:CountDownLatch, CyclicBarrier。

      共享锁获取流程:

       技术图片

 

 

16. ReentrantLock重入锁?

  可以防止死锁。

  1) lock()

    获取锁三种情况:

    锁空闲:直接获取锁并返回,同时设置锁持有者数量为:1;

    当前线程持有锁:直接获取锁并返回,同时锁持有者数量递增1;

    其他线程持有锁:当前线程会休眠等待,直至获取锁为止;

   2) lockInterruptibly()

    获取锁,逻辑和 lock() 方法一样,但这个方法在获取锁过程中能响应中断。

   3) tryLock()

    在尝试获取锁,获取成功返回:true,获取失败返回:false, 这个方法不会等待。

 

17. Synchronized 与 ReentrantLock 的区别?

  可重入性:

     两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

  锁的实现:

    Synchronized是依赖于JVM实现的,而ReentrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。

  性能的区别:

    在Synchronized优化以前,synchronized的性能是比ReentrantLock差很多的,

    但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了。

    在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReentrantLock中的CAS技术,

    都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

  功能区别:

    便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,

    而ReentrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

    锁的细粒度和灵活度:很明显ReentrantLock优于Synchronized。

  ReentrantLock独有的能力:(也就是什么情况下应该使用ReentrantLock)

    1.ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

    2.ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

    3.ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

以上是关于[Java复习05] 多线程&并发 知识点补充的主要内容,如果未能解决你的问题,请参考以下文章

JAVA复习笔记之多线程并发

和朱晔一起复习Java并发:线程池

[Java复习] 多线程 并发 JUC 补充

Java复习——多线程与并发库

复习多线程相关知识

复习多线程相关知识