精华内容
下载资源
问答
  • 多线程面试题

    2019-10-13 15:42:54
    多线程面试题
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    史上最强多线程面试47(含答案),建议收藏 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素?...

    史上最强多线程面试47题(含答案),建议收藏

    金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~

    1、并发编程三要素?

    1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

    3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

    2、实现可见性的方法有哪些?

    synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3、多线程的价值?

    1)发挥多核CPU的优势

    多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

    2)防止阻塞

    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

    3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    4、创建线程的有哪些方式?

    1)继承Thread类创建线程类

    2)通过Runnable接口创建线程类

    3)通过Callable和Future创建线程

    4)通过线程池创建

    5、创建线程的三种方式的对比?

    1)采用实现Runnable、Callable接口的方式创建多线程。

    优势是:

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

    2)使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

    3)Runnable和Callable的区别

    Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    Call方法可以抛出异常,run方法不可以。
    运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    6、线程的状态流转图

    线程的生命周期及五种基本状态:

    7、Java线程具有五中基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就
    绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    8、什么是线程池?有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

    java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

    9、四种线程池的创建:

    1)newCachedThreadPool创建一个可缓存线程池

    2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

    3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    10、线程池的优点?

    1)重用存在的线程,减少对象创建销毁的开销。

    2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

    3)提供定时执行、定期执行、单线程、并发数控制等功能。

    11、常用的并发工具类有哪些?

    CountDownLatch
    CyclicBarrier
    Semaphore
    Exchanger

    12、CyclicBarrier和CountDownLatch的区别

    1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

    2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

    4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

    13、synchronized的作用?

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

    synchronized既可以加在一段代码上,也可以加在方法上。

    14、volatile关键字的作用

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

    15、什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

    16、CAS的问题

    1)CAS容易造成ABA问题

    一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

    2) 不能保证代码块的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    3)CAS造成CPU利用率增加

    之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17、什么是Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

    Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

    18、什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

    19、AQS支持两种同步方式:

    1)独占式

    2)共享式

    这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    20、ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    22、synchronized和ReentrantLock的区别

    synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

    1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

    2)ReentrantLock可以获取各种锁的信息

    3)ReentrantLock可以灵活地实现多路通知

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

    23、什么是乐观锁和悲观锁

    1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

    2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    24、线程B怎么知道线程A修改了变量

    volatile修饰变量
    synchronized修饰修改变量的方法
    wait/notify
    while轮询

    25、synchronized、volatile、CAS比较

    synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
    volatile提供多线程共享变量可见性和禁止指令重排序优化。
    CAS是基于冲突检测的乐观锁(非阻塞)

    26、sleep方法和wait方法有什么区别?

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

    27、ThreadLocal是什么?有什么用?

    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

    28、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

    29、多线程同步有哪几种方法?

    Synchronized关键字,Lock锁实现,分布式锁等。

    30、线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    1)线程体中调用了yield方法让出了对cpu的占用权利

    2)线程体中调用了sleep方法使线程进入睡眠状态

    3)线程由于IO操作受到阻塞

    4)另外一个更高优先级线程出现

    5)在支持时间片的系统中,该线程的时间片用完

    31、ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    32、Linux环境下如何查找哪个线程使用CPU最长

    1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

    2)top -H -p pid,顺序不能改变

    33、Java死锁以及如何避免?

    Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源。

    Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    34、死锁的原因

    1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

    2)默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    35、怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    36、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    37、什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

    38、如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

    1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

    2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

    39、Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    40、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    41、什么是自旋

    很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

    42、Java
    Concurrency API中的Lock接口(Lock
    interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    可以使锁更公平
    可以使线程在等待锁的时候响应中断
    可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    可以在不同的范围,以不同的顺序获取和释放锁

    43、单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

    1)饿汉式单例模式的写法:线程安全

    2)懒汉式单例模式的写法:非线程安全

    3)双检锁单例模式的写法:线程安全

    44、Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    45、Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池

    46、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    47、同步方法和同步块,哪个是更好的选择?

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

    48、Java线程数过多会造成什么异常?

    1)线程的生命周期开销非常高

    2)消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

    3)降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。

    展开全文
  • java经典多线程面试题

    2018-04-14 01:31:32
    java经典多线程面试题。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
  • 主要为大家详细介绍了2018版java多线程面试题集合及答案,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 常见的多线程经典面试题和答案,包含了面试中常见了多线程问题。
  • 15个顶级Java多线程面试题答案
  • 面试题和答案. 有多线程, 数据库,框架题目等知识点
  • Java多线程面试题(面试必备)

    万次阅读 多人点赞 2020-05-26 01:15:38
    文章目录一、多线程基础基础知识1. 并发编程1.1 并发编程的优缺点1.2 并发编程的三要素1.3 并发和并行有和区别1.4 什么是多线程多线程的优劣?2. 线程与进程2.1 什么是线程与进程2.2 线程与进程的区别2.3 用户线程...

    能力有限,初级菜🐔,多线程可是一块庞大的知识块,慢慢总结吧!

    文章目录

    一、多线程基础基础知识

    1. 并发编程

    1.1 并发编程的优缺点

    优点

    • 充分利用多核CPU的计算能力,通过并发编程的形式将多核CPU的计算能力发挥到极致,性能得到提升。
    • 方面进行业务的拆分。提高系统并发能力和性能:高并发系统的开发,并发编程会显得尤为重要,利用好多线程机制可以大大提高系统的并发能力及性能;面对复杂的业务模型,并行程序会比串行程序更适应业务需求,而并发编程更适合这种业务拆分。cai

    缺点

    • 并发编程的目的是为了提高程序的执行效率,提高程序运行速度,但并发编程并不是总能提高性能,有时还会遇到很多问题,例如:内存泄漏,线程安全,死锁等。

    1.2 并发编程的三要素

    并发编程的三要素:(也是带来线程安全所在)

    1. 原子性:原子是不可再分割的最小单元,原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。
    2. 可见性:一个线程对共享变量的修改,另一个线程能看到(synchronized,volatile)
    3. 有序性:程序的执行顺序按照代码的先后顺序

    线程安全的问题原因有:

    1. 线程切换带来的原子性问题
    2. 缓存导致的可见性问题
    3. 编译优化带来的有序性问题
    

    解决方案:

    • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
    • synchronized、volatile、LOCK,可以解决可见性问题
    • Happens-Before 规则可以解决有序性问题

    1.3 并发和并行有和区别

    并发:多个任务在同一个CPU上,按照细分的时间片轮流交替执行,由于时间很短,看上去好像是同时进行的。
    并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行。
    串行:有n个任务,由一个线程按照顺序执行。

    1.4 什么是多线程,多线程的优劣?

    定义:多线程是指程序中包含多个流,即在一个程序中可以同时进行多个不同的线程来执行不同的任务
    优点:

    • 可以提高CPU的利用率,在多线程中,一个线程必须等待的时候,CPU可以运行其它线程而不是等待,这样就大大提高了程序的效率,也就是说单个程序可以创建多个不同的线程来完成各自的任务。
      缺点:
    • 线程也是程序,线程也需要占内存,线程也多内存也占的也多。
    • 多线程需要协调和管理,所以需要CPU跟踪线程。
    • 线程之间共享资源的访问会相互影响,必须解决禁用共享资源的问题。

    2. 线程与进程

    2.1 什么是线程与进程

    进程:内存中运行的运用程序,每个进程都有自己独立的内存空间,一个进程可以由多个线程,例如在Windows系统中,xxx.exe就是一个进程。
    线程:进程中的一个控制单元,负责当前进程中的程序执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。

    2.2 线程与进程的区别

    根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位。
    资源开销:每个进程都有自己独立的代码和空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
    包含关系:如果一个进程内有多个线程,则执行的过程不是一条线的,而是多条线(多个线程),共同完成;线程是进程的一部分,可以把线程看作是轻量级的进程。
    内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

    2.3 用户线程与守护线程

    用户(User)线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程都是用户线程。
    守护(Daemon)线程:运行在后台,为其它前台线程服务,也可以说守护线程是JVM非守护线程的”佣人“,一旦所有线程都执行结束,守护线程会随着JVM一起结束运行。
    main函数就是一个用户线程,main函数启动时,同时JVM还启动了好多的守护线程,如垃圾回收线程,比较明显的区别时,用户线程结束,JVM退出,不管这个时候有没有守护线程的运行,都不会影响JVM的退出。

    2.4 什么是线程死锁

    死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
    如图,线程A拥有的资源2,线程B拥有的资源1,此时线程A和线程B都试图去拥有资源1和资源2,但是它们的🔒还在,因此就出现了死锁。
    在这里插入图片描述

    2.5 形成死锁的四个必要条件

    1. 互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
    2. 请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
    3. 不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
    4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。

    2.6 如何避免死锁

    我们只需破坏形参死锁的四个必要条件之一即可。
    破坏互斥条件:无法破坏,我们的🔒本身就是来个线程(进程)来产生互斥
    破坏请求与保持条件:一次申请所有资源
    破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
    破坏循环等待条件:按序来申请资源。

    2.7 什么是上下文的切换

    当前任务执行完,CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换会这个任务时可以继续执行下去,任务从保存到再加载执行就是一次上下文切换。

    3. 创建线程

    3.1 创建线程的四种方式

    1. 继承Thread类
    2. 实现Runnable接口
    3. 实现Callable接口
    4. Executors工具类创建线程池

    3.2 Runnable接口和Callable接口有何区别

    相同点

    1. Runnable和Callable都是接口
    2. 都可以编写多线程程序
    3. 都采用Thread.start()启动线程

    不同点

    1. Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
    2. Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。

    :Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。

    3.2 run()方法和start()方法有和区别

    每个线程都是通过某个特定的Thread对象对于的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
    start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。

    start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。

    3.3 为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法

    这是一个常问的面试题,new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
    如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
    小结
    调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。

    3.4 什么是Callable和Future

    Callable接口也类似于Runnable接口,但是Runnable不会接收返回值,并且无法抛出返回结果的异常,而Callable功能更强大,被线程执行后,可有返回值,这个返回值可以被Future拿到,也就是说Future可以拿到异步执行任务的返回值。
    Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。

    3.5 什么是FutureTask

    FutureTask是一个异步运算的任务,FutureTask里面可以可以传入Callable实现类作为参数,可以对异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。只有当结果完成之后才能取出,如果尚未完成get方法将堵塞。一个Future对象可以调用Callable和Runable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    4. 线程状态和基本操作

    4.1 线程声明周期的6种状态

    很多地方说线程有5种状态,但实际上是6中状态,可以参考Thread类的官方api

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
    

    如图:
    在这里插入图片描述
    新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
    可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
    被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
    计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
    无线等待状态:获取锁对象后,调用wait()方法,释放锁进入无线等待状态
    锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。

    堵塞状态的详解:
    在这里插入图片描述

    4.2 Java用到的线程调度算法是什么?

    计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获取到CPU的使用权才能执行指令,所谓多线程的并发运行,其实从宏观上看,各线程轮流获取CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待,CPU的调度,JVM有一项任务就是负责CPU的调度,线程调度就是按照特定的机制为多个线程分配CPU的使用权。
    有两种调度模型:分时调度和抢占式调度
    分时调度就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
    抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。

    4.2 Java线程调度策略

    线程调度优先选择优先级高的运行,但是如果出现一下情况,就会终止运行(不是进入死亡状态):

    1. 线程调用了yield方法让出CPU的使用权,线程进入就绪状态。
    2. 线程调用sleep()方法,使其进入计时状态
    3. 线程由于IO受阻
    4. 另一个更高的优先级线程出现
    5. 在支持的时间片系统中,改线程的时间片用完。

    4.3 什么是线程调度(Thread Scheduler)和时间分片(Time Slicing )

    线程调度是一个操作系统服务,它负责为储在Runnable状态的线程分配CPU时间片,一旦我们创建一个线程并启动它,它的执行便依赖线程调度器的实现。
    时间分片是指CPU可用时间分配给Runnable的过程,分配的时间可以根据线程优先级或线程等待时间。

    4.4 Java线程同步和线程调度的相关方法

    1. wait():调用后线程进入无限等待状态,并释放所持对象的锁
    2. sleep():使一个线程进入休眠状态(堵塞状态),带有对象锁,是一个静态方法,需要处理InterruptException异常。
    3. notify():唤醒一个处于等待状态的线程(无线等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
    4. notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。

    4.5 sleep()和wait()有什么区别

    两者都可以使线程进入等待状态

    • 类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
    • 是否释放锁:sleep()不释放锁,wait()释放锁
    • 用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
    • 用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。

    4.6 你是如何调用wait()方法的,使用if还是循环

    处以等待状态的线程可能会收到错误警告或伪唤醒,如果不在循环中检查等待条件,程序可能会在没有满足条件的时候退出。

    synchronized (monitor) {
        //  判断条件谓词是否得到满足
        while(!locked) {
            //  等待唤醒
            monitor.wait();
        }
        //  处理其他的业务逻辑
    }
    
    

    4.7 为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中

    Java中任何对象都可以被当作锁对象,wait(),notify(),notifyAll()方法用于等待获取唤醒对象去获取锁,Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。

    有人会说,既然是线程放弃对象锁,那也可以把wait()放到Thread类中,新定义线程继承Thread类,也无需重新定义wait(),
    然而,这样做有一个很大的问题,因为一个线程可以持有多把锁,你放弃一个线程时,到底要放弃哪把锁,当然了这种设计不能不能实现,只是管理起来比较麻烦。
    综上:wait(),notify(),notifyAll()应该要被定义到Object类中。

    4.8 为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?

    wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。
    如对象调用wait()方法后持有的锁对象就释放出去,等待下一个线程来获取。
    如对象调用notifyAll()要唤醒等待中的线程,也要讲自身用于的锁对象释放,让就绪状态中的线程竞争获取锁。
    由于这些方法都需要线程持有锁对象,这样只能通过同步来实现,所以它们只能在同步块或同步方法中被调用。

    4.9 Thread的yiele方法有什么作用?

    让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。

    4.10 为什么Thread的sleep和yield是静态的?

    Thread类的sleep()和yield()方法将在当前正在运行的线程上工作,所以其它处于等待状态的线程调用它们是没有意义的,所以设置为静态最合适。

    4.11 线程sleep和yield方法有什么区别

    • 线程调用sleep()方法进入堵塞状态,醒来后因为(没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
    • sleep()方法使用时需要处理InterruptException异常,而yield没有。
    • sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。

    4.12 如何停止一个正在运行的线程?

    1. 使用stop方法终止,但是这个方法已经过期,不被推荐使用。
    2. 使用interrupt方法终止线程
    3. run方法执行结束,正常退出

    4.13 如何在两个线程间共享数据?

    两个线程之间共享变量即可实现共享数据。
    一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用。

    4.14 同步代码块和同步方法怎么选?

    同步块是更好的选择,因为它不会锁着整个对象,当然你也可以然它锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有不关联的同步块,这通常会导致停止继续执行,并等待获取这个对象锁。
    同步块扩展性比较好,只需要锁住代码块里面相应的对象即可,可以避免死锁的产生。
    原则:同步范围也小越好。

    4.15 什么是线程安全?Servlet是线程安全吗?

    线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,能程序能够正确完成。
    Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
    Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
    springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
    Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
    如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程。

    4.16 线程的构造方法,静态块是被哪个线程类调用的?

    线程的构造方法,静态块是被哪个线程类调用的?
    该线程在哪个类中被new出来,就是在哪个被哪个类调用,而run方法是线程类自身调用的。
    例子:mian函数中new Thread2,Thread2中new Thread1
    在这里插入图片描述
    thread1线程的构造方法,静态块是thread2线程调用的,run方法是thread1调用的。
    thread2线程的构造方法,静态块是main线程调用的,run方法是thread2调用的。

    4.17 Java中是如何保证多线程安全的?

    1. 使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
    2. 使用自动锁,synchronized锁
    3. Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法

    4.18 线程同步和线程互斥的区别

    线程同步:当一个线程对共享数据进行操作的时候,在没有完成相关操作时,不允许其它的线程来打断它,否则就会破坏数据的完整性,必然会引起错误信息,这就是线程同步。
    线程互斥
    而线程互斥是站在共享资源的角度上看问题,例如某个共享资源规定,在某个时刻只能一个线程来访问我,其它线程只能等待,知道占有的资源者释放该资源,线程互斥可以看作是一种特殊的线程同步。
    实现线程同步的方法

    1. 同步代码块:sychronized(对象){} 块
    2. 同步方法:sychronized修饰的方法
    3. 使用重入锁实现线程同步:reentrantlock类的锁又互斥功能,Lock lock = new ReentrantLock(); Lock对象的ock和unlock为其加锁

    4.19 你对线程优先级有什么理解?

    每个线程都具有优先级的,一般来说,高优先级的在线程调度时会具有优先被调用权。我们可以自定义线程的优先级,但这并不能保证高优先级又在低优先级前被调用,只是说概率有点大。
    线程优先级是1-10,1代表最低,10代表最高。
    Java的线程优先级调度会委托操作系统来完成,所以与具体的操作系统优先级也有关,所以如非特别需要,一般不去修改优先级。

    4.20 谈谈你对乐观锁和悲观锁的理解?

    乐观锁:每个去拿数据的时候都认为别人不会修改,所以不会都不会上锁,但是在更新的时候会判断一下在此期间有没有去更新这个数据。所以乐观锁使用了多读的场合,这样可以提高吞吐量,像数据库提供的类似write_condition机制,都是用的乐观锁,还有那个原子变量类,在java.util.concurrent.atomic包下
    悲观锁:总是假设最坏的情况,每次去拿数据的时候都会认为有人会修改,所以每次在拿数据的时候都会上锁。这样别的对象想拿到数据,那就必须堵塞,直到拿到锁。传统的关系型数据库用到了很多这种锁机制,比如读锁,写锁,在操作之前都会先上锁,再比如Java的同步代码块synchronized/方法用的也是悲观锁。

    展开全文
  • C#面试题 包括 ADO.net 多线程等 C#面试题 包括 ADO.net 多线程等 C#面试题 包括 ADO.net 多线程等 C#面试题 包括 ADO.net 多线程等 C#面试题 包括 ADO.net 多线程
  • 【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】多线程面试题【BAT必备】...
  • 几率大的多线程面试题(含答案)

    万次阅读 多人点赞 2019-04-28 15:39:12
    其他篇章:Java校招极大几率出的面试题(含答案)----汇总 ...多线程中 synchronized 锁升级的原理是什么? Synchronized和Lock的区别? synchronized和ReentrantLock有什么区别呢? 使用场景 线程池的工作原理,...

    其他面试题类型汇总:
    Java校招极大几率出的面试题(含答案)----汇总
    几率大的网络安全面试题(含答案)
    几率大的多线程面试题(含答案)
    几率大的源码底层原理,杂食面试题(含答案)
    几率大的Redis面试题(含答案)
    几率大的linux命令面试题(含答案)
    几率大的杂乱+操作系统面试题(含答案)
    几率大的SSM框架面试题(含答案)
    几率大的数据库(MySQL)面试题(含答案)
    几率大的JVM面试题(含答案)
    几率大的现场手撕算法面试题(含答案)
    临时抱佛脚必备系列(含答案)

    注:知识还在积累中,不能保证每个回答都满足各种等级的高手们,若发现有问题的话,本人会尽快完善。
    。◕‿◕。


    本文面试题如下
    线程和进程的区别?
    Thread和Runnable的关系,区别
    synchronized底层如何实现?锁优化,怎么优化?多线程中 synchronized 锁升级的原理是什么?
    Synchronized和Lock的区别?
    synchronized和ReentrantLock有什么区别呢? 使用场景
    线程池的工作原理,Java 并发类库提供的线程池有哪几种? 分别有什么特点?
    ReentrantLock 底层实现;
    AtomicInteger底层实现原理是什么?
    ThreadLocal的底层原理
    voliate 的实现原理
    happens-before原则有哪些
    synchronized 和 volatile 的区别是什么?
    线程池的几种方式(类型)与使用场景,线程池都有哪些状态?
    CountDownLatch CyclicBarrier之间的区别,使用场景
    写一个死锁
    死锁是什么,产生死锁的条件。如何避免死锁?怎么定位死锁线程?
    Java中synchronized 和 ReentrantLock 有什么不同?
    volatile 变量和 atomic 变量有什么不同?
    Java多线程中调用wait() 和 sleep()方法有什么不同?
    什么是Java Timer类?如何创建一个有特定时间间隔的任务?
    什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
    一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转移。
    线程的状态有哪些
    线程的 run() 和 start() 有什么区别?
    Runnable和Callable的区别
    什么是CAS
    什么是AQS
    Semaphore有什么作用
    进程间的通信的几种方式
    为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?
    为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?
    CopyOnWrite是什么?
    stop()和 suspend()方法的区别
    一个线程运行时发生异常会怎样?
    分布式锁的实现,目前比较常用的有以下几种方案:
    Hashtable的size()方法为什么要做同步?
    ConcurrentHashMap 的size()方法如何实现同步的?
    线程池会有哪些漏洞/安全问题
    notify和notifyAll方法的区别
    如何判断线程是否安全?


    线程和进程的区别?

    根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
    地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
    关系:一个程序至少一个进程,一个进程至少一个线程。
    Thread和Runnable的关系,区别
    在这里插入图片描述
    1) 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
    2) Runnable 是接口。Thread 是类,且实现了Runnable接口。
    3) 实现Runnable接口相比继承Thread类有如下好处:避免继承的局限,一个类可以实现多个接口。

    synchronized底层如何实现?锁优化,怎么优化?

    synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

    原理:

    synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

    底层实现:

    1)同步代码块是使用monitorenter和monitorexit指令实现的, ,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
    2)同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。 synchronized方法是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示 Klass 做为锁对象。

    Java对象头和monitor是实现synchronized的基础!
    synchronized存放的位置:
    synchronized用的锁是存在Java对象头里的。
    其中, Java对象头包括:
    Mark Word(标记字段): 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。它是实现轻量级锁和偏向锁的关键
    Klass Pointer(类型指针): 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
    monitor: 可以把它理解为一个同步工具, 它通常被描述为一个对象。 是线程私有的数据结构。

    锁优化,怎么优化?

    jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问而没有其他JavaThread访问的对象。( HotSpot JVM/JRockit JVM是支持锁降级的)
    偏斜锁
    当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。
    自旋锁
    自旋锁 for(;;)结合cas确保线程获取取锁
    就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
    轻量级锁
    引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
    重量级锁
    重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

    多线程中 synchronized 锁升级的原理是什么?

    synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

    锁的升级的目的

    锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

    Synchronized和Lock的区别?

    1)实现层面:synchronized(JVM层面)、Lock(JDK层面)
    2)响应中断:Lock 可以让等待锁的线程响应中断,而使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
    3)立即返回:可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,而synchronized却无法办到;
    4)读写锁:Lock可以提高多个线程进行读操作的效率
    5)可实现公平锁:Lock可以实现公平锁,而sychronized天生就是非公平锁
    6)显式获取和释放:synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

    synchronized和ReentrantLock有什么区别呢?

    synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
    ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制。编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

    1)ReentrantLock 使用起来比较灵活,可以对获取锁的等待时间进行设置,可以获取各种锁的信息,但是必须有释放锁的配合动作;
    2)ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
    3)ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
    4)synchronized是关键字,ReentrantLock是类
    5)Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的
    6)ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
    7)ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
    8)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

    场景:
    在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发

    线程池的工作原理 ,Java 并发类库提供的线程池有哪几种? 分别有什么特点?

    1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作
    线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
    2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这
    个工作队列里。如果工作队列满了,则进入下个流程。
    3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程
    来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
    在这里插入图片描述

    1.先讲下作用

    减少资源的开销 可以减少每次创建销毁线程的开销提高响应速度 由于线程已经创建成功提高线程的可管理性

    2.讲实现线程池

    主要有两部分组成,多个工作线程和一个阻塞队列。其中 工作线程是一组已经处在运行中的线程,它们不断地向阻塞队列中领取任务执行。而 阻塞队列用于存储工作线程来不及处理的任务。

    3.细分讲下线程的组成创建一个线程池需要要的一些核心参数。

    corePoolSize:基本线程数量 它表示你希望线程池达到的一个值。线程池会尽量把实际线程数量保持在这个值上下。
    maximumPoolSize:最大线程数量 这是线程数量的上界。 如果实际线程数量达到这个值: 阻塞队列未满:任务存入阻塞队列等待执行 阻塞队列已满:调用饱和策略 。keepAliveTime:空闲线程的存活时间 当实际线程数量超过corePoolSize时,若线程空闲的时间超过该值,就会被停止。 PS:当任务很多,且任务执行时间很短的情况下,可以将该值调大,提高线程利用率。
    timeUnit:keepAliveTime的单位
    runnableTaskQueue:任务队列 这是一个存放任务的阻塞队列,可以有如下几种选择:1)ArrayBlockingQueue 它是一个由数组实现的阻塞队列,FIFO。
    2) LinkedBlockingQueue 它是一个由链表实现的阻塞队列,FIFO。 吞吐量通常要高于
    3)ArrayBlockingQueue。fixedThreadPool使用的阻塞队列就是它。 它是一个无界队列。 4)SynchronousQueue 它是一个没有存储空间的阻塞队列,任务提交给它之后必须要交给一条工作线程处理;如果当前没有空闲的工作线程,则立即创建一条新的工作线程。 cachedThreadPool用的阻塞队列就是它。 它是一个无界队列。
    5)PriorityBlockingQueue 它是一个优先权阻塞队列。handler:饱和策略 当实际线程数达到maximumPoolSize,并且阻塞队列已满时,就会调用饱和策略。
    AbortPolicy 默认。直接抛异常。 CallerRunsPolicy 只用调用者所在的线程执行任务。 DiscardOldestPolicy 丢弃任务队列中最久的任务。 DiscardPolicy 丢弃当前任务。

    4.运行机制

    当有请求到来时:
    1.若当前实际线程数量 少于 corePoolSize,即使有空闲线程,也会创建一个新的工作线程;2 若当前实际线程数量处于corePoolSize和maximumPoolSize之间,并且阻塞队列没满,则任务将被放入阻塞队列中等待执行;
    3.若当前实际线程数量 小于 maximumPoolSize,但阻塞队列已满,则直接创建新线程处理任务;
    4.若当前实际线程数量已经达到maximumPoolSize,并且阻塞队列已满,则使用饱和策略。

    Java 并发类库提供的线程池有哪几种? 分别有什么特点?

    Executors 目前提供了 5 种不同的
    线程池创建配置:
    1)newCachedThreadPool():用来处理大量短时间工作任务的线程池。当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;其内部使用 SynchronousQueue 作为工作队列。
    2)newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。
    3)newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态
    4)newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
    5)newWorkStealingPool(int parallelism),Java 8 才加入这个创建方法,并行地处理任务,不保证处理顺序。
    6)ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

    ReentrantLock 底层实现

    https://blog.csdn.net/u011202334/article/details/73188404

    AQS原理:

    AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。

    概念+实现:

    ReentrantLock实现了Lock接口,是AQS( 一个用来构建锁和同步工具的框架, AQS没有 锁之 类的概念)的一种。加锁和解锁都需要显式写出,注意一定要在适当时候unlock。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。

    和synhronized相比:

    synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
    1)可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
    2)可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是
    不可中断锁,而ReentrantLock则z,dz提供了中断功能。
    3)公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

    lock()和unlock()是怎么实现的呢?

    由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。而Sync是ReentrantLock的内部类, 其扩展了AbstractQueuedSynchronizer。

    lock():
    final void lock() {
    if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
    else
    acquire(1);
    }
    }
    

    首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。( “非公平”即体现在这里)。
    设置state失败,走到了else里面。我们往下看acquire。

    1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

    2. 第二步,入队。( 自旋+CAS组合来实现非阻塞的原子操作)

    3. 第三步,挂起。 让已经入队的线程尝试获取锁,若失败则会被挂起

    public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    }
    

    流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,
    如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

    public void unlock() {
    sync.release(1);
    }
    public final boolean release(int arg) {
    if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
    return true;
    }
    return false;
    }
    

    在这里插入图片描述
    在这里插入图片描述

    AtomicInteger底层实现原理是什么?

    AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS(compare-and-swap)技术。从 AtomicInteger 的内部属性可以看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操作,以 volatile 的 value 字段,记录数值,以保证可见性,Unsafe 会利用 value 字段的内存地址偏移,直接完成操作。

    voliate 的实现原理

    volatile可以保证线程可见性且禁止指令重排序,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的, 加入volatile关键字时,汇编后会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。。
    happen-before原则保证了程序的“有序性,对volatile变量的写操作 happen-before 后续的读操作.
    当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
    当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。
    对该变量的写操作之后,编译器会插入一个写屏障。对该变量的读操作之前,编译器会插入一个读屏障。
    线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。
    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    happens-before原则有哪些:

    程序顺序规则:单线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;

    锁定规则:一个unlock操作先行发生于对同一个锁的lock操作;

    volatile变量规则:对一个Volatile变量的写操作先行发生于对这个变量的读操作;

    线程启动规则:Thread对象的start()方法先行发生于此线程的其他动作;

    线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

    线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

    对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始;

    传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

    synchronized 和 volatile 的区别是什么?

    * volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
    * volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
    * volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
    

    ThreadLocal的底层原理

    ThreadLocal,该类提供了线程局部 (thread-local) 变量,ThreadLocal会为每个线程创建变量的副本,线程之间互不影响,这样就不存在线程安全问题。在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals用来存储实际的变量副本容器, 键值为当前ThreadLocal变量,value为变量副本。
    1)初始时,在Thread的threadLocals为空,调用ThreadLocal变量调用get()方法或者set()方法,就会对threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals中。
    2)然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

    概括:

    ThreadLocal,很多地方叫做线程本地变量,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。ThreadLocal相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。在进行get之前,必须先set,否则会报空指针异常;否则必须重写initialValue()方法。
    场景:数据库连接和session管理
    该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
    每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
    使用:
    set(obj):向当前线程中存储数据 get():获取当前线程中的数据 remove():删除当前线程中的数据

    实现原理:

    ThreadLocal并不维护ThreadLocalMap(ThreadLocalMap是Thread的)并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。 每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值( ThreadLocalMap 是个弱引用类,内部 一个Entry由ThreadLocal对象和Object构成,
    为什么要用弱引用呢?
    如果是直接new一个对象的话,使用完之后设置为null后才能被垃圾收集器清理,如果为弱引用,使用完后垃圾收集器自动清理key,程序员不用再关注指针。)
    操作细节
    进行set,get等操作都是首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key ,再做相应的处理。
    内存泄露问题
    在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。
    每次操作set、get、remove操作时,ThreadLocal都会将key为null的Entry删除,从而避免内存泄漏。
    当然,当 如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法,此时该仍然可能会导致内存泄漏。 这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。

    使用场景

    Web系统Session的存储
    当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。

    ThreadLocal是如何为每个线程创建变量的副本的

    首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
      初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
      然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
    总结:
    1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
    2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
    3)在进行get之前,必须先set,否则会报空指针异常;
       如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

    线程池的几种方式与使用场景

    1、newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。适用:执行长期的任务,性能好很多
    2、newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:
    1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
    2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
    适用:执行很多短期异步的小程序或者负载较轻的服务器

    3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。
    适用:一个任务一个任务执行的场景

    4、newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。(这种线程池原理暂还没完全了解透彻)
    适用:周期性执行任务的场景

    线程池都有哪些状态?

    RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。

    SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。

    STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。

    TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。

    TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

    CountDownLatch CyclicBarrier之间的区别,使用场景

    概括性的:

    CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
    CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
    细分:
    CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。
    cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
    2)CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
    3)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

    使用场景:

    需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,可以使用CountDownLatch
    CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。
    Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限,作用是限制某段代码块的并发数。

    写一个死锁

    觉得这个问题真的很不错,经常说的死锁四个条件,背都能背上,那写一个看看,思想为:定义两个ArrayList,将他们都加上锁A,B,线程1,2,1拿住了锁A ,请求锁B,2拿住了锁B请求锁A,在等待对方释放锁的过程中谁也不让出已获得的锁。
    在这里插入图片描述
    在这里插入图片描述

    死锁是什么,产生死锁的条件。如何避免死锁?

    死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
    互斥条件:一个资源每次只能被一个进程使用。
    请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 避免: 1)尽量避免使用多个锁,并且只有需要时才持有锁定位死锁,嵌套的 synchronized 或者 lock 非常容易出问题。
    2)如果必须使用多个锁,尽量设计好锁的获取顺序
    3)使用带超时的方法,为程序带来更多可控性。Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait。
    4)尽量不要几个功能用同一把锁。

    怎么定位死锁线程?

    最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。
    如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。
    首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。
    其次,调用 jstack 获取线程栈:${JAVA_HOME}\bin\jstack your_pid
    然后,分析得到的输出,具体片段如下:
    在这里插入图片描述

    Java中synchronized 和 ReentrantLock 有什么不同?

    通过Lock接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等

    volatile 变量和 atomic 变量有什么不同?

    这是个有趣的问题。首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

    Java多线程中调用wait() 和 sleep()方法有什么不同?

    Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。需要注意的是,sleep()并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为Runnable,并且根据线程调度,它将得到执行。
    java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。
    java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。
    在这里插入图片描述
    在这里插入图片描述

    什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
    int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
    在 java.util.concurrent.atomic 包中添加原子变量类之后,这种情况才发生了改变。所有原子变量类都公开比较并设置原语(与比较并交换类似),这些原语都是使用平台上可用的最快本机结构(比较并交换、加载链接/条件存储,最坏的情况下是旋转锁)来实现的。 java.util.concurrent.atomic 包中提供了原子变量的 9 种风格( AtomicInteger; AtomicLong; AtomicReference; AtomicBoolean;原子整型;长型;引用;及原子标记引用和戳记引用类的数组形式,其原子地更新一对值)。

    一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转移。

    Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误

    线程的状态有哪些 :

    状态:在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State:
    新建(NEW)new,表示线程被创建出来还没真正启动的状态
    就绪,运行(RUNNABLE)runnable,表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。
    在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。
    阻塞(BLOCKED)blocked,这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。
    等待(WAITING)waiting,表示正在等待其他线程采取某些操作。
    计时等待(TIMED_WAIT)timed_wait:其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,
    终止(TERMINATED)terminated,不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    线程的 run() 和 start() 有什么区别?

    start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
    底层start()方法是使用C语言写的,调用JVM_startThread,开启子线程,然后调用里面的run方法
    在这里插入图片描述

    Runnable和Callable的区别

    Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    Call方法可以抛出异常,run方法不可以。
    运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。
    cas是一种乐观锁。CAS
    操作包含三个操作数 ——内存位置(V)、预期原值(A)和新值(B)。 当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
    缺点:
    1)ABA问题:一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了。
    解决办法:可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。
    2)CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
    3)CAS造成CPU利用率增加。

    什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore
    技术是 CAS自旋Volatile变量:它使用了一个Volatile成员变量表示同步状态,通过CAS修改该变量的值,修改成功的线程表示获取到该锁;若没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。
    AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)

    (final)不可变对象对多线程有什么帮助?为什么喜欢用final变量

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
    Final 变量在并发当中,原理是通过禁止cpu的指令集重排序,保证了对象的内存可见性, final 域能确保初始化过程的安全性, 防止对象引用在对象被完全构造完成前被其他线程拿到并使用( fianl 可以保证正在创建中的对象不能被其他线程访问到)

    Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
    Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。

    进程间的通信的几种方式

    管道(pipe)及命名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
    信号(signal):用于通知接收进程某个事件已经发生;
    消息队列:
    共享内存:可以说这是最有用的进程间通信方式。多个进程可以访问同一块内存空间
    信号量:进程之间及同一种进程的不同线程之间得同步和互斥手段
    套接字:用于网络中不同机器之间的进程间通信

    线程同步的方式

    互斥量 Synchronized/Lock:信号量 Semphare:事件(信号),Wait/Notify

    为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

    在Java中,任意对象都可以当作锁来使用,由于锁对象的任意性,所以这些通信方法需要被定义在Object类里。
    为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?其目的在于确保等待线程从Wait()返回时能够感知通知线程对共享变量所作出的修改。如果不在同步范围内使用,就会抛出java.lang.IllegalMonitorStateException的异常。

    CopyOnWrite是什么?

    即写时复制的容器,适用于读操作远多于修改操作的并发场景中,先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。是一种读写分离的思想,读和写不同的容器

    stop()和 suspend()方法的区别

    反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。
    suspend()方法容易发生死锁。调用 suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。 此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。

    一个线程运行时发生异常会怎样?

    如果异常没有被捕获该线程将会停止执行。

    分布式锁的实现,目前比较常用的有以下几种方案:

    分布式锁应该是怎么样的?
    1)可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
    2)这锁要是一把可重入锁(避免死锁)
    3)这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
    4)有高可用的获取锁和释放锁功能
    5)获取锁和释放锁的性能要好

    1)基于数据库实现分布式锁

    1)最简单的方式可能就是直接创建一张锁表
    1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
    2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
    3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
    4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

    数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
    没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
    非阻塞的?搞一个while循环,直到insert成功再返回成功。
    非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

    2) 借助数据中自带的锁来实现分布式的锁(select *** for update)

    在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。 通过connection.commit()操作来释放锁

    基于缓存(redis,memcached,tair)实现分布式锁

    基于 REDIS 的 SETNX()、EXPIRE() 方法( 设置过期时间)做分布式锁

    基于Zookeeper实现分布式锁

    每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

    Hashtable的size()方法为什么要做同步?

    对于类的非同步方法,可以多条线程同时访问。如果A线程执行了put方法,而B线程正在执行size方法,导致数据不一致。

    ConcurrentHashMap 的size()方法如何实现同步的?

    1) JDK 8 推荐使用mappingCount 方法(另外的叫size方法),因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值
    2)在没有并发的情况下,使用一个名为 baseCount 的volatile 变量就足够了,当并发的时候,CAS 修改 baseCount 失败后,就会使用 CounterCell 类了,会创建一个这个对象,通常对象的 volatile value 属性是 1。在计算 size 的时候,会将 baseCount 和 CounterCell 数组中的元素的 value 累加,得到总的大小,但这个数字仍旧可能是不准确的。
    3) 还有一个需要注意的地方就是,这个 CounterCell 类使用了 @sun.misc.Contended 注解标识,这个注解是防止伪共享的。是 1.8 新增的。使用时,需要加上 -XX:-RestrictContended 参数。size()/mappingCount()–>sumCount(){使用了baseCount变量和CounterCell数组},在put的时候调用了 addCount()方法

    JDK1.7 和 JDK1.8 对 size 的计算是不一样的。 1.7 中是先不加锁计算三次,如果三次结果不一样在加锁JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。

    线程池会有哪些漏洞/安全问题安全性问题

    notify和notifyAll方法的区别

    notify只会唤醒等待该锁的其中一个线程。notifyAll:唤醒等待该锁的所有线程。
    1)永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
    2)永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。

    如何判断线程是否安全?

    考虑原子性,可见性,有序性。
    1.明确哪些代码是多线程运行的代码,
    2.明确共享数据 对共享变量的操作是不是原子操作 , 当某一个线程对共享变量进行修改的时候,对其他线程是可见的
    保证原子性的是加锁或者同步, 提供了volatile关键字来保证可见性, synchronized和锁和 volatile都能保证有序性
    JVM还通过被称为happens-before原则隐式地保证顺序性。
    3.明确多线程运行代码中哪些语句是操作共享数据.

    1.该对象是否会被多个线程访问修改 ,是的话是否有加锁操作。
    2.注意静态变量. ,由于静态变量是属于该类和该类下所有对象共享,可直接通过类名访问/修改,因此在多线程的环境下.可以断言所有对静态变量的修改都会发生线程安全问题

    展开全文
  • 哈哈iOS多线程面试题

    2021-05-19 11:12:19
    iOS多线程面试题
  • 50个多线程面试题,你会多少?(一)

    万次阅读 多人点赞 2018-03-19 14:06:29
    下面是Java线程相关的热门面试题,你可以用它来好好准备面试。 什么是线程? 什么是线程安全和线程不安全? 什么是自旋锁? 什么是Java内存模型? 什么是CAS? 什么是乐观锁和悲观锁? 什么是AQS? 什么是...



    50个多线程面试题,你会多少?(二 (25 - 51))

    下面是Java线程相关的热门面试题,你可以用它来好好准备面试(1 -- 25)。

    1. 什么是线程?
    2. 什么是线程安全和线程不安全?
    3. 什么是自旋锁?
    4. 什么是Java内存模型?
    5. 什么是CAS?
    6. 什么是乐观锁和悲观锁?
    7. 什么是AQS?
    8. 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
    9. 什么是Executors框架?
    10. 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?
    11. 什么是Callable和Future?
    12. 什么是FutureTask?
    13. 什么是同步容器和并发容器的实现?
    14. 什么是多线程?优缺点?
    15. 什么是多线程的上下文切换?
    16. ThreadLocal的设计理念与作用?
    17. ThreadPool(线程池)用法与优势?
    18. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。
    19. synchronized和ReentrantLock的区别?
    20. Semaphore有什么作用?
    21. Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
    22. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
    23. ConcurrentHashMap的并发度是什么?
    24. ReentrantReadWriteLock读写锁的使用?
    25. CyclicBarrier和CountDownLatch的用法及区别?
    26. LockSupport工具?
    27. Condition接口及其实现原理?
    28. Fork/Join框架的理解?
    29. wait()和sleep()的区别?
    30. 线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?
    31. start()方法和run()方法的区别?
    32. Runnable接口和Callable接口的区别?
    33. volatile关键字的作用?
    34. Java中如何获取到线程dump文件?
    35. 线程和进程有什么区别?
    36. 线程实现的方式有几种(四种)?
    37. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
    38. 如果你提交任务时,线程池队列已满,这时会发生什么?
    39. 锁的等级:方法锁、对象锁、类锁?
    40. 如果同步块内的线程抛出异常会发生什么?
    41. 并发编程(concurrency)并行编程(parallellism)有什么区别?
    42. 如何保证多线程下 i++ 结果正确?
    43. 一个线程如果出现了运行时异常会怎么样?
    44. 如何在两个线程之间共享数据?
    45. 生产者消费者模型的作用是什么?
    46. 怎么唤醒一个阻塞的线程?
    47. Java中用到的线程调度算法是什么
    48. 单例模式的线程安全性?
    49. 线程类的构造方法、静态块是被哪个线程调用的?
    50. 同步方法和同步块,哪个是更好的选择?
    51. 如何检测死锁?怎么预防死锁?

    1 什么是线程?

    线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。

    比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒

    2 什么是线程安全和线程不安全?

    通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的

    线程安全

    线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染

    一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。

    线程不安全

    线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

    3 什么是自旋锁?

    基本概念

    自旋锁是SMP架构中的一种low-level的同步机制

    当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。

    锁需要注意

    • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
    • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁

    实现自旋锁

    参考

    segmentfault.com/q/101000000…

    一个简单的while就可以满足你的要求。

    目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。

    public class MyWaitNotify3{
    
      MonitorObject myMonitorObject = new MonitorObject();
      boolean wasSignalled = false;
    
      public void doWait(){
        synchronized(myMonitorObject){
          while(!wasSignalled){
            try{
              myMonitorObject.wait();
             } catch(InterruptedException e){...}
          }
          //clear signal and continue running.
          wasSignalled = false;
        }
      }
    
      public void doNotify(){
        synchronized(myMonitorObject){
          wasSignalled = true;
          myMonitorObject.notify();
        }
      }
    }
    

    4 什么是Java内存模型?

    Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情

    Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。

    “一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。

    此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。

    看下面代码展示的一个简单例子:

    ClassReordering {
        
        int x = 0, y = 0;
       
        public void writer() {
            x = 1;
            y = 2;
        }
    
        public void reader() {
            int r1 = y;
            int r2 = x;
        }
    }
    

    让我们看在两个并发线程中执行这段代码,读取Y变量将会得到2这个值。因为这个写入比写到X变量更晚一些,程序员可能认为读取X变量将肯定会得到1。但是,写入操作可能被重排序过。如果重排序发生了,那么,就能发生对Y变量的写入操作,读取两个变量的操作紧随其后,而且写入到X这个操作能发生。程序的结果可能是r1变量的值是2,但是r2变量的值为0。

    但是面试官,有时候不这么认为,认为就是JVM内存结构

    JVM内存结构主要有三大块:堆内存、方法区和栈

    堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

    JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

    java堆(Java Heap)

    • 可通过参数 -Xms 和-Xmx设置
    1. Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
    2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
    3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
    • 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
    • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    1. Survivor空间等Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可(就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的)。

    据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

    java虚拟机栈(stack)

    可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

    1.Java虚拟机栈是线程私有的,它的生命周期与线程相同

    1. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    2. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧用于存储 局部变量表操作数栈动态链接方法出口等信息。
    • 局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型
    • 操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
    • 动态连接每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接
    • 方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
    1. 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。

    2. 在方法运行期间不会改变局部变量表的大小。主要存放了编译期可知的各种基本数据类型、对象引用 (reference类型)、returnAddress类型)

    java虚拟机栈,规定了两种异常状况:

    1. 如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
    2. 如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

    本地方法栈

    可通过参数 栈容量可由-Xss设置

    1. 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。
    2. 本地方法栈则是为虚拟机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

    方法区(Method Area)

    可通过参数-XX:MaxPermSize设置

    1. 线程共享内存区域,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent Generation)

    2. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

    3. 如何实现方法区,属于虚拟机的实现细节,不受虚拟机规范约束。

    4. 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。

    5. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载

    6. 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

    运行时常量池

    JDK1.6之前字符串常量池位于方法区之中JDK1.7字符串常量池已经被挪到堆之中

    可通过参数-XX:PermSize和-XX:MaxPermSize设置

    • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量
    • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
    • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。
    1. 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。

    2. 字面量:文本字符串、声明为final的常量值等。

    3. 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

    直接内存

    可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样

    • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

    总结的简单一点

    java堆(Java Heap)

    可通过参数 -Xms 和-Xmx设置

    1. Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
    2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
    3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
    • 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
    • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    java虚拟机栈(stack)

    可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

    1. Java虚拟机栈是线程私有的,它的生命周期与线程相同
    2. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    3. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表操作数栈动态链接方法出口等信息

    方法区(Method Area)

    可通过参数-XX:MaxPermSize设置

    1. 线程共享内存区域),用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码方法区也称持久代(Permanent Generation)

    2. 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。

    3. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

    4. 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

    5 什么是CAS?

    CAS(compare and swap)的缩写,中文翻译成比较并交换

    CAS 不通过JVM,直接利用java本地方 JNI(Java Native Interface为JAVA本地调用),直接调用CPU 的cmpxchg(是汇编指令)指令。

    利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的

    整个java.util.concurrent都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

    CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    CAS应用

    CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    CAS优点

    确保对内存的读-改-写操作都是原子操作执行

    CAS缺点

    CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

    总结

    1. 使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用
    2. synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS

    参考blog.52itstyle.com/archives/94…

    6 什么是乐观锁和悲观锁?

    悲观锁

    Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。

    乐观锁

    乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

    7 什么是AQS?

    AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

    AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

    CAS 原子操作在concurrent包的实现

    参考blog.52itstyle.com/archives/94…

    由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

    • A线程写volatile变量,随后B线程读这个volatile变量。
    • A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
    • A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
    • A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

    Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

    首先,声明共享变量为volatile;然后,使用CAS的原子条件更新来实现线程之间的同步;

    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

    AQS,非阻塞数据结构和原子变量类(Java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

    image

    AQS没有锁之类的概念,它有个state变量,是个int类型,在不同场合有着不同含义。

    AQS围绕state提供两种基本操作“获取”和“释放”,有条双向队列存放阻塞的等待线程,并提供一系列判断和处理方法,简单说几点:

    • state是独占的,还是共享的;
    • state被获取后,其他线程需要等待;
    • state被释放后,唤醒等待线程;
    • 线程等不及时,如何退出等待。

    至于线程是否可以获得state,如何释放state,就不是AQS关心的了,要由子类具体实现。

    AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操作保证更新操作的原子性

    AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了相应的get,set方法。

    ReentrantLock实现原理

    www.cnblogs.com/maypattis/p…

    8 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

    int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

    为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。

    到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。  

    9 什么是Executors框架?

    Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。

    Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

    无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。

    利用Executors框架可以非常方便的创建一个线程池,

    Java通过Executors提供四种线程池,分别为:

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。  

    10 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

    JDK7提供了7个阻塞队列。(也属于并发容器)

    1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
    4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    5. SynchronousQueue:一个不存储元素的阻塞队列。
    6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

    什么是阻塞队列?

    阻塞队列是一个在队列基础上又支持了两个附加操作的队列。

    2个附加操作:

    支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

    阻塞队列的应用场景

    阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

    几个方法

    在阻塞队列不可用的时候,上述2个附加操作提供了四种处理方法

    方法\处理方式抛出异常返回特殊值一直阻塞超时退出
    插入方法add(e)offer(e)put(e)offer(e,time,unit)
    移除方法remove()poll()take()poll(time,unit)
    检查方法element()peek()不可用不可用

    JAVA里的阻塞队列

    JDK 7 提供了7个阻塞队列,如下

    1、ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

    2、LinkedBlockingQueue一个由链表结构组成的有界阻塞队列

    此队列按照先出先进的原则对元素进行排序

    3、PriorityBlockingQueue支持优先级的无界阻塞队列

    4、DelayQueue支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素

    5、SynchronousQueue不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列。

    6、LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法

    transfer方法

    如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。

    tryTransfer方法

    用来试探生产者传入的元素能否直接传给消费者。,如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

    7、LinkedBlockingDeque链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争。

    如何使用阻塞队列来实现生产者-消费者模型?

    通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

    使用BlockingQueue解决生产者消费者问题

    为什么BlockingQueue适合解决生产者消费者问题

    任何有效的生产者-消费者问题解决方案都是通过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的,一旦你实现了对方法的阻塞控制,那么你将解决该问题。

    Java通过BlockingQueue提供了开箱即用的支持来控制这些方法的调用(一个线程创建资源,另一个消费资源)。java.util.concurrent包下的BlockingQueue接口是一个线程安全的可用于存取对象的队列。

    BlockingQueue是一种数据结构,支持一个线程往里存资源,另一个线程从里取资源。这正是解决生产者消费者问题所需要的,那么让我们开始解决该问题吧。

    生产者

    以下代码用于生产者线程

    package io.ymq.example.thread;
    
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 描述:生产者
     *
     * @author yanpenglei
     * @create 2018-03-14 15:52
     **/
    class Producer implements Runnable {
    
        protected BlockingQueue<Object> queue;
    
        Producer(BlockingQueue<Object> theQueue) {
            this.queue = theQueue;
        }
    
        public void run() {
            try {
                while (true) {
                    Object justProduced = getResource();
                    queue.put(justProduced);
                    System.out.println("生产者资源队列大小= " + queue.size());
                }
            } catch (InterruptedException ex) {
                System.out.println("生产者 中断");
            }
        }
    
        Object getResource() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                System.out.println("生产者 读 中断");
            }
            return new Object();
        }
    }
    

    消费者

    以下代码用于消费者线程

    package io.ymq.example.thread;
    
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 描述: 消费者
     *
     * @author yanpenglei
     * @create 2018-03-14 15:54
     **/
    class Consumer implements Runnable {
        protected BlockingQueue<Object> queue;
    
        Consumer(BlockingQueue<Object> theQueue) {
            this.queue = theQueue;
        }
    
        public void run() {
            try {
                while (true) {
                    Object obj = queue.take();
                    System.out.println("消费者 资源 队列大小 " + queue.size());
                    take(obj);
                }
            } catch (InterruptedException ex) {
                System.out.println("消费者 中断");
            }
        }
    
        void take(Object obj) {
            try {
                Thread.sleep(100); // simulate time passing
            } catch (InterruptedException ex) {
                System.out.println("消费者 读 中断");
            }
            System.out.println("消费对象 " + obj);
        }
    }
    

    测试该解决方案是否运行正常

    package io.ymq.example.thread;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    /**
     * 描述: 测试
     *
     * @author yanpenglei
     * @create 2018-03-14 15:58
     **/
    public class ProducerConsumerExample {
    
        public static void main(String[] args) throws InterruptedException {
    
            int numProducers = 4;
            int numConsumers = 3;
    
            BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5);
    
            for (int i = 0; i < numProducers; i++) {
                new Thread(new Producer(myQueue)).start();
            }
    
            for (int i = 0; i < numConsumers; i++) {
                new Thread(new Consumer(myQueue)).start();
            }
    
            Thread.sleep(1000);
    
            System.exit(0);
        }
    }
    

    运行结果

    生产者资源队列大小= 1
    生产者资源队列大小= 1
    消费者 资源 队列大小 1
    生产者资源队列大小= 1
    消费者 资源 队列大小 1
    消费者 资源 队列大小 1
    生产者资源队列大小= 1
    生产者资源队列大小= 3
    消费对象 java.lang.Object@1e1aa52b
    生产者资源队列大小= 2
    生产者资源队列大小= 5
    消费对象 java.lang.Object@6e740a76
    消费对象 java.lang.Object@697853f6
    
    ......
    
    消费对象 java.lang.Object@41a10cbc
    消费对象 java.lang.Object@4963c8d1
    消费者 资源 队列大小 5
    生产者资源队列大小= 5
    生产者资源队列大小= 5
    消费者 资源 队列大小 4
    消费对象 java.lang.Object@3e49c35d
    消费者 资源 队列大小 4
    生产者资源队列大小= 5
    

    从输出结果中,我们可以发现队列大小永远不会超过5,消费者线程消费了生产者生产的资源

    11 什么是Callable和Future?

    Callable 和 Future 是比较有趣的一对组合。当我们需要获取线程的执行结果时,就需要用到它们。Callable用于产生结果,Future用于获取结果

    Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,必须等待它返回的结果。java.util.concurrent.Future对象解决了这个问题。

    在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法,等待Callable结束并获取它的执行结果。

    代码示例

    Callable 是一个接口,它只包含一个call()方法。Callable是一个返回结果并且可能抛出异常的任务

    为了便于理解,我们可以将Callable比作一个Runnable接口,而Callable的call()方法则类似于Runnable的run()方法

    public class CallableFutureTest {
    
        public static void main(String[] args) throws InterruptedException, ExecutionException {
    
            System.out.println("start main thread ");
    
            ExecutorService exec = Executors.newFixedThreadPool(2);
    
            //新建一个Callable 任务,并将其提交到一个ExecutorService. 将返回一个描述任务情况的Future.
            Callable<String> call = new Callable<String>() {
    
                @Override
                public String call() throws Exception {
                    System.out.println("start new thread ");
                    Thread.sleep(5000);
                    System.out.println("end new thread ");
                    return "我是返回的内容";
                }
            };
    
            Future<String> task = exec.submit(call);
            Thread.sleep(1000);
            String retn = task.get();
            //关闭线程池
            exec.shutdown();
            System.out.println(retn + "--end main thread");
        }
    }
    

    控制台打印

    start main thread 
    start new thread 
    end new thread 
    我是返回的内容--end main thread
    

    12 什么是FutureTask?

    FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。

    1.执行多任务计算

    FutureTask执行多任务计算的使用场景

    利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.*;
    
    public class FutureTaskForMultiCompute {
    
        public static void main(String[] args) {
    
            FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute();
            // 创建任务集合
            List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
            // 创建线程池
            ExecutorService exec = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 10; i++) {
                // 传入Callable对象创建FutureTask对象
                FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i));
                taskList.add(ft);
                // 提交给线程池执行任务,也可以通过exec.invokeAll(taskList)一次性提交所有任务;
                exec.submit(ft);
            }
    
            System.out.println("所有计算任务提交完毕, 主线程接着干其他事情!");
    
            // 开始统计各计算线程计算结果
            Integer totalResult = 0;
            for (FutureTask<Integer> ft : taskList) {
                try {
                    //FutureTask的get方法会自动阻塞,直到获取计算结果为止
                    totalResult = totalResult + ft.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
    
            // 关闭线程池
            exec.shutdown();
            System.out.println("多任务计算后的总结果是:" + totalResult);
    
        }
    
        private class ComputeTask implements Callable<Integer> {
    
            private Integer result = 0;
            private String taskName = "";
    
            public ComputeTask(Integer iniResult, String taskName) {
                result = iniResult;
                this.taskName = taskName;
                System.out.println("生成子线程计算任务: " + taskName);
            }
    
            public String getTaskName() {
                return this.taskName;
            }
    
            @Override
            public Integer call() throws Exception {
                // TODO Auto-generated method stub
    
                for (int i = 0; i < 100; i++) {
                    result = +i;
                }
                // 休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。
                Thread.sleep(5000);
                System.out.println("子线程计算任务: " + taskName + " 执行完成!");
                return result;
            }
        }
    }
    
    生成子线程计算任务: 0
    生成子线程计算任务: 1
    生成子线程计算任务: 2
    生成子线程计算任务: 3
    生成子线程计算任务: 4
    生成子线程计算任务: 5
    生成子线程计算任务: 6
    生成子线程计算任务: 7
    生成子线程计算任务: 8
    生成子线程计算任务: 9
    所有计算任务提交完毕, 主线程接着干其他事情!
    子线程计算任务: 0 执行完成!
    子线程计算任务: 2 执行完成!
    子线程计算任务: 3 执行完成!
    子线程计算任务: 4 执行完成!
    子线程计算任务: 1 执行完成!
    子线程计算任务: 8 执行完成!
    子线程计算任务: 7 执行完成!
    子线程计算任务: 6 执行完成!
    子线程计算任务: 9 执行完成!
    子线程计算任务: 5 执行完成!
    多任务计算后的总结果是:990
    

    2.高并发环境下

    FutureTask在高并发环境下确保任务只执行一次

    在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。举一个例子,假设有一个带key的连接池,当key存在时,即直接返回key对应的对象;当key不存在时,则创建连接。对于这样的应用场景,通常采用的方法为使用一个Map对象来存储key和连接池对应的对应关系,典型的代码如下面所示:

      private Map<String, Connection> connectionPool = new HashMap<String, Connection>();
        private ReentrantLock lock = new ReentrantLock();
    
        public Connection getConnection(String key) {
            try {
                lock.lock();
                if (connectionPool.containsKey(key)) {
                    return connectionPool.get(key);
                } else {
                    //创建 Connection  
                    Connection conn = createConnection();
                    connectionPool.put(key, conn);
                    return conn;
                }
            } finally {
                lock.unlock();
            }
        }
    
        //创建Connection  
        private Connection createConnection() {
            return null;
        }
    
    

    在上面的例子中,我们通过加锁确保高并发环境下的线程安全,也确保了connection只创建一次,然而确牺牲了性能。改用ConcurrentHash的情况下,几乎可以避免加锁的操作,性能大大提高,但是在高并发的情况下有可能出现Connection被创建多次的现象。这时最需要解决的问题就是当key不存在时,创建Connection的动作能放在connectionPool之后执行,这正是FutureTask发挥作用的时机,基于ConcurrentHashMap和FutureTask的改造代码如下:

      private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();
    
        public Connection getConnection(String key) throws Exception {
            FutureTask<Connection> connectionTask = connectionPool.get(key);
            if (connectionTask != null) {
                return connectionTask.get();
            } else {
                Callable<Connection> callable = new Callable<Connection>() {
                    @Override
                    public Connection call() throws Exception {
                        // TODO Auto-generated method stub  
                        return createConnection();
                    }
                };
                FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
                connectionTask = connectionPool.putIfAbsent(key, newTask);
                if (connectionTask == null) {
                    connectionTask = newTask;
                    connectionTask.run();
                }
                return connectionTask.get();
            }
        }
    
        //创建Connection  
        private Connection createConnection() {
            return null;
        }
    

    经过这样的改造,可以避免由于并发带来的多次创建连接及锁的出现。

    13 什么是同步容器和并发容器的实现?

    一、同步容器

    主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。锁的粒度为当前对象整体。迭代器是及时失败的,即在迭代的过程中发现被修改,就会抛出ConcurrentModificationException。

    二、并发容器

    主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。锁的粒度是分散的、细粒度的,即读和写是使用不同的锁。迭代器具有弱一致性,即可以容忍并发修改,不会抛出ConcurrentModificationException。

    JDK 7 ConcurrentHashMap

    采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达到高效的并发访问;

    JDK 8 ConcurrentHashMap

    采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达到高效的并发访问;

    三、阻塞队列

    主要代表有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。适用于生产者、消费者模式(线程池和工作队列-Executor),同时也是同步容器

    四、双端队列

    主要代表有ArrayDeque和LinkedBlockingDeque。意义:正如阻塞队列适用于生产者消费者模式,双端队列同样适用与另一种模式,即工作密取。在生产者-消费者设计中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作。具有更好的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,他们都只是访问自己的双端队列,从而极大的减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争。适用于:网页爬虫等任务中

    五、比较及适用场景

    如果不需要阻塞队列,优先选择ConcurrentLinkedQueue;如果需要阻塞队列,队列大小固定优先选择ArrayBlockingQueue,队列大小不固定优先选择LinkedBlockingQueue;如果需要对队列进行排序,选择PriorityBlockingQueue;如果需要一个快速交换的队列,选择SynchronousQueue;如果需要对队列中的元素进行延时操作,则选择DelayQueue。

    14 什么是多线程?优缺点?

    什么是多线程?

    多线程:是指从软件或者硬件上实现多个线程的并发技术。

    多线程的好处:

    1. 使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载
    2. 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好

    多线程的缺点:

    1. 大量的线程降低代码的可读性;
    2. 更多的线程需要更多的内存空间
    3. 当多个线程对同一个资源出现争夺时候要注意线程安全的问题。

    15 什么是多线程的上下文切换?

    即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)

    上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态

    • 从任务保存到再加载的过程就是一次上下文切换

    16 ThreadLocal的设计理念与作用?

    Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量

    ThreadLocal

    如何创建ThreadLocal变量

    以下代码展示了如何创建一个ThreadLocal变量:

    private ThreadLocal myThreadLocal = new ThreadLocal();
    

    通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值

    如何访问ThreadLocal变量

    一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:

    myThreadLocal.set("A thread local value”);
    

    可以通过下面方法读取保存在ThreadLocal变量中的值:

    String threadLocalValue = (String) myThreadLocal.get();
    

    get()方法返回一个Object对象,set()对象需要传入一个Object类型的参数。

    为ThreadLocal指定泛型类型

    public static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
    

    我们可以创建一个指定泛型类型的ThreadLocal对象,这样我们就不需要每次对使用get()方法返回的值作强制类型转换了。下面展示了指定泛型类型的ThreadLocal例子:

    ThreadLocal的设计理念与作用

    http://blog.csdn.net/u011860731/article/details/48733073http://blog.csdn.net/u011860731/article/details/48733073)

    InheritableThreadLocal

    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
    

    InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每个线程拥有它自己的值,与ThreadLocal不同的是,InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以访问它保存的值

    InheritableThreadLocal 原理

    Java 多线程:InheritableThreadLocal 实现原理

    blog.csdn.net/ni357103403…

    17 ThreadPool(线程池)用法与优势?

    为什么要用线程池:

    1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
    3. Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

    new Thread 缺点

    1. 每次new Thread新建对象性能差。
    2. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
    3. 缺乏更多功能,如定时执行、定期执行、线程中断。

    ThreadPool 优点

    减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

    可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

    • 减少在创建和销毁线程上所花的时间以及系统资源的开销
    • 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存

    Java提供的四种线程池的好处在于

    1. 重用存在的线程,减少对象创建、销毁的开销,提高性能。
    2. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
    3. 提供定时执行、定期执行、单线程、并发数控制等功能。

    比较重要的几个类:

    描述
    ExecutorService真正的线程池接口。
    ScheduledExecutorService能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
    ThreadPoolExecutorExecutorService的默认实现。
    ScheduledThreadPoolExecutor继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

    Executors提供四种线程池

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    一般都不用Executors提供的线程创建方式

    使用ThreadPoolExecutor创建线程池

    ThreadPoolExecutor的构造函数

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    

    参数:

    1. corePoolSize核心线程数大小,当线程数<corePoolSize ,会创建线程执行runnable
    2. maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中
    3. keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
    4. unit 时间单位
    5. workQueue 保存任务的阻塞队列
    6. threadFactory 创建线程的工厂
    7. handler 拒绝策略

    任务执行顺序:

    1. 当线程数小于corePoolSize时,创建线程执行任务。
    2. 当线程数大于等于corePoolSize并且workQueue没有满时,放入workQueue中
    3. 线程数大于等于corePoolSize并且当workQueue满时,新任务新建线程运行,线程总数要小于maximumPoolSize
    4. 当线程总数等于maximumPoolSize并且workQueue满了的时候执行handler的rejectedExecution。也就是拒绝策略。

    ThreadPoolExecutor默认有四个拒绝策略:

    1. ThreadPoolExecutor.AbortPolicy() 直接抛出异常RejectedExecutionException
    2. ThreadPoolExecutor.CallerRunsPolicy() 直接调用run方法并且阻塞执行
    3. ThreadPoolExecutor.DiscardPolicy() 直接丢弃后来的任务
    4. ThreadPoolExecutor.DiscardOldestPolicy() 丢弃在队列中队首的任务

    当然可以自己继承 RejectedExecutionHandler 来写拒绝策略.

    java 四种线程池的使用

    juejin.im/post/59df0c…

    18 Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。

    阻塞队列

    1、ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

    CountDownLatch

    CountDownLatch 允许一个或多个线程等待其他线程完成操作。

    应用场景

    假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。

    在这个需求中,要实现主线程等待所有线程完成sheet的解析操作,最简单的做法是使用join。代码如下:

    public class JoinCountDownLatchTest {
    
    	public static void main(String[] args) throws InterruptedException {
    		Thread parser1 = new Thread(new Runnable() {
    			@Override
    			public void run() {
    			}
    		});
    
    		Thread parser2 = new Thread(new Runnable() {
    			@Override
    			public void run() {
    				System.out.println("parser2 finish");
    			}
    		});
    
    		parser1.start();
    		parser2.start();
    		parser1.join();
    		parser2.join();
    		System.out.println("all parser finish");
    	}
    
    }
    

    join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远wait,代码片段如下,wait(0)表示永远等待下去。

    while (isAlive()) {
     wait(0);
    }
    
    • 方法isAlive()功能是判断当前线程是否处于活动状态。
    • 活动状态就是线程启动且尚未终止,比如正在运行或准备开始运行。

    CountDownLatch用法

    public class Test {
         public static void main(String[] args) {   
    	 
             final CountDownLatch latch = new CountDownLatch(2);
     
             new Thread(){
                 public void run() {
                     try {
                         System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                        Thread.sleep(3000);
                        System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                        latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 };
             }.start();
     
             new Thread(){
                 public void run() {
                     try {
                         System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                         Thread.sleep(3000);
                         System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                         latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 };
             }.start();
     
             try {
                 System.out.println("等待2个子线程执行完毕...");
                latch.await();
                System.out.println("2个子线程已经执行完毕");
                System.out.println("继续执行主线程");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
         }
     }
    
    线程Thread-0正在执行
    线程Thread-1正在执行
    等待2个子线程执行完毕...
    线程Thread-0执行完毕
    线程Thread-1执行完毕
    2个子线程已经执行完毕
    继续执行主线程
    

    new CountDownLatch(2)的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。

    当我们调用一次CountDownLatch的countDown()方法时,N就会减1,CountDownLatch的await()会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,你只需要把这个CountDownLatch的引用传递到线程里。

    Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore

    www.importnew.com/21889.html

    19 synchronized和ReentrantLock的区别?

    java在编写多线程程序时,为了保证线程安全,需要对数据同步,经常用到两种同步方式就是Synchronized和重入锁ReentrantLock。

    基础知识

    • 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁
    • 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
    • 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
    • CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

    Synchronized

    synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一定的局限性

    例如:

    1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
    2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。

    ReentrantLock

    ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

    代码示例

    private Lock lock = new ReentrantLock();
    public void test(){
     lock.lock();
     try{
     doSomeThing();
     }catch (Exception e){
     // ignored
     }finally {
     lock.unlock();
     }
    }
    
    • **lock()**, 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
    • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
    • tryLock(long timeout,TimeUnit unit)****,如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
    • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

    ReentrantLock 一些特性

    1. 等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
    2. 公平锁与非公平锁多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

    公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;

    非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。

    Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作

    ReenTrantLock实现的原理:

    简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

    总结一下

    在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

    synchronized

    在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。

    ReentrantLock:

    ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。

    ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。

    ReentrantLock实现原理

    www.cnblogs.com/maypattis/p…

    分析ReentrantLock的实现原理(ReentrantLock和同步工具类的实现基础都是AQS)

    www.jianshu.com/p/fe027772e…

    20 Semaphore有什么作用?

    1. Semaphore就是一个信号量,它的作用是限制某段代码块的并发数
    2. Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问
    3. 如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入
    4. 由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    Semaphore类位于java.util.concurrent包下,它提供了2个构造器:

    //参数permits表示许可数目,即同时可以允许多少线程进行访问  
    public Semaphore(int permits) {  
        sync = new NonfairSync(permits);  
    }  
    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可  
    public Semaphore(int permits, boolean fair) {  
        sync = (fair)? new FairSync(permits) : new NonfairSync(permits);  
    }  
    
    • Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
    • acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    • release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
    Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
    acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
    

    这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false  
    public boolean tryAcquire() { };  
    //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false  
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };   
    //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false  
    public boolean tryAcquire(int permits) { };   
    //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true  
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { };  
    //得到当前可用的许可数目  
    public int availablePermits(); 
    

    示例

    假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

    public class Test {  
        public static void main(String[] args) {  
            int N = 8; //工人数  
            Semaphore semaphore = new Semaphore(5); //机器数目  
            for(int i=0;i<N;i++)  
                new Worker(i,semaphore).start();  
        }      
        static class Worker extends Thread{  
            private int num;  
            private Semaphore semaphore;  
            public Worker(int num,Semaphore semaphore){  
                this.num = num;  
                this.semaphore = semaphore;  
            }          
            @Override  
            public void run() {  
                try {  
                    semaphore.acquire();  
                    System.out.println("工人"+this.num+"占用一个机器在生产...");  
                    Thread.sleep(2000);  
                    System.out.println("工人"+this.num+"释放出机器");  
                    semaphore.release();              
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    } 
    

    运行结果:

    工人0占用一个机器在生产...  
    工人1占用一个机器在生产...  
    工人2占用一个机器在生产...  
    工人4占用一个机器在生产...  
    工人5占用一个机器在生产...  
    工人0释放出机器  
    工人2释放出机器  
    工人3占用一个机器在生产...  
    工人7占用一个机器在生产...  
    工人4释放出机器  
    工人5释放出机器  
    工人1释放出机器  
    工人6占用一个机器在生产...  
    工人3释放出机器  
    工人7释放出机器  
    工人6释放出机器
    

    21 Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    • 可以使锁更公平
    • 可以使线程在等待锁的时候响应中断
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    • 可以在不同的范围,以不同的顺序获取和释放锁

    22 Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

    同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。

    而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

    23 ConcurrentHashMap的并发度是什么?

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势

    24 ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。

    读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

    如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

    ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁

    线程进入读锁的前提条件

    • 没有其他线程的写锁
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个

    线程进入写锁的前提条件

    • 没有其他线程的读锁
    • 没有其他线程的写锁
    • 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
    • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
    • 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
    • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
    • 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。

    50个多线程面试题,你会多少?(二)

     

    展开全文
  • 史上最全Java多线程面试题及答案

    万次阅读 多人点赞 2018-08-20 11:17:08
    这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都...
  • 多线程面试题: 1.什么是线程,什么是进程,它们有什么区别和联系,一个进程里面是否必须有个线程 (先讲进程) 答案 进程本质上是一个执行的程序,一个进程可以有多个线程。它允许计算机同时运行两个或多个程序。...
  • java多线程面试题.docx

    2020-09-09 11:01:32
    java大厂的多线程面试题,涵盖了基础和进阶的多线程的理论和实际的应用,供各位下载使用。多线程面试题
  • 主要为大家分享了15个顶级Java多线程面试题,考察面试者是否有足够的Java线程与并发知识,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 主要介绍了15个高级Java多线程面试题及回答,翻译自国外的一篇文章,这些面试题容易混淆、较难回答,需要的朋友可以参考下吧
  • java多线程面试题

    2017-11-08 23:09:17
    java多线程面试经典,内含50道常被面试官问到的java多线程
  • Java多线程面试题

    万次阅读 2020-10-25 15:56:40
    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话); wait 方法: 是 Object 的方法...
  • C++ 多线程 面试题详解

    万次阅读 2018-01-16 22:33:46
    首先看看堆栈 堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。...栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 t...
  • 15个顶级Java多线程面试题及回答(高级java工程师)
  • 2019Android多线程面试题总结

    千次阅读 2019-09-17 22:50:57
    极力推荐文章:欢迎收藏Android 干货分享和您一起终身学习,这里是程序员Android1.什么是线程线程就是进程中运行的个子任务,是操作系统调用的最小单元2.线程...
  • 多线程面试题汇总(一)

    万次阅读 2018-07-21 14:53:00
    本文是作者整理的个人笔记,文中可能引用到其他人的成果...同一进程的线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程之间切换快,无需陷入内核态。 线程状态 阻塞状态分分为...
  • Java面试——多线程面试题

    万次阅读 2016-09-02 10:22:35
    0.前言在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分,本文汇总了常见的一些多线程面试题。一些问题,比如volatile关键词的作用,synchronized和ReentrantLock的区别,wait()和sleep()的区别等等...
  • 个人总结40个Java多线程面试问题和答案,很全面,让你不再担心多线程的面试问题。
  • 多线程,多线程面试题,C#源码.zip
  • 想进大厂?50个多线程面试题,你会多少?(一)

    万次阅读 多人点赞 2020-03-19 16:53:01
    最近看到网上流传着,各种面试经验及面试题,往往都是一大堆技术题目贴上去,而没有答案。 不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java...
  • 点击上方“终端研发部”,选择“星标” 回复“资源”,领取全网最火的Java核心知识总结~ 链接:https://www.jianshu.com/p/3e88a5fe75f0 前言 今天给大家更新的是一篇关于多线程面试的文章,也是霸哥根据时下热门的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 167,210
精华内容 66,884
关键字:

多线程面试题