本文转载自微信公众号《后端技术小牛说》,作者后端技术小牛说。转载本文请联系后端技术小牛说公众号。写在最上面最近收到了一些读者的反馈。感谢我开发了网站interviewtop.top,对他们的面试帮助很大。甚至有读者给我发小红包赞助我。当然我也知道大家还是以学生群体为主,红包赞助都是免费的!之前是学生,租了阿里云、腾讯云等云服务器9.9元的学生套餐,利用了各厂的学生福利云服务器负载均衡作为服务器后端.这几个月,既然成了社会人,当然就没有这个福利了(9.9等效配置,社会人现在快200了,所以还在上学的同学需要这个毛线来收藏一下。一把)。而且随着使用频率的增加,配置要求当然不一定满足。另外,interviewtop网站也需要您的帮助和支持!如果你有面试经验,希望投稿:点击Search在All下搜索自己的面试题和对应的题库点击这个问题我之前面试过,投稿如果你的一些面试经验没有包含在题目中银行,欢迎添加小牛微信,小牛后台更新!java内存模型(JMM)简介java内存模型定义了程序中各种变量的访问规则。它规定所有的变量都存放在主存中,线程有自己的工作内存。线程使用的变量的主内存副本保存在工作内存中。线程对变量的所有操作都必须在工作空间进行,不能直接读写主存数据。操作完成后,线程工作内存通过缓存一致性协议将操作数据刷新回主内存。简而言之,as-if-serial编译器等会对原程序进行指令重排序和优化。但是无论怎么重新排序,结果都是和用户原程序输出的预定结果一致的。简述happens-before程序顺序规则的八个原则:写在线程前面的操作先发生在后面。加锁规则:解锁操作先发生在同一把锁上的加锁操作之前。volatile规则:对volatile变量的写操作发生在后续读操作之前。线程启动规则:线程的start方法在线程的每个动作中最先出现。线程中断规则:线程interrupt()方法的调用发生在被中断线程的代码检测到中断事件发生之前。线程终止规则:线程中的所有操作都发生在线程终止检测之前。对象终结规则:对象的初始化首先发生在finalize方法中。传递规则:如果操作A发生在操作B之前,操作B发生在操作C之前,那么操作A发生在操作Cas-if-serial和happens-before的区别as-if-serial保证了单线程程序的执行结果是不变的,happens-before保证一个正确同步的多线程程序的执行结果不变。原子操作简述一个操作或多个操作,要么全部执行并且执行过程不会被任何因素打断,要么都不执行,这就是原子操作。简述线程的可见性可见性是指当一个线程修改共享变量时,其他线程可以立即知道修改。volatile、synchronized、final可以保证可见性。简单描述一下有序性就是多线程中虽然存在并发、指令优化等操作,但是在本线程中观察到的线程的所有执行操作都是有序的。简述java中volatile关键字的作用,保证变量对所有线程的可见性。当一个线程修改变量的值时,新值立即可供其他线程使用。禁用指令重新排序优化。使用volatile变量进行写操作。汇编指令以lock为前缀,相当于内存屏障。编译器不会重新排列内存屏障之前的后续指令。java线程的实现实现了Runnable接口,继承了Thread类。实现Callable接口简单描述了java线程的状态。线程状态包括New,RUNNABLE,BLOCK,WAITING,TIMED_WAITING,THERMINATEDNEW:New:新的状态,线程已经创建,还没有启动,还没有调用start方法。RUNNABLE:运行状态。意思是线程在JVM中执行,但是这个执行不一定是在运行,也有可能是在排队等待CPU。BLOCKED:阻塞状态。线程正在等待获取锁,而锁还没有获取到。WAITING:等待状态。线程中的run方法运行完语句Object.wait()/Thread.join()进入该状态。TIMED_WAITING:定时等待。一定时间后跳出状态。调用Thread.sleep(long)Object.wait(long)Thread.join(long)进入状态。其中,这些参数代表等待时间。TERMINATED:终止状态。线程在调用run方法后进入该状态。简述线程通信的方式。volatile关键字修饰变量,保证所有线程访问变量的可见性。同步关键字。确保一次只有多个线程中的一个在方法或同步块中。wait/notify方法IO通信简要线程池如果没有线程池,多次创建和销毁线程的开销很大。如果创建的线程执行完当前任务后执行下一个任务,则重新使用创建的线程以减少开销并控制最大并发数。线程池在创建线程时,会将线程封装成一个工作线程Worker,Worker完成任务后也会循环获取工作队列中的任务执行。向线程池派发任务时,以下几种情况核心线程池未满,会创建一个新的线程来执行任务。如果核心线程池已满,而工作队列未满,则将线程存入工作队列。如果工作队列已满且线程数小于最大线程数,则创建一个新线程来处理任务。如果超过大小线程数,任务将按照拒绝策略进行处理。线程池参数corePoolSize:常驻核心线程数。超过这个值后,如果线程空闲,就会被销毁。maximumPoolSize:线程池可以容纳并发执行线程的最大数量。keepAliveTime:线程空闲时间,空闲时间达到这个值后线程会被销毁,直到只剩下corePoolSize个线程,避免浪费内存资源。workQueue:工作队列。threadFactory:线程工厂,用来生产一组任务相同的线程。处理程序:拒绝策略。有以下几种拒绝策略:AbortPolicy:中止任务并抛出异常CallerRunsPolicy:重试提交任务DiscardOldestPolicy放弃队列中等待时间最长的任务,将当前任务加入队列DiscardPolicy表示直接丢弃当前任务,不抛出一个例外。线程池创建方法newFixedThreadPool创建一个固定大小的线程池。newSingleThreadExecutor,使用单线程线程池。newCachedThreadPool,maximumPoolSize设置为Integer的最大值,worker线程工作完成后会被回收。newScheduledThreadPool:支持定时、周期性的任务执行,不回收工作线程。newWorkStealingPool:具有多个任务队列的线程池。简单描述Executor框架Executor框架的目的是将任务的提交和任务的运行方式分开。用户不再需要从代码层考虑设计任务的提交和执行,只需要调用Executor框架实现类的Execute方法提交任务即可。生成线程池的函数ThreadPoolExecutor也是Executor的具体实现类。简单描述一下Executor的继承关系Executor:一个接口,定义了一个接收Runnable对象的方法执行器,它接收一个Runable实例来执行这个任务。ExecutorService:Executor的子类接口,定义了一个方法,接收一个Callable对象,返回一个Future对象,并提供一个execute方法。ScheduledExecutorService:ExecutorService的子类接口,支持周期性执行任务。AbstractExecutorService:抽象类,提供ExecutorService执行方法的默认实现。Executors:实现了ExecutorService接口的静态工厂类,提供了一系列创建线程池的工厂方法。ThreadPoolExecutor:继承AbstractExecutorService创建线程池。ForkJoinPool:继承AbstractExecutorService。fork将一个大任务fork成多个小任务,然后让小任务执行。Join是获取小任务的结果,类似于mapreduce。ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建一个线程池,里面有定时任务。简单描述一下线程池的状态Running:可以接受新提交的任务,也可以处理阻塞队列的任务。关机:不再接受新提交的任务,但可以处理已有的任务。在线程池运行时调用shutdown方法会进入该状态。停止:不接受新任务,不处理已有任务,调用shutdownnow进入该状态。Tidying:所有任务已经终止,worker_count(有效线程数)为0。Terminated:线程池完全终止。在整理模式下调用终止方法将进入该状态。阻塞队列的简要说明阻塞队列是生产者消费者的具体实现组件之一。当阻塞队列为空时,从队列中获取元素的操作将被阻塞,当阻塞队列满时,向队列中添加元素的操作将被阻塞。具体实现是:ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。PriorityBlockingQueue:阻塞优先级队列。DelayQueue:创建元素时,可以指定从队列中获取当前元素需要多长时间SynchronousQueue:不存储元素的阻塞队列,每次存储都要等待一个fetch操作LinkedTransferQueue:与LinkedBlockingQueue相比,多了一个moretransfer方法,即如果当前有consumer在等待接收元素,producer传入的元素可以立即传输给consumer。LinkedBlockingDeque:双向阻塞队列。说说ThreadLocalThreadLocal是一个线程共享变量。ThreadLoacl有一个静态内部类ThreadLocalMap,它的Key是一个ThreadLocal对象,它的值是一个Entry对象。ThreadLocalMap是每个线程私有的。set设置ThreadLocalMap的值。获取ThreadLocalMap。remove删除ThreadLocalMap类型的对象。存在的问题对于线程池来说,由于线程池会重用Thread对象,Thread绑定的ThreadLocal也会被重用,从而引发一系列问题。内存泄漏。由于ThreadLocal是弱引用,而Entry的值是强引用,当ThreadLocal被垃圾回收时,该值仍然不会被释放,从而导致内存泄漏。说说你对javaconcurrent包下unsafe类的理解。对于Java语言来说,没有直接的指针组件,一般不能使用偏移量对某块内存进行操作。这些操作相对安全。Java有一个名为Unsafe类的类。该类使Java具备了像C语言指针一样操作内存空间的能力,但同时也带来了指针问题。这个类可以说是Java并发开发的基础。JAVA中的乐观锁和CAS算法对于乐观锁,开发者认为在数据传输过程中发生并发冲突的概率较低,所以在读操作之前不加锁。写操作的时候会判断,这段时间数据是否被其他线程修改过。如果有修改,则返回写入失败;如果没有被修改,则执行修改操作,返回修改成功。乐观锁定通常使用比较和交换(CAS)算法来实现。顾名思义,该算法涉及两个操作,比较(Compare)和交换(Swap)。CAS算法的思想是这样的:该算法认为不同线程对变量进行操作时竞争较少。算法的核心是比较当前读取的变量值E和内存中旧的变量值V。如果相等,说明其他线程没有修改变量,将变量值更新为新值N。如果不相等,则认为其他线程修改了变量,从读取值E到比较阶段,并且不执行任何操作。ABA问题及解决方案简述CAS算法是基于值进行比较的。如果当前有两个线程,一个线程把变量值从A改成B,然后又把B改回A。当当前线程开始执行CAS算法时,很容易认为值没有变,而误认为从读取数据到执行CAS算法这段时间没有线程修改过数据。juc包提供了一个AtomicStampedReference,在原始版本上加上版本号戳,解决ABA问题。常见Atomic类的简单描述很多时候,我们需要的只是一个简单、高效、线程安全的++或--解决方案,可以使用synchronized关键字和lock来实现,但是成本比较高.这时候,使用Atomic类就更方便了。基本数据类型的原子类有:AtomicInteger原子更新整数AtomicLong原子更新长整数AtomicBoolean原子更新Boolean原子数组类型包括:AtomicIntegerArray原子更新整数数组中的元素AtomicLongArray原子更新长整数数组中的元素AtomicReferenceArray原子更新引用数组中的元素的类型。原子引用类型包括AtomicReference原子更新引用类型AtomicMarkableReference带标记位的原子更新引用类型,可以绑定一个布尔标记AtomicStampedReference带版本号的原子更新引用类型longintegerfieldsupdaterAtomicReferenceFieldUpdater简单介绍原子更新引用类型字段的updaterAtomicInteger中存储的值将AtomicInteger的当前值加1调用compareAndSet方法进行原子更新首先检查当前值是否等于expect如果相等则说明当前值没有被其他线程修改过,则将值更新到next,如果不相等则更新失败并返回false,程序将进入for循环再次执行compareAndSet操作。简而言之,CountDownLatchcountDownLatch类使一个线程在执行之前等待其他线程完成它们的执行。它是通过一个计数器来实现的,计数器的初始值就是线程数。每执行一个线程,就会调用countDown方法,计数器的值减1。当计数器的值为0时,表示所有线程都执行完了,等待的线程就可以继续工作了。它只能使用一次,不能重置。CyclicBarrier的简单描述CyclicBarrier的主要功能类似于countDownLatch。它还使用一个计数器让一个线程在执行之前等待其他线程完成它们的执行。但它可以重复使用(重置)。简单描述一下SemaphoreSemaphore就是信号量。Semaphore的构造函数参数接收一个int值,并设置一个计数器来指示可用的许可数,即最大并发数。使用acquire方法获取许可证,递减计数器,使用release方法返回许可证并递增计数器。如果此时计数器值为0,则线程进入休眠状态。简介ExchangerExchanger类可用于在两个线程之间交换信息。Exchanger对象可以简单理解为一个包含两个网格的容器,通过exchanger方法可以将信息填充到两个网格中。线程通过exchange方法交换数据。第一个线程执行完exchange方法后,会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时,两个线程就可以交换数据了。当两个格子都填满后,对象会自动交换两个格子的信息,然后返回给线程,从而实现两个线程的信息交换。简而言之,ConcurrentHashMapJDK7采用了锁分段技术。先把数据分成Segment数据段,然后给每个数据段分配一个锁。当一个线程占用锁访问一个段的数据时,其他段的数据也可以被其他线程访问。get不需要锁定,除非它读取一个空值。该方法先经过一个hash,然后用这个hash值通过hash运算定位段,最后通过hash算法定位元素。必须锁定。首先定位到Segment,然后执行插入操作。第一步判断Segment中的HashEntry数组是否需要扩容。第二步,定位添加元素的位置,然后放入数组中。JDK8的改进取消了分段锁机制,使用CAS算法来设置值。如果CAS失败,则使用synchronizedlock添加元素引入红黑树结构。当槽中的元素个数超过8且Node数组的容量大于64时,链表变为红黑树。使用更优化的方式来计算集合中元素的数量。Synchronized的底层实现原理Java对象的底层关联了一个监视器。使用synchronized时,JVM会根据使用环境找到对象的monitor,根据monitor的状态判断是否添加或解锁。如果成功锁定,它就成为monitor的唯一持有者,monitor在释放前不能被其他线程获取。JVM编译完成后,synchronized会生成monitorenter和monitorexit两条字节码指令来获取和释放monitor。两个字节码指令都需要一个引用类型参数来指示要锁定和解锁的对象。对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁是同步括号中的对象。当执行monitorenter指令时,它首先尝试获取对象锁。如果对象没有被锁定,或者当前线程已经持有锁,则锁计数器加1,执行monitorexit指令时锁计数器减1。一旦计数器达到0,锁就会被释放。Synchronized关键字的使用方法直接修改一个实例方法直接修改一个静态方法修改代码块简要说明偏向锁的概念是在JDK1.6中提出的。之所以有这个锁,是因为开发者发现在大多数情况下并没有锁的竞争,一个锁往往是同一个线程获取的。偏向锁不会主动释放,这样每次进入偏向锁时都会判断资源是否偏向自己。如果是偏向自身,则不需要额外操作,直接进入同步操作即可。应用流程如下:首先需要判断对象的MarkWord是否属于偏向模式,如果不属于则进入轻量级锁判断逻辑。否则,继续下一步判断;判断当前申请锁的线程ID与偏向锁自身记录的线程ID是否一致。如果一致,继续下一步判断,如果不一致,跳到第4步;判断是否需要重新偏置。如果没有,直接获取偏向锁;使用CAS算法改变对象的MarkWord,使线程ID部分被原来的线程ID替换。如果替换成功,则重新偏向完成,获得偏向锁。如果失败,说明存在多线程竞争,升级为轻量级锁。轻量级锁简介轻量级锁旨在减少重量级锁在没有竞争的情况下带来的性能消耗。应用过程如下:如果同步对象没有被锁定,虚拟机会在当前线程的栈帧中创建一个锁记录空间,用于存放锁对象当前MarkWord的副本。虚拟机使用CAS尝试将对象的MarkWord更新为指向锁记录的指针。如果更新成功,则表示该线程拥有锁,锁标志会变为00,表示处于轻量级锁状态。如果更新失败,说明至少有一个线程在与当前线程竞争。虚拟机检查对象的MarkWord是否指向当前线程的栈帧。如果指向当前线程的栈帧,说明当前线程已经拥有锁,直接进入同步块继续执行。如果没有,说明锁对象已经被其他线程占用了。如果两个或多个线程竞争同一个锁,轻量级锁将不再有效,将扩展为重量级锁,锁标志状态变为10。此时MarkWord存储一个指向重量级锁的指针.等待锁的线程也必须阻塞。简述锁优化策略,如自适应自旋、锁淘汰、锁粗化、锁升级等。java自旋锁线程获取锁失败后,可以采用这样的策略,不放弃CPU,不断重试。此操作也称为自旋锁。自适应自旋锁简介自适应自旋锁的自旋次数不再人为设定,通常由同一把锁上一次自旋时间和锁拥有者的状态决定。锁粗化简述锁粗化的思想是扩大加锁的范围,避免重复加锁和解锁。锁消除简述锁消除是一种更彻底的优化。在编译时,java编译器扫描运行上下文以移除不太可能有共享资源竞争的锁。简单的说,Lock和ReentrantLockLock的连接是java并发包的顶层接口。ReentrantLock是Lock最常见的实现,与synchronized一样可重入。ReentrantLock默认是非公平锁,可以通过构造函数指定公平锁。一旦使用公平锁,性能就会下降。简述AQSAQS(AbstractQuenedSynchronizer)抽象队列同步器。AQS将每个请求共享资源的线程封装成一个锁队列的一个节点(Node)来实现锁的分配。AQS是用于构建锁或其他同步组件的基本框架。它使用volatileint状态变量作为共享资源。如果线程获取资源失败,则进入同步队列等待;如果获取成功,将执行临界区中的代码,并释放资源。通知同步队列中的等待线程。子类通过继承同步器并实现其抽象方法getState、setState和compareAndSetState来改变同步状态。AQSAcquire/ReleaseExclusiveLock原理获取:(acquire)调用tryAcquire方法,安全获取线程同步状态。获取失败的线程会被构造为同步节点,通过addWaiter方法加入到同步队列的尾部,在队列中自旋。调用acquireQueued方法使节点无限循环获取同步状态,获取不到则阻塞。Release:(release)调用tryRelease方法释放同步状态。调用unparkSuccessor方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。AQS获取共享锁/??释放共享锁的原理获取锁(acquireShared)调用tryAcquireShared方法尝试获取同步状态,返回值不小于0表示可以获取到同步状态。Release(releaseShared)释放,并唤醒后续的等待节点。线程池类型newCachedThreadPool是一个可缓存的线程池。您可以设置最小线程数和最大线程数。线程空闲1分钟后自动销毁。newFixedThreadPool指定线程池中工作线程的数量。newSingleThreadExecutor单线程执行器。newScheduleThreadPool是一个线程池,支持指定数量的工作线程用于定时任务。newSingleThreadScheduledExecutor是一个支持定时任务的单线程Executor。
