zoukankan      html  css  js  c++  java
  • 面经总结:多线程

    • 多线程的实现?

    三种方法:1.继承Thread类;2.实现Runnable接口;3.使用Executor创建线程池;

    • 多线程的的同步/线程安全的方式?

    (1)同步方法:synchronized修饰的方法;

    (2)同步代码块:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

      同步方法和同步代码块的区别是什么?

      答:同步方法默认用this或者当前类class对象作为锁; 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。

    (3)使用volatile实现同步:每次线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。

    (4)使用重入锁实现线程同步:ReentrantLock是concurrent包的类;常用方法有lock()和unlock();可以创建公平锁;支持非阻塞的tryLock(可超时);需要手动释放锁。

    (5)使用ThreadLocal实现线程同步:每个线程都创建一个变量副本,修改副本不会影响其他线程的副本。ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量)。

    • 多线程的优化?

    影响多线程性能的问题:死锁、过多串行化、过多锁竞争等;

    预防和处理死锁的方法:

      1)尽量不要在释放锁之前竞争其他锁;一般可以通过细化同步方法来实现;

      2)顺序索取锁资源;

      3)尝试定时锁tryLock();

    降低锁竞争方法:

      1)缩小锁的范围,减小锁的粒度;

      2)使用读写分离锁ReadWriteLock来替换独占锁:来实现读-读并发,读-写串行,写-写串行的特性。这种方式更进一步提高了可并发性,因为有些场景大部分是读操作,因此没必要串行工作。

    • 线程池有哪几种?有哪些参数?

    1.创建;2.四类线程池;3.参数;

    创建:线程池的顶级接口是Executor,是执行线程的工具;真正的线程接口是ExecutorService;

    ThreadPoolExecutor是ExecutorService的默认实现;

    示例:

    ExecutorService pool=Executors.newFixedThreadPool(2);

    四类线程池: 

    1. newSingleThreadExecutor

    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    2.newFixedThreadPool

    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

    3. newCachedThreadPool

    创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    4.newScheduledThreadPool

    创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

    线程池ThreadPoolExecutor的参数:

    corePoolSize - 池中所保存的线程数,包括空闲线程。

    maximumPoolSize-池中允许的最大线程数。

    keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

    unit - keepAliveTime 参数的时间单位。

    workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。

    threadFactory - 执行程序创建新线程时使用的工厂。

    handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

    关于corePoolSize和maximumPoolSize:

    如果线程数<corePoolSize,有任务时,无论是否有空闲线程,都会创建新线程;

    如果corePoolSize<线程数<maximumPoolSize,大于的部分放到任务队列,直到队列满了, 才会创建小于等于maximumPoolSize的线程。

     

    • 线程的状态有哪些?

    根据java.lang.Thread.State类,线程包括六个状态:

    NEW、RUNNABLE、BLOCKED、WAITTING、TIME_WAITTING、TERMINATED

    NEW:线程实例化还未执行start();

    RUNNABLE:线程已经在JVM执行。(根据是否取得CPU时间片对应操作系统的Running和Ready状态;)

    BLOCKED:线程阻塞;等待锁,

    WAITTING:线程无限期等待;调用Object.wait() 没有timeout 或者 Thread.join() 没有timeout 时进入该状态;

    TIMED_WAITTING:线程限期等待;调用Thread.sleep、Object.wait(timeout) 或者 Thread.join(timeout)是进入该状态;

    TERMINATED:线程终止;

    (相比较原版本,DEAD更改为TERMINATED,没有Running状态,有WAITTING和TIMED_WAITTING状态;)

    状态变化:

    (和原版本的区别,没有Running状态;等待不属于阻塞、sleep/join不属于阻塞!)

     Runnable:t.start();从new变为Runnable;

     Waitting:Runnable执行wait()/join();无限等待唤醒;

     Timed_Waitting:Runnable执行sleep()/wait(timeout)/join(timeout); wait释放锁,sleep()不释放锁;等待唤醒,但设置了时限;

     Blocked:线程在等待锁;

    • wait和sleep区别?

    1.属性;2.锁;3.范围;
      1)属性:wait()是Object的方法,而sleep()是线程类Thread的方法;
      2)锁:wait会放弃对象锁,进入等待队列,只有调用了notify(),才会结束wait状态,参与竞争同步资源锁;而sleep()只让出了cpu,而不会释放同步资源锁,sleep指定时间后CPU再回到该线程继续往下执行;
      3)范围:sleep可以在任何地方使用,wait只能在同步方法或同步块中使用;
     
    • volatile的作用?

    在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 

      要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。 

      volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 

      使用volatile关键字修饰变量,线程要访问变量时都是从内存中读取,而不是从缓存当中读取,因此每个线程访问到的变量值都是一样的。

     

    • synchronized和lock的区别?

    1.属性;2.功能-公平非公平/trylock;3.释放;4.线程交互
      1)lock是接口,syn是Java中的关键字;
      2)lock的tryLock方法可以选择性获取锁,能够规避死锁;而syn会一直获取下去;
      3)syn发生异常或者同步块结束的时候,会自动释放锁;而Lock必须手动释放unlock;
      4)syn线程交互,用到的是同步对象的wait/notify/notifyAll方法;lock得到一个Condition对象,调用Condition对象的await/signal/signalAll;

    tryLock(),避免死锁;synchronized是不公平锁,而Lock可以指定锁公平还是不公平;

    syn实现wait/notify机制通知的线程是随机的,Lock可以有选择性的通知。

    • 并发过程需要保证的特性?

    并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    原子性:一组操作要么全部完成,要么全部不完成;

    可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值;

    有序性:并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。

     
     
    • synchronized和volatile的内存可见性和原子性?

    可见性与原子性
       可见性:一个线程对共享变量的修改,更够及时的被其他线程看到
       原子性:即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性。类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤:
    ① 取出a和b
    ② 计算a+b
    ③ 将计算结果写入内存
    (1)Synchronized:保证可见性和原子性
        Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
    (2)Volatile:保证可见性,但不保证操作的原子性
        Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作前加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证volatile变量的原子性。
     
    • 读写锁的优势?

    ReentrantReadWriteLock:读写各用一把锁;对于读多写少场景效率高。

    读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁。我把这两个操作理解为三句话:

    1、读和读之间不互斥,因为读操作不会有线程安全问题

    2、写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题

    3、读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题

    总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

    • ThreadLocal的作用?

    作用:ThreadLocal的作用是为每个线程创建一个变量副本,每个线程可以修改自己所拥有的变量副本,而不会影响其他线程的副本,从而实现线程安全。
    实现方式:ThreadLocal内置一个map,key表示当前线程,value是对应的值,实现线程变量私有化。
    比较:ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量)。
     
    • Java内存模型?

    PS:JVM内存模型和JMM(Java内存模型)没有关系。JMM的目的是为了解决Java多线程对共享数据的读写一致性问题。

    Java内存模型(Java Memory Model,JMM)

    Java内存模型规定:

    所有的变量都存储在主内存中;

    每个线程有自己的工作内存,线程的工作内存保存被线程使用到变量的主内存副本拷贝;

    线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量;

    不同线程之间不能直接访问对方工作内存中的变量,线程间变量值的传递通过主内存来完成。

    每个线程都有自己的执行空间(即工作内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存;
     
    工作内存可类比高速缓存,为了获得更好的执行性能。
     

    内存间交互操作

    Java内存模型定义了八种操作:

    (lock/unlock、read/load主内存-工作内存、use/assign工作内存-执行引擎、store/write工作内存-主内存)

    lock(锁定):作用于主内存的变量,它把一个变量标识为一个线程独占的状态;

    unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

    read(读取):作用于主内存的变量,它把一个变量的值从主内存传送到线程中的工作内存,以便随后的load动作使用;

    load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;

    use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎;

    assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量;

    store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作;

    write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值写入主内存的变量中。

    操作时规则

    不允许read和load、store和write操作之一单独出现;

    说明:如果要把一个变量从主内存复制到工作内存,那要顺序执行read和load操作;如果要把变量从工作内存同步回主内存,就要顺序执行store和write操作。Java内存模型要求上述连个操作必须按顺序执行,但不必须连续执行。

    不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把变化同步回主内存;

    不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存;

    一个新变量只能在主内存诞生,不允许在工作内存直接使用一个为被初始化(load或assign)的变量,即在对一个变量实施use和store操作之前,必须先执行assign和load操作;

    一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才被解锁;

    如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值;

    如果一个变量事先没有被lock操作锁定,则不允许对他执行unlock操作;也不允许去unlock一个被其他线程锁定住的变量;

    对一个变量执行unlock操作之前,必须先把此变量同步回主内存。

    volatile

    关键字volatile是JVM提供最轻量级的同步机制。

    当一个变量被定义成volatile后,它具有两个特性:

    第一,volatile变量对所有线程是立即可见的,对于volatile变量所有的写操作都能立刻反应到其他线程之中;但volatile变量只能保证可见性,不能保证在并发下是安全的。

    第二,禁止指令重排序优化。

    Java模型特性

    原子性:read、load、assign、use、store和write保证原子性变量操作;lock和unlock通过synchronized语法来加锁,在synchronized块之间的操作也具有原子性;

    可见性:volatile、synchronized和final实现变量修改值在其他线程可见;synchronized可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得的;final可见性是“被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程就能看到final字段的值。”

    有序性:Java语言提供volatile和synchronized两个关键字来保证线程之间的有序性;volatile本身禁止指令重排序;synchronized是由“一个变量在同一个时刻只允许一条线程对其进行lock操作“这条规则获得的。

     
     
    • Concurren下的类?

    Concurrent包下的类包括:
    并发集合类:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、ArrayBlockingQueue、LinkedBlockingQueue;
    原子类:AtomicInteger
    线程池:ThreadPoolExecutor、Executor;
  • 相关阅读:
    实现Maven自动下载源代码包并关联
    Maven3入门篇
    小典故:为什么数组的索引总是从0开始,而不是1?
    C语言算法探究之(一):算法的准确性
    C语言算法探究之(二):算法的准确性
    Visual Studio对无用引用(unused using)的处理方法
    C# CRC8的实现(原创)
    C#4.0:新功能和展望
    C#控件重绘学习(一)
    双加号(++)在C#中的用法解释
  • 原文地址:https://www.cnblogs.com/buwenyuwu/p/7225382.html
Copyright © 2011-2022 走看看