当前位置: 首页 > news >正文

做特产网站的原因深圳禅城网站设计

做特产网站的原因,深圳禅城网站设计,用php做的网站怎么上传,苏州百姓网toc Java并发编程面试题(60道) 基础 1.并行跟并发有什么区别#xff1f; 从操作系统的角度来看#xff0c;线程是CPU分配的最小单位。 并行就是同一时刻#xff0c;两个线程都在执行。这就要求有两个CPU去分别执行两个线程。并发就是同一时刻#xff0c;只有一个执行 从操作系统的角度来看线程是CPU分配的最小单位。 并行就是同一时刻两个线程都在执行。这就要求有两个CPU去分别执行两个线程。并发就是同一时刻只有一个执行但是一个时间段内两个线程都执行了。并发的实现依赖于CPU切换线程因为切换的时间特别短所以基本对于用户是无感知的。 就好像我们去食堂打饭并行就是我们在多个窗口排队几个阿姨同时打菜并发就是我们挤在一个窗口阿姨给这个打一勺又手忙脚乱地给那个打一勺。 2.说说什么是进程和线程 要说线程必须得先说说进程。 进程进程是代码在数据集合上的一次运行活动是系统进行资源分配和调度的基本单位。线程线程是进程的一个执行路径一个进程中至少有一个线程进程中的多个线程共享进程的资源。 操作系统在分配资源时是把资源分配给进程的但是CPU资源比较特殊它是被分配到线程的因为真正要占用CPU运行的是线程所以也说线程是 CPU分配的基本单位。 比如在Java中当我们启动main函数其实就启动了一个JVM进程而main函数在的线程就是这个进程中的一个线程也称主线程。 一个进程中有多个线程多个线程共用进程的堆和方法区资源但是每个线程有自己的程序计数器和栈。 3.说说线程有几种创建方式 Java中创建线程主要有三种方式分别为继承Thread类、实现Runnable接口、实现Callable接口。 继承Thread类重写run() 方法调用start() 方法启动线程 public class ThreadTest {/*** 继承Thread类*/public static class MyThread extends Thread {Overridepublic void run() {System.out.println(This is child thread);}}public static void main(String[] args) {MyThread thread new MyThread();thread.start();} }实现Runnable接口重写run() 方法 public class RunnableTask implements Runnable {public void run() {System.out.println(Runnable!);}public static void main(String[] args) {RunnableTask task new RunnableTask();new Thread(task).start();} }上面两种都是没有返回值的但是如果我们需要获取线程的执行结果该怎么办呢 实现Callable接口重写call() 方法这种方式可以通过FutureTask获取任务执行的返回值 public class CallerTask implements CallableString {public String call() throws Exception {return Hello,i am running!;}public static void main(String[] args) {//创建异步任务FutureTaskString task new FutureTaskString(new CallerTask());//启动线程new Thread(task).start();try {//等待执行完成并获取返回结果String result task.get();System.out.println(result);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}} }4.为什么调用start()方法时会执行run()方法那怎么不直接调用run()方法 JVM执行start方法会先创建一条线程由创建出来的新线程去执行thread的run方法这才起到多线程的效果。 为什么我们不能直接调用run() 方法也很清楚如果直接调用Thread的run() 方法那么run方法还是运行在主线程中相当于顺序执行就起不到多线程的效果。 5.线程有哪些常用的调度方法 线程等待与通知 在Object类中有一些函数可以用于线程的等待与通知。 wait()当一个线程A调用一个共享变量的 wait()方法时线程A会被阻塞挂起发生下面几种情况才会返回 1线程A调用了共享对象notify()或者 notifyAll()方法 2其他线程调用了线程A的interrupt()方法线程A抛出InterruptedException异常返回。wait(long timeout)这个方法相比wait()方法多了一个超时参数它的不同之处在于如果线程A调用共享对象的wait(long timeout)方法后没有在指定的timeout ms时间内被其它线程唤醒那么这个方法还是会因为超时而返回。wait(long timeout, int nanos)其内部调用的是 wait(long timout)函数。 上面是线程等待的方法而唤醒线程主要是下面两个方法 notify()一个线程A调用共享对象的notify()方法后会唤醒一个在这个共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待具体唤醒哪个等待的线程是随机的。notifyAll()不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。 Thread类也提供了一个方法用于等待的方法 join()如果一个线程A执行了thread.join()语句其含义是当前线程A等待thread线程终止之后才从thread.join()返回。 线程休眠 sleep(long millis)Thread类中的静态方法当一个执行中的线程A调用了Thread的sleep方法后线程A会暂时让出指定时间的执行权但是线程A所拥有的监视器资源比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回接着参与CPU的调度获取到CPU资源后就可以继续运行。 让出优先权 yield()Thread类中的静态方法当一个线程调用 yield方法时实际就是在暗示线程调度器当前线程请求让出自己的CPU但是线程调度器可以无条件忽略这个暗示。 线程中断 Java中的线程中断是一种线程间的协作模式通过设置线程的中断标志并不能直接终止该线程的执行而是被中断的线程根据中断状态自行处理。 void interrupt()中断线程例如当线程A运行时线程B可以调用线程interrupt()方法来设置线程的中断标志为true 并立即返回。设置标志仅仅是设置标志线程A实际并没有被中断会继续往下执行。boolean isInterrupted()方法检测当前线程是否被中断。boolean interrupted()方法检测当前线程是否被中断与 isinterrupted 不同的是该方法如果发现当前线程被中断则会清除中断标志。 6.线程有几种状态 在Java中线程共有六种状态 线程在自身的生命周期中并不是固定地处于某个状态而是随着代码的执行在不同的状态之间进行切换Java线程状态变化如图示 7.什么是线程上下文切换 使用多线程的目的是为了充分利用CPU但是我们知道并发其实是一个CPU来应付多个线程。 为了让用户感觉多个线程是在同时执行的CPU资源的分配采用了时间片轮转也就是给每个线程分配一个时间片线程在时间片内占用CPU执行任务。当线程使用完时间片后就会处于就绪状态并让出CPU让其他线程占用这就是上下文切换。 8.守护线程了解吗 Java中的线程分为两类分别为daemon线程守护线程和user线程用户线程。 用户线程是虚拟机启动的线程中的普通线程当所有用户线程结束运行后虚拟机才会停止运行即使还有一些守护线程在运行。 守护线程是在程序中创建的线程它的作用是为其他线程提供服务。当所有的用户线程结束运行后守护线程也会随之结束而不管它是否执行完毕。守护线程通常用于执行一些辅助性任务如垃圾回收、缓存清理等它们不需要等待所有的任务完成后再退出。 它们之间的区别在于虚拟机在何时结束进程。 那么守护线程和用户线程有什么区别呢 当最后一个非守护线程束时JVM会正常退出而不管当前是否存在守护线程也就是说守护线程是否结束并不影响JVM退出。 换而言之只要有一个用户线程还没结束正常情况下JVM就不会退出。 9.线程间有哪些通信方式 volatile和synchronized关键字: 关键字volatile可以用来修饰字段成员变量就是告知程序任何对该变量的访问均需要从共享内存中获取而对它的改变必须同步刷新回共享内存它能保证所有线程对变量访问的可见性。 关键字synchronized可以修饰方法或者以同步块的形式来进行使用它主要确保多个线程在同一个时刻只能有一个线程处于方法或者同步块中它保证了线程对变量访问的可见性和排他性。 等待/通知机制: 可以通过Java内置的等待/通知机制(wait()/notify())实现一个线程修改一个对象的值而另一个线程感知到了变化然后进行相应的操作。 管道输入/输出流 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于它主要用于线程之间的数据传输而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现PipedOutputStreamPipedInputStream、PipedReader和PipedWriter前两种面向字节而后两种面向字符。 使用Thread.join(): Thread.join()join()的作用是“等待该进程终止”也就是在子线程调用了join()方法后主线程后面的代码要等到子线程结束了才能执行。一般应用于一个线程的输入可能依赖于另一个或者多个线程的输出此时这个线程就需要等待依赖线程执行完毕才能继续执行。 如果一个线程A执行了thread.join()语句其含义是当前线程A等待thread线程终止之后才从thread.join()返回。 线程Thread除了提供join()方法之外还提供了join(long millis)和join(long millisint nanos)两个具备超时特性的方法。 使用ThreadLocal: ThreadLocal即线程变量是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T) 方法来设置一个值在当前线程下再通过get() 方法获取到原先设置的值。 关于多线程其实很大概率还会出一些笔试题比如交替打印、银行转账、生产消费模型等等 ThreadLocal ThreadLocal其实应用场景不是很多但却是被炸了千百遍的面试老油条涉及到多线程、数据结构、JVM可问的点比较多一定要拿下。 10.ThreadLocal是什么 ThreadLocal也就是线程本地变量。如果你创建了一个ThreadLocal变量那么访问这个变量的每个线程都会有这个变量的一个本地拷贝多个线程操作这个变量的时候实际是操作自己本地内存里面的变量从而起到线程隔离的作用避免了线程安全问题。 创建 创建了一个ThreadLoca变量localVariable任何一个线程都能并发访问localVariable。 //创建一个ThreadLocal变量 public static ThreadLocalString localVariable new ThreadLocal();写入 线程可以在任何地方使用localVariable写入变量。 localVariable.set(鄙人某某”);读取 线程在任何地方读取的都是它写入的变量。 localVariable.get();11.你在工作中用到过ThreadLocal吗 有用到过的用来做用户信息上下文的存储。 我们的系统应用是一个典型的MVC架构登录后的用户每次访问接口都会在请求头中携带一个token在控制层可以根据这个token解析出用户的基本信息。那么问题来了假如在服务层和持久层都要用到用户信息比如rpc调用、更新用户获取等等那应该怎么办呢 一种办法是显式定义用户相关的参数比如账号、用户名….….这样一来我们可能需要大面积地修改代码多少有点瓜皮那该怎么办呢 这时候我们就可以用到ThreadLocal在控制层拦截请求把用户信息存入ThreadLocal这样我们在任何一个地方都可以取出ThreadLocal中存的用户数据。 很多其它场景的cookiesession等等数据隔离也都可以通过ThreadLocal去实现。 我们常用的数据库连接池也用到了ThreadLocal 数据库连接池的连接交给ThreadLoca进行管理保证当前线程的操作都是同一个Connnection。 12.ThreadLocal怎么实现的呢 我们看一下ThreadLocal的set(T) 方法发现先获取到当前线程再获取ThreadLocalMap然后把元素存到这个map中。 public void set(T value) {//获取当前线程Thread t Thread.currentThread();//获取ThreadLocalMapThreadLocalMap map getMap(t);//将当前元素存入mapif (map ! null)map.set(this, value);elsecreateMap(t, value); }ThreadLocal实现的秘密都在这个ThreadLocalMap了可以Thread类中定义了一个类型为ThreadLocal.ThreadLocalMap的成员变量 threadLocals。 public class Thread implements Runnable {//ThreadLocal.ThreadLocalMap是Thread的属性ThreadLocal.ThreadLocalMap threadLocals null; }ThreadLocalMap既然被称为Map那么毫无疑问它是keyvalue型的数据结构。我们都知道map的本质是一个个keyvalue形式的节点组成的数组那ThreadLocalMap的节点是什么样的呢 static class Entry extends WeakReferenceThreadLocal? {/** The value associated with this ThreadLocal. */Object value;//节点类Entry(ThreadLocal? k, Object v) {//key赋值super(k);//value赋值value v;} }这里的节点key可以简单低视作ThreadLocalvalue为代码中放入的值当然实际上key并不是ThreadLocal本身而是它的一个弱引用可以看到Entry的key继承了WeakReference弱引用再来看一下key怎么赋值的 public WeakReference(T referent) {super(referent); }key的赋值使用的是WeakReference的赋值。 所以怎么回答ThreadLocal原理要答出这几个点 Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals每个线程都有一个属于自己的ThreadLocalMap。ThreadLocalMap内部维护着Entry数组每个Entry代表一个完整的对象key是ThreadLocal的弱引用value是ThreadLocal的泛型值。每个线程在往ThreadLocal里设置值的时候都是往自己的ThreadLocalMap里存读也是以某个ThreadLocal作为引用在自己的map里找对应的key从而实现了线程隔离。ThreadLocal本身不存储值它只是作为一个key来让线程往ThreadLocalMap里存取值。 13.ThreadLocal 内存泄露是怎么回事 我们先来分析一下使用ThreadLocal时的内存我们都知道在JVM中栈内存线程私有存储了对象的引用堆内存线程共享存储了对象实例。 所以呢栈中存储了ThreadLocalThread的引用堆中存储了它们的具体实例。 ThreadLocalMap中使用的key为ThreadLocal的弱引用。 “弱引用只要垃圾回收机制一运行不管JVM的内存空间是否充足都会回收该对象占用的内存。” 那么现在问题就来了弱引用很容易被回收如果ThreadLocal(ThreadLocalMap的Key) 被垃圾回收器回收了但是ThreadLocalMap生命周期和Thread是一样的它这时候如果不被回收就会出现这种情况ThreadLocalMap的key没了value还在这就会造成了内存泄漏问题。 那怎么解决内存泄漏问题呢 很简单使用完ThreadLocal后及时调用remove方法释放内存空间。 ThreadLocalString localVariable new ThreadLocal(); try {localVariable.set(鄙人某某”);…… } finally {localVariable.remove(); }那为什么key还要设计成弱引用 key设计成弱引用同样是为了防止内存泄漏。 假如key被设计成强引用如果ThreadLocal Reference被销毁此时它指向ThreadLocal的强引用就没有了但是此时key还强引用指向ThreadLocal就会导致ThreadLocal不能被回收这时候就发生了内存泄漏的问题。 14.ThreadLocalMap的结构了解吗 ThreadLocalMap虽然被叫做Map其实它是没有实现Map接口的但是结构还是和HashMap比较类似的主要关注的是两个要素元素数组 和 散列方法。 元素数组 一个table数组存储Entry类型的元素Entry是ThreaLocal弱引用作为keyObject作为value的结构。 private Entry[] table;散列方法 散列方法就是怎么把对应的key映射到table数组的相应下标ThreadLocalMap用的是哈希取余法取出key的threadLocalHashCode然后和table数组长度减一运算相当于取余。 int i key.threadLocalHashCode (table.length - 1);这里的threadLocalHashCode计算有点东西每创建一个ThreadLocal对象它就会新增0x61c88647这个值很特殊它是斐波那契数也叫黄金分割数。hash增量为这个数字带来的好处就是hash分布非常均匀。 private static final int HASH_INCREMENT 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT); }15.ThreadLocalMap怎么解决Hash冲突的 我们可能都知道HashMap使用了链表来解决冲突也就是所谓的链地址法。 ThreadLocalMap没有使用链表自然也不是用链地址法来解决冲突了它用的是另外一种方式——开放定址法。 开放定址法是什么意思呢简单来说就是这个坑被人占了那就接着去找空着的坑。 如上图所示如果我们插入一个value27的数据通过hash计算后应该落入第4个槽位中而槽位4已经有了Entry数据而且Entry数据的key和当前不相等。此时就会线性向后查找一直找到Entry为null的槽位才会停止查找把元素放到空的槽中。 在get的时候也会根据ThreadLocal对象的hash值定位到table中的位置然后判断该槽位Entry对象中的key是否和get的key一致如果不一致就判断下一个位置。 16.ThreadLocalMap扩容机制了解吗 在ThreadLocalMap.set() 方法的最后如果执行完启发式清理工作后未清理到任何数据且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3)就开始执行rehash()逻辑 if (!cleanSomeSlots(i, sz) sz threshold)rehash();再着看rehash()具体实现这里会先去清理过期的Entry然后还要根据条件判断size threshold - threshold/4 也就是 size threshold * 3/4 来决定是否需要扩容resize()。 接着看看具体的resize() 方法扩容后的newTab的大小为老数组的两倍然后遍历老的table数组散列方法重新计算位置开放地址解决冲突然后放到新的newTab遍历完成之后oldTab中所有的entry数据都已经放入到newTab中了然后table引用指向newTab。 17.父子线程怎么共享数据 父线程能用ThreadLocal来给子线程传值吗毫无疑问不能。那该怎么办 这时候可以用到另外一个类——InheritableThreadLocal。 使用起来很简单在主线程的InheritableThreadLocal实例设置值在子线程中就可以拿到了。 public class InheritableThreadLocalTest {public static void main(String[] args) {final ThreadLocal threadLocal new InheritableThreadLocal();// 主线程threadLocal.set(不擅技术);//子线程Thread t new Thread() {Overridepublic void run() {super.run();System.out.println(鄙人某某 threadLocal.get());}};t.start();} }那原理是什么呢 原理很简单在Thread类里还有另外一个变量 ThreadLocal.ThreadLocalMap inheritableThreadLocals null;在Thread.init的时候如果父线程的inheritableThreadLocals不为空就把它赋给当前线程子线程的inheritableThreadLocals。 if (inheritThreadLocals parent.inheritableThreadLocals ! null)this.inheritableThreadLocals ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);Java 内存模型 18.说一下你对Java内存模型JMM的理解 Java内存模型Java Memory ModelJMM是一种抽象的模型被定义出来屏蔽各种硬件和操作系统的内存访问差异。 JMM定义了线程和主内存之间的抽象关系线程之间的共享变量存储在 主内存Main Memory中每个线程都有一个私有的 本地内存Local Memory本地内存中存储了该线程以读/写共享变量的副本。 本地内存是JMM的一个抽象概念并不真实存在。它其实涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 图里面的是一个双核 CPU 系统架构每个核有自己的控制器和运算器其中控制器包含一组寄存器和操作控制器运算器执行算术逻辅运算。 每个核都有自己的一级缓存在有些架构里面还有一个所有CPU共享的二级缓存。那么Java内存模型里面的工作内存就对应这里的 LI 缓存或者L2缓存或者CPU寄存器。 19.说说你对原子性、可见性、有序性的理解 原子性、有序性、可见性是并发编程中非常重要的基础概念JMM的很多技术都是围绕着这三大特性展开。 原子性原子性指的是一个操作是不可分割、不可中断的要么全部执行并且执行的过程不会被任何因素打断要么就全不执行。可见性可见性指的是一个线程修改了某一个共享变量的值时其它线程能够立即知道这个修改。有序性有序性指的是对于一个线程的执行代码从前往后依次执行单线程下可以认为程序是有序的但是并发时有可能会发生指令重排。 分析下面几行代码的原子性 int i 2; int j i; i; i i 1;第1句是基本类型赋值是原子性操作。第2句先读i的值再赋值到j两步操作不能保证原子性。第3和第4句其实是等效的先读取i的值再1最后赋值到i三步操作了不能保证原子性。 原子性、可见性、有序性都应该怎么保证呢 原子性JMM只能保证基本的原子性如果要保证一个代码块的原子性需要使用synchronized。可见性Java是利用volatile关键字来保证可见性的除此之外final和synchronized也能保证可见性。有序性synchronized或者volatile都可以保证多线程之间操作的有序性。 20.那说说什么是指令重排 在执行程序时为了提高性能编译器和处理器常常会对指令做重排序。重排序分3种类型: 编译器优化的重排序。编译器在不改变单线程程序语义的前提下可以重新安排语句的执行顺序。指令级并行的重排序。现代处理器采用了指令级并行技术Instruction-Level ParallelismILP来将多条指令重叠执行。如果不存在数据依赖性处理器可以改变语句对应机器指令的执行顺序。内存系统的重排序。由于处理器使用缓存和读/写缓冲区这使得加载和存储操作看上去可能是在乱序执行。 从Java源代码到最终实际执行的指令序列会分别经历下面3种重排序如图 我们比较熟悉的双重校验单例模式就是一个经典的指令重排的例子singleton instancenew Singleton(); 对应的JVM指令分为三步分配内存空间–初始化对象—对象指向分配的内存空间但是经过了编译器的指令重排序第二步和第三步就可能会重排序。 JMM属于语言级的内存模型它确保在不同的编译器和不同的处理器平台之上通过禁止特定类型的编译器重排序和处理器重排序为程序员提供一致的内存可见性保证。 21.指令重排有限制吗happens-before了解吗 指令重排也是有一些限制的有两个规则 happens-before 和 as-if-serial 来约束。 happens-before的定义 如果一个操作happens-before另一个操作那么第一个操作的执行结果将对第二个操作可见而且第一个操作的执行顺序排在第二个操作之前。 两个操作之间存在happens-before关系并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按happens-before关系来执行的结果一致那么这种重排序并不非法 happens-before和我们息息相关的有六大规则 程序顺序规则一个线程中的每个操作happens-before于该线程中的任意后续操作。监视器锁规则对一个锁的解锁happens-before于随后对这个锁的加锁。volatile变量规则对一个volatile域的写happens-before于任意后续对这个volatile域的读。传递性如果A happens-before B且B happens-before C那么A happens-before Cstart()规则如果线程A执行操作ThreadB.start()启动线程B那么A线程的ThreadB.start() 操作happens-before于线程B中的任意操作。join()规则如果线程A执行操作ThreadB.join()并成功返回那么线程B中的任意操作happens-before于线程A从ThreadB.join() 操作成功返回。 22.as-if-serial又是什么单线程的程序一定是顺序的吗 as-if-serial语义的意思是不管怎么重排序编译器和处理器为了提高并行度单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。 为了遵守as-if-serial语义编译器和处理器不会对存在数据依赖关系的操作做重排序因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系这些操作就可能被编译器和处理器重排序。为了具体说明请看下面计算圆面积的代码示例。 double pi 3.14; // A double r 1.0; // B double area pi * r * r; // C上面3个操作的数据依赖关系 A和C之间存在数据依赖关系同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中C不能被重排序到A和B的前面C排到A和B的前面程序的结果将会被改变。但A和B之间没有数据依赖关系编译器和处理器可以重排序A和B之间的执行顺序。 所以最终程序可能会有两种执行顺序 as-if-serial语义把单线程程序保护了起来遵守as-if-serial语义的编译器、runtime和处理器共同编织了这么一个楚门的世界单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程情况下我们不需要担心重排序的问题可见性的问题。 23.volatile实现原理了解吗 volatile有两个作用保证可见性和有序性。 volatile怎么保证可见性的呢 相比synchronized的加锁方式来解决共享变量的内存可见性问题volatile就是更轻量的选择它没有上下文切换的额外开销成本。 volatile可以确保对某个变量的更新对其他线程马上可见一个变量被声明为volatile时线程在写入变量时不会把值缓存在寄存器或者其他地方而是会把值刷新回主内存当其它线程读取该共享变量会从主内存重新获取最新值而不是使用当前线程的本地内存中的值。 例如我们声明一个volatile变量volatile int x 0线程A修改x1修改完之后就会把新的值刷新回主内存线程B读取x的时候就会清空本地内存变量然后再从主内存获取最新值。 volatile怎么保证有序性的呢 重排序可以分为编译器重排序和处理器重排序valatile保证有序性就是通过分别限制这两种类型的重排序。 为了实现volatile的内存语义编译器在生成字节码时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。 在每个volatile写操作的前面插入一个storestore屏障在每个volatile写操作的后面插入一个storeLoad屏障在每个volatile读操作的后面插入一个 LoadLoad 屏障在每个volatile读操作的后面插入一个LoadStore屏障 锁 24.synchronized用过吗怎么使用 synchronized经常用的用来保证代码的原子性。 synchronized主要有三种用法 修饰实例方法作用于当前对象实例加锁进入同步代码前要获得当前对象实例的锁 synchronized void method() {//业务代码 }修饰静态方法也就是给当前类加锁会作用于类的所有对象实例进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象是类成员static表明这是该类的一个静态资源不管new了多少个对象只有一份。 如果一个线程A调用一个实例对象的非静态synchronized方法而线程B需要调用这个实例对象所属类的静态synchronized方法是允许的不会发生互斥现象因为访问静态synchronized方法占用的锁是当前类的锁而访问非静态synchronized方法占用的锁是当前实例对象锁。 synchronized void staic method() {//业务代码 }修饰代码块指定加锁对象对给定对象/类加锁。synchronizedthis|object表示进入同步代码库前要获得给定对象的锁。synchronized类.class表示进入同步代码前要获得当前class的锁 synchronized(this) {//业务代码 }25.synchronized的实现原理 synchronized是怎么加锁的呢 我们使用synchronized的时候发现不用自己去lock和unlock是因为JVM帮我们把这个事情做了。 synchronized修饰代码块时JVM采用 monitorenter、monitorexit 两个指令来实现同步monitorenter指令指向同步代码块的开始位置monitorexit指令则指向同步代码块的结束位置。 反编译一段synchronized修饰代码块代码javap -c -s -v -l SynchronizedDemo.class可以看到相应的字节码指令。 synchronized修饰同步方法时JVM采用ACC_SYNCHRONIZED标记符来实现同步这个标识指明了该方法是一个同步方法。 同样可以写段代码反编译看一下。 synchronized锁住的是什么呢 monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。 实例对象结构里有对象头对象头里面有一块结构叫Mark WordMark Word指针指向了monitor。 所谓的Monitor其实是一种同步工具也可以说是一种同步机制。在Java虚拟机HotSpot中Monitor是由ObjectMonitor实现的可以叫做内部锁或者Monitor锁。 ObjectMonitor的工作原理 ObjectMonitor有两个队列WaitSet、EntryList用来保存ObjectWaiter 对象列表。_owner获取Monitor对象的线程进入_owner区时_count 1。如果线程调用了wait()方法此时会释放Monitor对象_owner恢复为空_count-1。同时该等待线程进入_WaitSet中等待被唤醒。 ObjectMonitor() {_header NULL;_count 0; // 记录线程获取锁的次数_waiters 0,_recursions 0; //锁的重入次数_object NULL;_owner NULL; // 指向持有ObjectMonitor对象的线程_WaitSet NULL; // 处于wait状态的线程会被加入到_WaitSet_WaitSetLock 0 ;_Responsible NULL ;_succ NULL ;_cxq NULL ;FreeNext NULL ;_EntryList NULL ; // 处于等待锁block状态的线程会被加入到该列表_SpinFreq 0 ;_SpinClock 0 ;OwnerIsThread 0 ; }可以类比一个去医院就诊的例子 首先患者在门诊大厅前台或自助挂号机进行挂号随后挂号结束后患者找到对应的诊室就诊 诊室每次只能有一个患者就诊如果此时诊室空闲直接进入就诊如果此时诊室内有其它患者就诊那么当前患者进入候诊室等待叫号 就诊结束后走出就诊室候诊室的下一位候诊患者进入就诊室。 这个过程就和Monitor机制比较相似 门诊大厅所有待进入的线程都必须先在入口Entry Set挂号才有资格就诊室就诊室**_Owner**里里只能有一个线程就诊就诊完线程就自行离开候诊室就诊室繁忙时进入等待区Wait Set就诊室空闲的时候就从**等待区Wait Set**叫新的线程 所以我们就知道了同步是锁住的什么东西 monitorenter在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的owner此时计数器 1。monitorexit当执行完退出后计数器-1归 0 后被其他进入的线程获得。 26.除了原子性synchronized可见性有序性可重入性怎么实现 synchronized怎么保证可见性 线程加锁前将清空工作内存中共享变量的值从而使用共享变量时需要从主内存中重新读取最新的值。线程加锁后其它线程无法获取主内存中的共享变量。线程解锁前必须把共享变量的最新值刷新到主内存中。 synchronized怎么保证有序性 synchronized同步的代码块具有排他性一次只能被一个线程拥有所以synchronized保证同一时刻代码是单线程执行的。 因为as-if-serial语义的存在单线程的程序能保证最终结果是有序的但是不保证不会指令重排。 所以synchronized保证的有序是执行结果的有序性而不是防止指令重排的有序性。 synchronized怎么实现可重入的呢 synchronized是可重入锁也就是说允许一个线程二次请求自己持有对象锁的临界资源这种情况称为可重入锁。 synchronized锁对象的时候有个计数器他会记录下线程获取锁的次数在执行完对应的代码块之后计数器就会-1直到计数器清零就释放锁了。 之所以是可重入的。是因为synchronized锁对象有个计数器会随着线程获取锁后1计数当线程执行完毕后-1直到清零释放锁。 27.锁升级synchronized优化了解吗 了解锁升级得先知道不同锁的状态是什么样的。这个状态指的是什么呢 Java对象头里有一块结构叫Mark word标记字段这块结构会随着锁的状态变化而变化。 64位虚拟机Mark Word是 64bit我们来看看它的状态变化 Mark Word存储对象自身的运行数据如**哈希码、GC分代年龄、锁状态标志、偏向时间戳Epoch**等。 synchronized做了哪些优化 在JDK1.6之前synchronized的实现直接调用ObjectMonitor的enter和exit这种锁被称之为重量级锁。从JDK6开始HotSpot虚拟机开发团队对Java中的锁进行优化如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略提升了synchronized的性能。 偏向锁在无竞争的情况下只是在Mark Word里存储当前线程指针CAS操作都不做。轻量级锁在没有多线程竞争时相对重量级锁减少操作系统互斥量带来的性能消耗。但是如果存在锁竞争除了互斥量本身开销还额外有CAS操作的开销。自旋锁减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时就使用了自旋加锁的方式锁粗化将多个连续的加锁、解锁操作连接在一起扩展成一个范围更大的锁。锁消除虚拟机即时编译器在运行时对一些代码上要求同步但是被检测到不可能存在共享数据竞争的锁进行消除。 锁升级的过程是什么样的 锁升级方向无锁–偏向锁—轻量级锁----重量级锁这个方向基本上是不可逆的。 偏向锁 偏向锁的获取 判断是否为可偏向状态–MarkWord中锁标志是否为’01’是否偏向锁是否为’1’如果是可偏向状态则查看线程ID是否为当前线程如果是则进入步骤’5’否则进入步骤’3’通过CAS操作竞争锁如果竞争成功则将MarkWord中线程ID设置为当前线程ID然后执行’5’竞争失败则执行’4’CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起偏向锁升级为轻量级锁然后被阻塞在安全点的线程继续往下执行同步代码块执行同步代码 偏向锁的撤销 偏向锁不会主动释放撤销只有遇到其他线程竞争时才会执行撤销由于撤销需要知道当前持有该偏向锁的线程栈状态因此要等到safepoint时执行此时持有该偏向锁的线程T有’2’3’两种情况撤销----T线程已经退出同步代码块或者已经不再存活则直接撤销偏向锁变成无锁状态----该状态达到阈值20则执行批量重偏向升级—T线程还在同步代码块中则将T线程的偏向锁升级为轻量级锁当前线程执行轻量级锁状态下的锁获取步骤—该状态达到阈值40则执行批量撤销 轻量级锁 轻量级锁的获取 进行加锁操作时jvm会判断是否已经时重量级锁如果不是则会在当前线程栈帧中划出一块空间作为该锁的锁记录并且将锁对象MarkWord复制到该锁记录中复制成功之后jvm使用CAS操作将对象头MarkWord更新为指向锁记录的指针并将锁记录里的owner指针指向对象头的MarkWord。如果成功则执行’3’否则执行’4’更新成功则当前线程持有该对象锁并且对象MarkWord锁标志设置为‘00’即表示此对象处于轻量级锁状态更新失败jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录如果是则执行’5’否则执行’4’表示锁重入然后当前线程栈帧中增加一个锁记录第一部分Displaced Mark Word为null并指向Mark Word的锁对象起到一个重入计数器的作用。表示该锁对象已经被其他线程抢占则进行自旋等待默认10次等待次数达到闽值仍未获取到锁则升级为重量级锁 28.说说synchronized和ReentrantLock的区别 可以从锁的实现、功能特点、性能等几个维度去回答这个问题 锁的实现synchronized是Java语言的关键字基于JVM实现。而ReentrantLock是基于JDK的API层面实现的一般是lock()和unlock()方法配合try/finally语句块来完成。性能在JDK1.6锁优化以前synchronized的性能比ReenTrantLock差很多。但是JDK6开始增加了适应性自旋、锁消除等两者性能就差不多了。功能特点ReentrantLock 比 synchronized增加了一些高级功能如等待可中断、可实现公平锁、可实现选择性通知。 ReentrantLock提供了一种能够中断等待锁的线程的机制通过lock.lockInterruptibly()来实现这个机制ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制ReentrantLock类借助Condition接口与newCondition()方法实现。ReentrantLock需要手工声明来加锁和释放锁一般跟finally配合释放锁。而synchronized不用手动释放锁。 下面的表格列出出了两种锁之间的区别 区别sychronizedReetrantLock锁实现机制对象头监视器模式依赖AQS灵活性不灵活支持响应中断、超时、尝试获取锁释放锁形式自动释放锁显示调用unlock()支持锁类型非公平锁公平锁非公平锁条件队列单条件队列多个条件队列可重入支持支持支持 29.AQS了解多少 AbstractQueuedSynchronizer 抽象同步队列简称AQS它是Java并发包的根基并发包中的锁就是基于AQS实现的。 AQS是基于一个FIFO的双向队列其内部定义了一个节点类NodeNode节点内部的SHARED用来标记该线程是获取共享资源时被阻挂起后放入AQS队列的EXCLUSIVE用来标记线程是取独占资源时被挂起后放入AQS队列AQS使用一个volatile修饰的int类型的成员变量state来表示同步状态修改同步状态成功即为获得锁volatile保证了变量在多线程之间的可见性修改State值时通过CAS机制来保证修改的原子性获取state的方式分为两种独占方式和共享方式一个线程使用独占方式获取了资源其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源另外一个线程还可以通过CAS的方式进行获取。如果共享资源被占用需要一定的阻塞等待唤醒机制来保证锁的分配AQS中会将竞争共享资源失败的线程添加到一个变体的CLH队列中。 AQS 中的队列是 CLH变体的虚拟双向队列通过将每条请求共享资源的线程封装成一个节点来实现锁的分配 AQS 中的 CLH 变体等待队列拥有以下特性 AQS 中队列是个双向链表也是 FIFO 先进先出的特性通过 Head、Tail 头尾两个节点来组成队列结构通过 volatile 修饰保证可见性Head 指向节点为已获得锁的节点是一个虚拟节点节点本身不持有具体线程获取不到同步状态会将节点进行自旋获取锁自旋一定次数失败后会将线程阻塞相对于CLH队列性能较好 30.ReentrantLock实现原理 ReentrantLock是可重入的独占锁只能有一个线程可以获取该锁其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。 看看ReentrantLock的加锁操作 // 创建非公平锁 ReentrantLock lock new ReentrantLock(); // 获取锁操作 lock.lock(); try {// 执行代码逻辑 } catch (Exception ex) {// ... } finally {// 解锁操作lock.unlock(); }new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync。 公平锁 FairSync 公平锁是指多个线程按照申请锁的顺序来获取锁线程直接进入队列中排队队列中的第一个线程才能获得锁公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低等待队列中除第一个线程以外的所有线程都会阻塞CPU 唤醒阻塞线程的开销比非公平锁大。 非公平锁NonfairSync 非公平锁是多个线程加锁时直接尝试获取锁获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用那么这个线程可以无需阻塞直接获取到锁非公平锁的优点是可以减少唤起线程的开销整体的吞吐效率高因为线程有几率不阻塞直接获得锁CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死或者等很久才会获得锁 默认创建的对象lock()时候 如果锁当前没有被其它线程占用并且当前线程之前没有获取过该锁则当前线程会获取到该锁然后设置当前锁的拥有者为当前线程并设置AQS的状态值为1然后直接返回。如果当前线程之前己经获取过该锁则这次只是简单地把AQS的状态值加1后返回。如果该锁己经被其他线程持有非公平锁会尝试去获取锁获取失败的话则调用该方法线程会被放入AQS队列阻塞挂起。 31.ReentrantLock怎么实现公平锁的 new ReentrantLock() 构造函数默认创建的是非公平锁NonfairSynd public ReentrantLock() {sync new NonfairSync(); }同时也可以在创建锁构造函数中传入具体参数创建公平锁 FairSync ReentrantLock lock new ReentrantLock(true); --- ReentrantLock // true 代表公平锁false 代表非公平锁 public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync(); }FairSync、NonfairSync 代表公平锁和非公平锁两者都是 ReentrantLock 静态内部类只不过实现不同锁语义。 非公平锁和公平锁的两处不同 非公平锁在调用lock后首先就会调用CAS进行一次抢锁如果这个时候恰巧锁没有被占用那么直接就获取到锁返回了。非公平锁在CAS失败后和公平锁一样都会进入到tryAcquire方法在tryAcquire方法中如果发现锁这个时候被释放了state 0非公平锁会直接CAS抢锁但是公平锁会判断等待队列是否有线程处于等待状态如果有则不去抢锁乖乖排到后面。 相对来说非公平锁会有更好的性能因为它的吞吐量比较大。当然非公平锁让获取锁的时间变得更加不确定可能会导致在阻塞队列中的线程长期处于饥饿状态。 32.CAS呢CAS了解多少 CAS叫做CompareAndSwap比较并交换主要是通过处理器的指令来保证操作的原子性的。 CAS 指令包含3个参数共享变量的内存地址A、预期的值B和共享变量的新值C。 只有当内存中地址A处的值等于B时才能将内存中地址A处的值更新为新值C。作为一条CPU指令CAS指令本身是能够保证原子性的。 33.CAS有什么问题如何解决 ABA问题 并发环境下假设初始条件是A去修改数据时发现是A就会执行修改。但是看到的虽然是A中间可能发生了A变BB又变回A的情况。此时A已经非彼A数据即使成功修改也可能有问题。 怎么解决ABA问题 加版本号 每次修改变量都在这个变量的版本号上加1这样刚刚A-B-A虽然A的值没变但是它的版本号已经变了再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号这种做法可以给数据带上了一种实效性的检验。 Java提供了AtomicStampReference类它的compareAndSet方法首先检查当前的对象引用值是否等于预期引用并且当前印戳Stamp标志是否等于预期标志如果全部相等则以原子方式将引用值和印戳标志的值更新为给定的更新值。 循环性能开销 自旋CAS如果一直循环执行一直不成功会给CPU带来非常大的执行开销。 怎么解决循环性能开销问题 在Java中很多使用自旋CAS的地方会有一个自旋次数的限制超过一定次数就停止自旋。 只能保证一个变量的原子操作 CAS保证的是对一个变量执行操作的原子性如果对多个变量操作时CAS目前无法直接保证操作的原子性的。 怎么解决只能保证一个变量的原子操作问题 可以考虑改用锁来保证操作的原子性可以考虑合并多个变量将多个变量封装成一个对象通过AtomicReference来保证原子性。 34.Java有哪些保证原子性的方法如何保证多线程下i结果正确 使用循环原子类例如Atomiclnteger实现i原子操作使用juc包下的锁如ReentrantLock对i操作加锁lock.lock()来实现原子性使用synchronized对i操作加锁 35.原子操作类了解多少 当程序更新一个变量时如果多线程同时更新这个变量可能得到期望之外的值比如变量i1A线程更新i计1B线程也更新i1经过两个线程操作之后可能i不等于3而是等于2。因为A和B线程在更新变量i的时候拿到的都是1这就是线程不安全的更新操作一般我们会使用synchronized来解决这个问题synchronized会保证多线程不会同时更新变量i。 其实除此之外还有更轻量级的选择Java从WDK 1.5开始提供了java.util.concurrent.atomic包这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。 因为变量的类型有很多种所以在Atomic包里一共提供了13个类属于4种类型的原子更新方式分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性字段。 Atomic包里的类基本都是使用Unsafe实现的包装类。 使用原子的方式更新基本类型Atomic包提供了以下3个类 AtomicBoolean原子更新布尔类型。Atomiclnteger原子更新整型。AtomicLong原子更新长整型。 通过原子的方式更新数组里的某个元素Atomic包提供了以下4个类 AtomiclntegerArray原子更新整型数组里的元素。AtomicLongArray原子更新长整型数组里的元素。AtomicReferenceArray原子更新引用类型数组里的元素。AtomiclntegerArray类主要是提供原子的方式更新数组里的整型 原子更新基本类型的Atomiclnteger只能更新一个变量如果要原子更新多个变量就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类 AtomicReference原子更新引用类型。AtomicReferenceFieldUpdater原子更新引用类型里的字段。AtomicMarkableReference原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReferenceVinitialRefboolean initialMark。 如果需原子地更新某个类里的某个字段时就需要使用原子更新字段类Atomic包提供了以下3个类进行原子字段更新 AtomiclntegerFieldUpdater原子更新整型的字段的更新器。AtomicLongFieldUpdater原子更新长整型字段的更新器。AtomicStampedReference原子更新带有版本号的引用类型。该类将整数值与引用关联起来可用于原子的更新数据和数据的版本号可以解决使用CAS进行原子更新时可能出现的ABA问题。 36.Atomiclnteger的原理 一句话概括使用CAS实现。 以Atomiclnteger的添加方法为例 public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1); }通过Unsafe类的实例来进行添加操作来看看具体的CAS操作 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; }compareAndSwaplnt是一个native方法基于CAS来操作int类型变量。其它的原子操作类基本都是大同小异。 37.线程死锁了解吗该如何避免 死锁是指两个或两个以上的线程在执行过程中因争夺资源而造成的互相等待的现象在无外力作用的情况下这些线程会一直相互等待而无法继续运行下去。 那么为什么会产生死锁呢死锁的产生必须具备以下四个条件 互斥条件指线程对己经获取到的资源进行它性使用即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源则请求者只能等待直至占有资源的线程释放该资源。请求并持有条件指一个线程己经持有了至少一个资源但又提出了新的资源请求而新资源己被其它线程占有所以当前线程会被阻塞但阻塞的同时并不释放自己已经获取的资源。不可剥夺条件指线程获取到的资源在自己使用完之前不能被其它线程抢占只有在自己使用完毕后才由自己释放该资源。环路等待条件指在发生死锁时必然存在一个线程----资源的环形链即线程集合{T0T1T2……Tn}中T0正在等待----T1占用的资源T1正在等待T2用的资源……Tn在等待己被T0占用的资源。 该如何避免死锁呢答案是至少破坏死锁发生的一个条件。 其中互斥这个条件我们没有办法破坏因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的到底如何做呢对于“请求并持有”这个条件可以一次性请求所有的资源。对于“不可剥夺”这个条件占用部分资源的线程进一步申请其他资源时如果申请不到可以主动释放它占有的资源这样不可抢占这个条件就破坏掉了。对于“环路等待”这个条件可以靠按序申请资源来预防。所谓按序申请是指资源是有线性顺序的申请的时候可以先申请资源序号小的再申请资源序号大的这样线性化后就不存在环路了。 38.那死锁问题怎么排查呢 可以使用jdk自带的命令行工具排查 使用jps查找运行的Java进程jps -l使用jstack查看线程堆栈信息jstack -l 进程id 基本就可以看到死锁的信息。 还可以利用图形化工具比如如Console。出现线程死锁以后点击Console线程面板的检测到死锁按钮将会看到线程的死锁信息。 并发工具类 39.CountDownLatch倒计数器了解吗 CountDownLatch倒计数器有两个常见的应用场景 场景1协调子线程结束动作等待所有子线程运行结束 CountDownLatch允许一个或多个线程等待其他线程完成操作。 例如我们很多人喜欢玩的王者荣耀开黑的时候得等所有人都上线之后才能开打。 CountDownLatch模仿这个场景 创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家主线程必须在他们都完成确认后才可以继续运行。 在这段代码中new CountDownLatch(5)用户创建初始的latch数量各玩家通过countDownLatch.countDown()完成状态确认主线程通过countpownLatch.await()等待。 场景2.协调子线程开始动作统一各线程动作开始的时机 王者游戏中也有类似的场景游戏开始时各玩家的初始状态必须一致。不能有的玩家都出完装了有的才降生。 所以大家得一块出生在这个场景中仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是各玩家虽然都调用了start()线程但是它们在运行时都在等待countpownLatch的信号在信号未收到前它们不会往下执行。 CountDownLatch的核心方法也不多: await(): 等待latch降为0;boolean await(long timeout,TimeUnit unit): 等待latch降为0,但是可以设置超时时间.比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老.countDown(): latch数量减1;getcount(): 获取当前的latch数量. 40.CyclicBarrier同步屏障了解吗 CyclicBarrier的字面意思是可循环使用Cyclic的屏障Barrier。它要做的事情是让一组线程到达一个屏障也可以叫同步点时被阻塞直到最后一个线程到达屏障时屏障才会开门所有被屏障拦截的线程才会继续运行。 它和CountDownLatch类似都可以协调多线程的结束动作在它们结束后都可以执行特定动作但是为什么要有CyclicBarrier自然是它有和CountDownLatch不同的地方。 我们拿代码模拟这一场景发现CountDownLatch无能为力了因为CountDownLatch的使用是一次性的无法重复利用而这里等待了两次。此时我们用CyclicBarrier就可以实现因为它可以重复利用。 运行结果 CyclicBarrier最最核心的方法仍然是await() 如果当前线程不是第一个到达屏障的话它将会进入等待直到其他线程都到达除非发生被中断、屏障被拆除、屏障被重设等情况 上面的例子抽象一下本质上它的流程就是这样就是这样 41.CyclicBarrier和CountDownLatch有什么区别 两者最核心的区别 CountDownLatch是一次性的而CyclicBarrier则可以多次设置屏障实现重复利用CountDownLatch中的各个子线程不可以等待其他线程只能完成自己的任务而CyclicBarrier中的各个线程可以等待其他线程 它们区别用一个表格整理 42.Semaphore信号量了解吗 Semaphore信号量是用来控制同时访问特定资源的线程数量它通过协调各个线程以保证合理的使用公共资源。 听起来似乎很抽象现在汽车多了开车出门在外的一个老大难问题就是停车。停车场的车位是有限的只能允许若干车辆停泊如果停车场还有空位那么显示牌显示的就是绿灯和剩余的车位车辆就可以驶入如果停车场没位了那么显示牌显示的就是红灯和数字0车辆就得等待。如果满了的停车场有车离开那么显示牌就又变绿显示空车位数量等待的车辆就能进停车场。 我们把这个例子类比一下车辆就是线程进入停车场就是线程在执行离开停车场就是线程执行完毕看见红灯就表示线程被阻塞不能执行Semaphore的本质就是协调多个线程对共享资源的获取。 我们再来看一个Semaphore的用途它可以用于做流量控制特别是公用资源有限的应用场景比如数据库连接。 假如有一个需求要读取几万个文件的数据因为都是IO密集型任务我们可以启动几十个线程并发地读取但是如果读到内存后还需要存储到数据库中而数据库的连接数只有10个这时我们必须控制只有10个线程同时获取数据库连接保存数据否则会报错无法获取数据库连接。这个时候就可以使用Semaphore来做流量控制如下 public class SemaphoreTest {private static final int THREAD_COUNT 30;private static ExecutorService threadPool Executors.newFixedThreadPool(THREAD_COUNT);private static Semaphore s new Semaphore(10);public static void main(String[] args) {for (int i 0; i THREAD_COUNT; i) {threadPool.execute(new Runnable() {Overridepublic void run() {try {s.acquire();System.out.println(save data);s.release();} catch (InterruptedException e) {}}});}threadPool.shutdown();} }在代码中虽然有30个线程在执行但是只允许10个并发执行。Semaphore的构造方法 semaphore(int permits)接受一个整型的数字表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证也就是最大并发数是10Semaphore的用法也很简单首先线程使用Semaphore的acquire()方法获取一个许可证使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。 43.Exchanger 了解吗 Exchanger交换者是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点在这个同步点两个线程可以交换彼此的数据。 这两个线程通过exchange方法交换数据如果第一个线程先执行exchange()方法它会一直等待第二个线程也执行exchange方法当两个线程都到达同步点时这两个线程就可以交换数据将本线程生产出来的数据传递给对方。 Exchanger可以用于遗传算法遗传算法里需要选出两个人作为交配对象这时候会交换两人的数据并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水为了避免错误采用AB岗两人进行录入录入到Excel之后系统需要加载这两个Excel并对两个Excel数据进行校对看看是否录入一致。 public class ExchangerTest {private static final ExchangerString exgr new ExchangerString();private static ExecutorService threadPool Executors.newFixedThreadPool(2);public static void main(String[] args) {threadPool.execute(new Runnable() {Overridepublic void run() {try {String A 银行流水A; // A录入银行流水数据exgr.exchange(A);} catch (InterruptedException e) {}}});threadPool.execute(new Runnable() {Overridepublic void run() {try {String B 银行流水B; // B录入银行流水数据String A exgr.exchange(B);System.out.println(A和B数据是否一致 A.equals(B) A录入的是 A B录入是 B);} catch (InterruptedException e) {}}});threadPool.shutdown();} }假如两个线程有一个没有执行exchange()方法则会一直等待如果担心有特殊情况发生避免一直等待 可以使用exchange(V x, long timeOut, TimeUnit unit)设置最大等待时长。 线程池 44.什么是线程池 线程池简单理解它就是一个管理线程的池子。 它帮我们管理线程避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象创建一个对象需要经过类加载过程销毁一个对象需要走GC垃圾回收流程都是需要资源开销的。提高响应速度。如果任务到达了相对于从线程池拿线程重新去创建一条线程执行速度肯定慢很多。重复利用。线程用完再放回池子可以达到重复利用的效果节省资源。 45.能说说工作中线程池的应用吗 之前我们有一个和第三方对接的需求需要向第三方推送数据引入了多线程来提升数据推送的效率其中用到了线程池来管理线程。 完整可运行代码地址https://gitee.com/fighter3/thread-demo.git 线程池的参数如下 corePoolSize线程核心参数, 选择了CPU数×2maximumPoolSize最大线程数, 选择了和核心线程数相同keepAliveTime非核心闲置线程存活时间, 直接置为0unit非核心线程保持存活的时间, 选择了 TimeUnit.SECONDS 秒workQueue线程池等待队列使用 LinkedBlockingQueue阻塞队列同时还用了synchronized 来加锁保证数据不会被重复推送 synchronized (PushProcessServiceImpl.class) {}ps这个例子只是简单地进行了数据推送实际上还可以结合其他的业务像什么数据清洗啊、数据统计啊都可以套用。 46.能简单说一下线程池的工作流程吗 用一个通俗的比喻 有一个营业厅总共有六个窗口现在开放了三个窗口现在有三个窗口坐着三个营业员小姐姐在营业。 我去办业务可能会遇到什么情况呢 我发现有空间的在营业的窗口直接去找小姐姐办理业务。 我发现没有空闲的窗口就在排队区排队等。 我发现没有空闲的窗口等待区也满了蚌埠住了经理一看就让休息的小姐姐赶紧回来上班等待区号靠前的赶紧去新窗口办我去排队区排队。小姐姐比较辛苦假如一段时间发现他们可以不用接着营业经理就让她们接着休息。 我一看六个窗口都满了等待区也没位置了。我急了要闹经理赶紧出来了经理该怎么办呢 我们银行系统已经瘫痪谁叫你来办的你找谁去看你比较急去队里加个塞今天没办法不行你看改一天 上面的这个流程几乎就跟JDK 线程池的大致流程类似 营业中的 3个窗口对应核心线程池数corePoolSize总的营业窗口数6对应maximumPoolSize打开的临时窗口在多少时间内无人办理则关闭对应unit排队区就是等待队列workQueue无法办理的时候银行给出的解决方法对应RejectedExecutionHandlerthreadFactory该参数在JDK 中是 线程工厂用来创建线程对象一般不会动。 所以我们线程池的工作流程也比较好理解了 线程池刚创建时里面没有一个线程。任务队列是作为参数传进来的。不过就算队列里面有任务线程池也不会马上执行它们。当调用 execute()方法添加一个任务时线程池会做如下判断 如果正在运行的线程数量小于corePoolSize那么马上创建线程运行这个任务如果正在运行的线程数量大于或等于 corePoolSize那么将这个任务放入队列如果这时候队列满了而且正在运行的线程数量小于 maximumPoolSize那么还是要创建非核心线程立刻运行这个任务如果队列满了而且正在运行的线程数量大于或等于 maximumPoolSize那么线程池会根据拒绝策略来对应处理。 当一个线程完成任务时它会从队列中取下一个任务来执行。当一个线程无事可做超过一定的时间keepAliveTime时线程池会判断如果当前运行的线程数大于corePoolSize那么这个线程就被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。 47.线程池主要参数有哪些 线程池有七大参数需要重点关注corePoolSize、maximumPoolSize、workQueue、handler这四个。 corePoolSize 此值是用来初始化线程池中核心线程数当线程池中线程池数 corePoolSize时系统默认是添加一个任务才创建一个线程池。 当线程数 corePoolSize时新任务会追加到workQueue中。 maximumPoolSize maximumPoolSize表示允许的最大线程数等于非核心线程数核心线程数当BlockingQueue也满了但线程池中总线程数 maximumPoolSize时候就会再次创建新的线程。 keepAliveTime 非核心线程 maximumPoolSize - corePoolSize非核心线程闲置下来不干活最多存活时间。 unit 线程池中非核心线程保持存活的时间的单位 TimeUnit.DAYS天TimeUnit.HOURS小时TimeUnit.MINUTES分钟TimeUnit.SECONDS秒TimeUnit.MILLISECONDS毫秒TimeUnit.MICROSECONDS微秒TimeUnit.NANOSECONDS纳秒 workQueue 线程池等待队列维护着等待执行的Runnable对象。当运行当线程数 corePoolSize时新的任务会被添加到workQueue中如果workQueue也满了则尝试用非核心线程执行任务等待队列应该尽量用有界的。 threadFactory 创建一个新线程时使用的工厂可以用来设定线程名、是否为daemon线程等等。 handler corePoolSize、workQueue、maximumPoolSize都不可用的时候执行的饱和策略。 48.线程池的拒绝策略有哪些 类比前面的例子无法办理业务时的处理方式帮助记忆 AbortPolicy直接抛出异常默认使用此策略CallerRunsPolicy用调用者所在的线程来执行任务DiscardOldestPolicy丢弃阻塞队列里最老的任务也就是队列里靠前的任务DiscardPolicy当前任务直接丟弃 想实现自己的拒绝策略实现RejectedExecutionHandler接口即可。 49.线程池有哪几种工作队列 常用的阻塞队列主要有以下几种 ArrayBlockingQueueArrayBlockingQueue有界队列是一个用数组实现的有界阻塞队列按FIFO排序量。LinkedBlockingQueueLinkedBlockingQueue可设置容量队列是基于链表结构的阻塞队列按FlFO排序任务容量可以选择进行设置不设置的话将是一个无边界的阻塞队列最大长度为Integer.MAX-VALUE吞吐量通常要高于ArrayBlockingQuenenewFixedThreadPool线程池使用了这个队列DelayQueueDelayQueue延迟队列是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。PriorityBlockingQueuePriorityBlockingQueue优先级队列是具有优先级的无界阻塞队列SynchronousQueueSynchronousQueue同步队列是一个不存储元素的阻塞队列每个插入操作必须等到另一个线程调用移除操作否则插入操作一直处于阻塞状态吞吐量通常要高于LinkedBlockingQuenenewCachedThreadPool线程池使用了这个队列。 50·线程池提交execute和submit有什么区别 execute用于提交不需要返回值的任务 threadsPool.execute(new Runnable() {Override public void run() {// TODO Auto-generated method stub } });submit() 方法用于提交需要返回值的任务。线程池会返回一个future类型的对象通过这个future对象可以判断任务是否执行成功并且可以通过future的get()方法来获取返回值 FutureObject future executor.submit(harReturnValuetask); try { Object s future.get(); } catch (InterruptedException e) {// 处理理中断异常 } catch (ExecutionException e) {// 处理理⽆无法执⾏行行任务异常 } finally {// 关闭线程池 executor.shutdown(); }51.线程池怎么关闭知道吗 可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程然后逐个调用线程的interrupt方法来中断线程所以无法响应中断的任务可能永远无法终止。 shutdown() 将线程池状态置为shutdown并不会立即停止 停止接收外部submit的任务内部正在跑的任务和队列里等待的任务会执行完等到第二步完成后才真正停止 shutdownNow()将线程池状态置为stop。一般会立即停止事实上不一定 和shutdown()一样先停止接收外部提交的任务忽略队列里等待的任务尝试将正在跑的任务interrupt中断返回未执行的任务列表 shutdown和shutdownnow简单来说区别如下 shutdownNow() 能立即停止线程池正在跑的和正在等待的任务都停下了。这样做立即生效但是风险也比较大。shutdown()只是关闭了提交通道用submit()是无效的而内部的任务该怎么跑还是怎么跑跑完再彻底停止线程池。 52.线程池的线程数应该怎么配置 线程在Java中属于稀缺资源线程池不是越大越好也不是越小越好。任务分为计算密集型、IO密集型、混合型。 计算密集型大部分都在用CPU跟内存加密逻辑操作业务处理等。IO密集型数据库链接网络通讯传输等。 一般的经验不同类型线程池的参数配置 计算密集型一般推荐线程池不要过大一般是CPU数 11是因为可能存在页缺失就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存。如果线程池数太大可能会频繁的进行线程上下文切换跟任务调度。获得当前CPU核心数代码如下 Runtime.getRuntime().availableProcessors();IO密集型线程数适当大一点机器的CPU核心数*2。混合型可以考虑根绝情况将它拆分成CPU密集型和IO密集型任务如果执行时间相差不大拆分可以提升吞吐量反之没有必要。 53.有哪几种常见的线程池 面试常问主要有四种都是通过工具类Excutors创建出来的需要注意阿里巴巴《Java开发手册》里禁止使用这种方式来创建线程池。 54. 能说一下四种常见线程池的原理吗 前三种线程池的构造直接调用ThreadPoolExecutor的构造方法。 newSingleThreadExecutor public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueueRunnable(),threadFactory)); }线程池特点 核心线程数为1最大线程数也为1阻塞队列是无界队列LinkedBlockingQueue可能会导致OOMkeepAliveTime为0 工作流程 提交任务线程池是否有一条线程在如果没有新建线程执行任务如果有将任务加到阻塞队列当前的唯一线程从队列取任务执行完一个再继续取一个线程执行任务。 适用场景 适用于串行执行任务的场景一个任务一个任务地执行。 newFixedThreadPool public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueueRunnable(),threadFactory);}线程池特点 核心线程数和最大线程数大小一样没有所谓的非空闲时间即keepAliveTime为0阻塞队列为无界队列LinkedBlockingQueue可能会导致OOM 工作流程 提交任务如果线程数少于核心线程创建核心线程执行任务如果线程数等于核心线程把任务添加到LinkedBlockingQueue阻塞队列如果线程执行完任务去阻塞队列取任务继续执行。 使用场景 FixedThreadPool适用于处理CPU密集型的任务确保CPU在长期被工作线程使用的情况下尽可能的少的分配线程即适用执行长期的任务。 newCachedThreadPool public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueueRunnable(),threadFactory); }线程池特点 核心线程数为0最大线程数为Integer.MAX_VALUE即无限大可能会因为无限创建线程导致OOM阻塞队列是SynchronousQueue非核心线程空闲存活时间为60秒 当提交任务的速度大于处理任务的速度时每次提交一个任务就必然会创建一个线程。极端情况下会创建过多的线程耗尽CPU和内存资源。由于空闲60秒的线程会被终止长时间保持空闲的CachedThreadPool不会占用任何资源。 工作流程 提交任务因为没有核心线程所以任务直接加到SynchronousQueue队列。判断是否有空闲线程如果有就去取出任务执行。如果没有空闲线程就新建一个线程执行。执行完任务的线程还可以存活60秒如果在这期间接到任务可以继续活下去否则被销毁。 适用场景 用于并发执行大量短期的小任务。 newScheduledThreadPool public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue()); }线程池特点 最大线程数为Integer.MAX_VALUE也有OOM的风险 阻塞队列是DelayedWorkQueue keepAliveTime为0 scheduleAtFixedRate()按某种速率周期执行 scheduleWithFixedDelay()在某个延迟后执行 工作机制 线程从DelayQueue中获取已到期的ScheduledFutureTask (DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。线程执行这个ScheduledFutureTask。线程修改ScheduledFutureTask的time变量为下次将要被执行的时间。线程把这个修改time之后的ScheduledFutureTask放回DelayQueue中DelayQueue.add()。 使用场景 周期性执行任务的场景需要限制线程数量的场景 使用无界队列的线程池会导致什么问题吗 例如newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue如果线程获取一个任务后任务的执行时间比较长会导致队列的任务越积越多导致机器内存使用不停飙升最终导致OOM。 55.线程池异常怎么处理知道吗 在使用线程池处理任务的时候任务代码可能抛出RuntimeException抛出异常后线程池可能捕获它也可能创建一个新的线程来代替异常的线程我们可能无法感知任务出现了异常因此我们需要考虑线程池异常情况。 常见的异常处理方式 56.能说一下线程池有几种状态吗 线程池有这几个状态RUNNINGSHUTDOWNSTOPTIDYINGTERMINATED。 //线程池状态 private static final int RUNNING -1 COUNT_BITS; private static final int SHUTDOWN 0 COUNT_BITS; private static final int STOP 1 COUNT_BITS; private static final int TIDYING 2 COUNT_BITS; private static final int TERMINATED 3 COUNT_BITS;RUNNING 该状态的线程池会接收新任务并处理阻塞队列中的任务调用线程池的shutdown方法可以切换到SHUTDOWN状态调用线程池的shutdownNow方法可以切换到STOP状态 SHUTDOWN 该状态的线程池不会接收新任务但会处理阻塞队列中的任务队列为空并且线程池中执行的任务也为空进入TIDYING状态 STOP 该状态的线程不会接收新任务也不会处理阻塞队列中的任务而且会中断正在运行的任务线程池中执行的任务为空进入TIDYING状态 TIDYING 该状态表明所有的任务已经运行终止记录的任务数量为0。terminated执行完毕进入TERMINATED状态 TERMINATED 该状态表示线程池彻底终止 57.线程池如何实现参数的动态修改 线程池提供了几个setter方法来设置线程池的参数。 这里主要有两个思路 在我们微服务的架构下可以利用配置中心如Nacos、Apollo等等也可以自己开发配置中心。业务服务读取线程池配置获取相应的线程池实例来修改线程池的参数。如果限制了配置中心的使用也可以自己去扩展ThreadPoolExecutor重写方法监听线程池参数变化来动态修改线程池参数。 线程池调优了解吗 线程池配置没有固定的公式通常事前会对线程池进行一定评估常见的评估方案如下 上线之前也要进行充分的测试上线之后要建立完善的线程池监控机制。 事中结合监控告警机制分析线程池的问题或者可优化点结合线程池动态参数配置机制来调整配置。 事后要注意仔细观察随时调整。 58.你能设计实现一个线程池吗 这道题在阿里的面试中出现频率比较高 线程池实现原理可以查看要是以前有人这么讲线程池我早就该明白了当然我们自己实现只需要抓住线程池的核心流程 我们自己的实现就是完成这个核心流程 线程池中有N个工作线程把任务提交给线程池运行如果线程池已满把任务放入队列最后当有空闲时获取队列中任务来执行 59.单机线程池执行断电了应该怎么处理 我们可以对正在处理和阻塞队列的任务做事务管理或者对阻塞队列中的任务持久化处理并且当断电或者系统崩溃操作无法继续下去的时候可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。 也就是说对阻塞队列持久化正在处理任务事务控制断电之后正在处理任务的回滚通过日志恢复该次操作服务器重启后阻塞队列中的数据再加载。 并发容器和框架 60.Fork/Join框架了解吗 Fork/Join框架是Java7提供的一个用于并行执行任务的框架是一个把大任务分割成若干个小任务最终汇总每个小任务结果后得到大任务结果的框架。 要想掌握Fork/Join框架首先需要理解两个点分而治之和工作窃取算法。 工作窃取算法 大任务拆成了若干个小任务把这些小任务放到不同的队列里各自创建单独线程来执行队列里的任务。 那么问题来了有的线程干活快有的线程干活慢。干完活的线程不能让它空下来得让它去帮没干完活的线程干活。它去其它线程的队列里窃取一个任务来执行这就是所谓的工作窃取。 工作窃取发生的时候它们会访问同一个队列为了减少窃取任务线程和被窃取任务线程之间的竞争通常任务会使用双端队列被窃取任务线程永远从双端队列的头部拿而窃取任务的线程永远从双端队列的尾部拿任务执行。 ForkjoinTask与一般Task的主要区别在于它需要实现compute方法在这个方法里首先需要判断任务是否足够小如果足够小就直接执行任务。如果比较大就必须分割成两个子任务每个子任务在调用fork方法时又会进compute方法看看当前子任务是否需要继续分割成子任务如果不需要继续分割则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。 资料来源地址面渣逆袭Java并发六十问图文详解快来看看你会多少道
http://www.dnsts.com.cn/news/195732.html

相关文章:

  • 中英版网站怎么做没有网站没有推广如何做外贸
  • 瑞安网站建设电话正规的网站建设官网
  • 广州购物商城网站wordpress 编辑器 代码高亮
  • 甘肃省安装建设集团公司网站烟台做网站电话
  • 长沙做网站美工的公司手机软件商店下载
  • 北京建设招标信息网站开发公司五一节前安全生产工作部署会
  • 有哪些网站做二手房好的东莞网站建设-南城石佳
  • 故宫博物院官网网站咋做的移动网站网上营业厅
  • 12306网站学生做北京品牌建设网站公司排名
  • 网站详情页设计广州做网站平台的企业
  • 模板的网站都有哪些无锡网络营销推广软件
  • 公司制作网站怎么做深圳有哪些网站开发公司
  • 山东省建设银行网站网站建设好了怎么在百度可以搜到
  • 连锁租车网站源码wordpress做微信小程序
  • 网站建设策划模板下载网站开发方法 优帮云
  • 安庆跨境电商建站哪家好深圳网站建设公司有哪些内容
  • 台州网站建站服务哪家奿先备案还是先做网站
  • 建设安全员协会网站个人养老保险缴费明细
  • 广东个人 网站备案广州谷歌优化
  • 深圳企业网站制作企业公司邮箱怎么查询
  • 哪些网站是用php做的网站建设相关职业岗位
  • 做搜狗网站点击赚钱电商网站优化方案
  • 网站建设推广公众号制作推广青岛wordpress竞猜插件
  • 自己建网站做那个模块好wordpress学习pdf
  • 永州网站建设求职简历开县做网站
  • 信誉好的网站建设wordpress添加音乐
  • 网站论坛源码新加坡域名注册网站
  • 宜昌网站企业关键词排名点击软件工具
  • 长沙网站建设及推广公司成品网站货源1688免费推荐
  • 不会写代码如何做网站品优购html代码