公众号(Java工程师八股文)                Java基础(背诵版)                         Java集合高频(背诵版)                               计算机网络(背诵版)                   Mysql(背诵版)                             jvm虚拟机(背诵版)              1.什么是多线程,多线程的优劣?*    多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。    多线程的好处:      可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。      多线程的劣势:          线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;              多线程需要协调和管理,所以需要 CPU 时间跟踪线程;              线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。         2.进程与线程的区别 ***    根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位   资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。   包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。   内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的   影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。   执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。   3.什么是上下文切换?       多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。      4.什么是线程死锁?***       死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程。      形成死锁的四个必要条件是什么      互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放       请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。       不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。       循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞      如何避免线程死锁?   破坏互斥条件(不能)   破坏请求与保持条件      一次性申请所有的资源。      破坏不剥夺条件      占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。      破坏循环等待条件      靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。      5.创建线程的四种方式?***       继承 Thread 类;       实现 Runnable 接口;       实现 Callable 接口;       使用 Executors 工具类创建线程池      继承 Thread 类**步骤**      定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法       创建自定义的线程子类对象       调用子类实例的star()方法来启动线程      实现 Runnable 接口**步骤**:      定义Runnable接口实现类MyRunnable,并重写run()方法       创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象       调用线程对象的start()方法      实现 Callable 接口**步骤**      创建实现Callable接口的类myCallable       以myCallable为参数创建FutureTask对象       将FutureTask作为参数创建Thread对象       调用线程对象的start()方法      使用 Executors 工具类创建线程池      Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool      runnable 和 callable 有什么区别?       Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果       Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息      什么是 FutureTask      FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装      6.线程的生命周期和状态***          线程创建之后它将处于 NEW(新建) 状态,调⽤ start() ⽅法后开始运⾏, 线程这时候处于 READY(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间⽚(timeslice) 后就处于 RUNNING(运⾏) 状态。当线程执⾏ wait() ⽅法之后,线程进⼊ WAITING(等待) 状态。进⼊等待状态的线程需要依靠 其他线程的通知才能够返回到运⾏状态,⽽ TIME_WAITING(超时等待) 状态相当于在等待状态 的基础上增加了超时限制,⽐如通过 sleep⽅法或 wait ⽅法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状 态。当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞) 状态。线程在执⾏ Runnable 的 run() ⽅法之后将会进⼊到 TERMINATED(终⽌) 状态         7.sleep() 和 wait() 有什么区别?***           两者都可以暂停线程的执行              类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。              是否释放锁:sleep() 不释放锁;wait() 释放锁。              用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。              用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。         8.线程的sleep()方法和yield()方法有什么不同? * *       sleep()方***使得当前线程暂停指定的时间,没有消耗CPU时间片。       sleep()使得线程进入到阻塞状态,yield()只是对CPU进行提示,如果CPU没有忽略这个提示,会使得线程上下文的切换,进入到就绪状态。       sleep()一定会完成给定的休眠时间,yield()不一定能完成。       sleep()需要抛出InterruptedException,而yield()方法无需抛出异常。      9.如何停止一个正在运行的线程?*    在java中有以下3种方法可以终止正在运行的线程:       使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。       使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。       使用interrupt方法中断线程。(interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。)      10.Java 中你怎样唤醒一个阻塞的线程?*    首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行;    其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。    11.notify() 和 notifyAll() 有什么区别?*       如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。       notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。      12.什么是线程同步?什么是线程互斥?他们是如何实现的? * * *       线程的互斥是指某一个资源只能被一个访问者访问,具有唯一性和排他性。但访问者对资源访问的顺序是乱序的。       线程的同步是指在互斥的基础上使得访问者对资源进行有序访问。      线程同步的实现方法:       同步方法       同步代码块       wait()和notify()       使用volatile实现线程同步       使用重入锁实现线程同步       使用局部变量实现线程同步       使用阻塞队列实现线程同步      13.什么叫线程安全?servlet 是线程安全吗?**    线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。           Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。              Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。              SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。              Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。         14.Java 程序中怎么保证多线程的运行安全?***    线程的安全性问题体现在:          原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性              可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到              有序性:程序执行的顺序按照代码的先后顺序执行          导致原因:              缓存导致的可见性问题              线程切换带来的原子性问题              编译优化带来的有序性问题          解决办法:              JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题              synchronized、volatile、LOCK,可以解决可见性问题              Happens-Before 规则可以解决有序性问题          Happens-Before 规则如下:              程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作              管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作              volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作              线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作              线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测              线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生              对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始         15.一个线程运行时发生异常会怎样?    如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。    16.Java 线程数过多会造成什么异常?*           线程的生命周期开销非常高              消耗过多的 CPU         资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。       降低稳定性JVM      在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。    16.synchronized***    synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。   **    怎么使用 synchronized 关键字,在项目中用到了吗?    **    **synchronized关键字最主要的三种使用方式:       修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁       修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。       修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。      总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。    17.synchronized 同步语句块的情况***       synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。      synchronized 修饰方法的的情况       synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。      synchronized可重入的原理      重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。      多线程中 synchronized 锁升级的原理是什么?   **       synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。      18.synchronized 和 Lock 有什么区别?***       synchronized是Java内置关键字,在JVM层面,Lock是个Java类;       synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。       synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。       通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。      19.synchronized 和 ReentrantLock 区别是什么?(synchronized说上上面的修饰)***       synchronized 是关键字,ReentrantLock 是类。 ReentrantLock 是类,它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量       synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。      相同点:两者都是可重入锁   两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。    主要区别:      ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;       ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;       ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。       二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word。      20.volatile***    volatile 关键字的作用          对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。              从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性。         volatile实现内存可见性原理?**       导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致所导致,例如上面所说线程A访问自己本地内存A的X值时,但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是一个脏数据。那如何解决这种问题呢?     volatile可以保证内存可见性的关键是volatile的读/写实现了缓存一致性,缓存一致性的主要内容为:       每个处理器会通过嗅探总线上的数据来查看自己的数据是否过期,一旦处理器发现自己缓存对应的内存地址被修改,就会将当前处理器的缓存设为无效状态。此时,如果处理器需要获取这个数据需重新从主内存将其读取到本地内存。       当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为无效状态。      那缓存一致性是如何实现的呢?可以发现通过volatile修饰的变量,生成汇编指令时会比普通的变量多出一个Lock指令,这个Lock指令就是volatile关键字可以保证内存可见性的关键,它主要有两个作用:       将当前处理器缓存的数据刷新到主内存。       刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效。      volatile实现有序性原理       重排序可以提高代码的执行效率,但在多线程程序中可以导致程序的运行结果不正确,那volatile是如何解决这一问题的呢?     为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障来禁止指令重排序。   内存屏障:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行。    volatile 变量和 atomic 变量有什么不同?          volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。              而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方***原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。         volatile 能使得一个非原子操作变成原子操作吗?          关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。              volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。         21.synchronized 和 volatile 的区别是什么?***       synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。       volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。      区别          volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。              volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。              volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。              volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。              volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。         22.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?***           悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。              乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。         乐观锁的实现方式:          使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。              java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。         什么是 CAS          CAS 是一种基于锁的操作,而且是乐观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。              CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。         CAS 的会产生什么问题? 1、ABA 问题:   比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。两种方法:从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。ABA的解决方法也很简单,就是利用版本号。给变量加上一个版本号,每次变量更新的时候就把版本号加1,这样即使E的值从A—>B—>A,版本号也发生了变化,这样就解决了CAS出现的ABA问题    2、循环时间长开销大:   对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。    3、只能保证一个共享变量的原子操作:   当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。    23.AQS 介绍**      AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器      AQS核心思想是:   如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。    CLH队列是一个虚拟的双向队列,这个队列是通过CLH队列实现的,从上图可以看出,该队列是一个双向队列,有Node结点组成,每个Node结点维护一个prev引用和next引用,这两个引用分别指向自己结点的前驱结点和后继结点,同时AQS还维护两个指针Head和Tail,分别指向队列的头部和尾部。      AQS定义两种资源共享方式          Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:              公平锁:按照线程在队列中的排队顺序,先到者先拿到锁              非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的              Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。         24.ReentrantLock(重入锁)实现原理?**           ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。              要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。              ReentrantLock的可重入功能基于AQS的同步状态:state。          其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。         25.ConcurrentHashMap***(一般就问实现原理线程安全)    ConcurrentHashMap在JDK1.7和JDK1.8版本中的区别?           实现结构上的不同,JDK1.7是基于Segment实现的,JDK1.8是基于Node数组+链表/红黑树实现的。              保证线程安全方面:JDK1.7采用了分段锁的机制,当一个线程占用锁时,会锁住一个Segment对象,不会影响其他Segment对象。JDK1.8则是采用了CAS和synchronize的方式来保证线程安全。              在存取数据方面:          JDK1.7中的put()方法:               先计算出key的hash值,利用hash值对segment数组取余找到对应的segment对象。             尝试获取锁,失败则自旋直至成功,获取到锁,通过计算的hash值对hashentry数组进行取余,找到对应的entry对象。             遍历链表,查找对应的key值,如果找到则将旧的value直接覆盖,如果没有找到,则添加到链表中。(JDK1.7是插入到链表头部,JDK1.8是插入到链表尾部,这里可以思考一下为什么这样)              JDK1.8中的put()方法:            计算key值的hash值,找到对应的Node,如果当前位置为空则可以直接写入数据。       利用CAS尝试写入,如果失败则自旋直至成功,如果都不满足,则利用synchronized锁写入数据。      SynchronizedMap 和 ConcurrentHashMap 有什么区别?          SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。              ConcurrentHashMap 使用分段锁来保证在多线程下的性能。              ConcurrentHashMap 中则是一次锁住一个桶。         ConcurrentHashMap有什么缺点?    因为ConcurrentHashMap在更新数据时只会锁住部分数据,并不会将整个表锁住,读取的时候也并不能保证读取到最近的更新,只能保证读取到已经顺利插入的数据。    ConcurrentHashMap默认初始容量是多少?每次扩容为原来的几倍?    默认的初始容量为16,每次扩容为之前的两倍。    26.ThreadLocal***   **ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。    原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。    ThreadLocal造成内存泄漏的原因?      ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法      ThreadLocal内存泄漏解决方案?      每次使用完ThreadLocal,都调用它的remove()方法,清除数据。       在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。      27.BlockingQueue阻塞队列**    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。    这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。    ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。   LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。   PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。   DelayQueue:一个使用优先级队列实现的无界阻塞队列。   SynchronousQueue:一个不存储元素的阻塞队列。   LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。   LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。   阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。    28.线程池***   **    什么是线程池?为什么使用线程池    线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交给线程池来管理。    为什么使用线程池?      降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。       提高响应速度,当任务到达时,任务可以不需要等到线程创建就立即执行。       提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一分配。      线程池实现原理      创建线程池的几种方式?   (1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。   (2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。    (3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。    (4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。   线程池都有哪些状态? RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。 SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。 STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。 TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。 TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。    ThreadPoolExecutor构造函数的重要参数分析    三个比较重要的参数:      corePoolSize :核心线程数,定义了最小可以同时运行的线程数量。       maximumPoolSize :线程中允许存在的最大工作线程数量       workQueue:存放任务的阻塞队列。新来的任务会先判断当前运行的线程数是否到达核心线程数,如果到达的话,任务就会先放到阻塞队列。      其他参数:      keepAliveTime:当线程池中的数量大于核心线程数时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到时间超过keepAliveTime时才会被销毁。       unit :keepAliveTime参数的时间单位。       threadFactory:为线程池提供创建新线程的线程工厂。       handler :线程池任务队列超过maxinumPoolSize之后的拒绝策略      ThreadPoolExecutor的饱和策略(拒绝策略)    当同时运行的线程数量达到最大线程数量并且阻塞队列也已经放满了任务时,ThreadPoolExecutor会指定一些饱和策略。主要有以下四种类型:       AbortPolicy策略:该策略会直接抛出异常拒绝新任务       CallerRunsPolicy策略:当线程池无法处理当前任务时,会将该任务交由提交任务的线程来执行。       DiscardPolicy策略:直接丢弃新任务。       DiscardOleddestPolicy策略:丢弃最早的未处理的任务请求。      Executors和ThreaPoolExecutor创建线程池的区别   newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。    newCachedThreadPool 和 newScheduledThreadPool:    主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。    29.execute()和submit()的区别主要有两点:       execute()方法只能执行Runnable类型的任务。submit()方法可以执行Runnable和Callable类型的任务。       submit()方法可以返回持有计算结果的Future对象,同时还可以抛出异常,而execute()方法不可以。      30.Atomic 原子类*           原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。              处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。              原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。         JUC包中的4种原子类          基本类型:使用原子的方式更新基本类型               AtomicInteger:整形原子类             AtomicLong:长整型原子类             AtomicBoolean:布尔型原子类                  数组类型:使用原子的方式更新数组里的某个元素               AtomicIntegerArray:整形数组原子类             AtomicLongArray:长整形数组原子类             AtomicReferenceArray:引用类型数组原子类                  引用类型:               AtomicReference:引用类型原子类,存在ABA问题             AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。             AtomicMarkableReference:原子更新带有标记位的引用类型                  原子更新字段类              AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。              AtomicLongFieldUpdater:原子更新长整型字段的更新器。              AtomicReferenceFieldUpdater:引用类型更新器原子类         ** atomic 的原理?**    Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。    AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。    31.常用的并发工具类有哪些?*       Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。       CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。       CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。      **    **    **    ** 
点赞 28
评论 0
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务