2020java多线程面试题(98道重点java面试题)

目录:

1. 什么是并发?并发与并行有什么区别?
  • 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
  • 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。

2. 什么是并发编程?为什么不叫并行编程?
  • 并发编程:用编程语言编写让计算机可以在一个时间段内执行多个任务的程序。
  • 现在的服务器都是多核处理器多CPU的服务器,多个处理器同时处理多个任务不是叫并行吗?注意,举个例子,8核处理器同时处理8个任务叫真正意义上的并行。一旦8核处理器处理9个任务,并行与并发就同时存在了。另外,在实际应用中,一台服务器承担的任务数量是成千上万的,那么平均到每个处理器上,每个处理器承担的任务数量也有很多。所以当有成千上万的任务时,不可能使用成千上万的处理器去做真正意义上的并行编程,因为一台服务器上没有那么多的处理器。所以并发编程主要研究的是,让很多任务在单个处理器上如何快速有序的运行,从而使用少量的处理器去完成大量的任务。所以叫并发编程。

3. 为什么要使用并发编程?并发编程的优点是什么?
  • 1、并发编程可以提升 CPU 的计算能力的利用率。
  • 2、提升程序的性能,如:响应时间、吞吐量、计算机资源使用率等。
  • 3、并发程序可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务。

4. 并发编程的缺点?
  • 1、Java 中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间;
  • 2、容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致;
  • 3、多线程容易造成死锁、活锁、线程饥饿等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重;
  • 4、对编程人员的技术要求较高,编写出正确的并发程序并不容易;
  • 5、并发程序易出问题,且难调试和排查;问题常常诡异地出现,又诡异地消失。

5. 并发编程三要素?
  • 1、原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • 2、可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。
  • 3、有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

6. 线程的安全性问题体现在哪几个方面?
  • 1、原子性;
  • 2、可见性;
  • 3、有序性。

7. 如何解决线程的安全性问题?
  • 因为线程的安全性问题体现在原子性、可见性、有序性三个方面,所以解决这个问题也要从这三个方面入手:
  • 1、使用Atomic开头的原子类、synchronized(自动锁)、LOCK(手动锁),可以解决原子性问题;
  • 2、synchronized、volatile、LOCK,可以解决可见性问题;
  • 3、2.Happens-Before 规则可以解决有序性问题。

8. 什么是进程?
  • 1、程序执行时的一个实例;
  • 2、每个进程都有独立的内存地址空间;
  • 3、系统进行资源分配和调度的基本单位;
  • 4、进程里的堆,是一个进程中最大的一块内存,被进程中的所有线程共享的,进程创建时分配,主要存放 new 创建的对象实例;
  • 5、进程里的方法区,是用来存放进程中的代码片段的,是线程共享的;
  • 6、在多线程 OS 中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码;
  • 7、举例说明:打开并登录QQ,就是创建了一个进程1,再打开一个QQ登录另一个账号,这就是第二个进程2。你在你的QQ账号中一边跟朋友A视频,一边跟朋友B打字聊天,这就是在进程1上又创建了两个线程1和2,一个负责视频的线程,一个负责打字聊天的线程。

9. 什么是线程?
  • 1、进程中的一个实体
  • 2、进程的一个执行路径
  • 3、CPU 调度和分派的基本单位
  • 4、线程本身是不会独立存在
  • 5、当前线程 CPU 时间片用完后,会让出 CPU 等下次轮到自己时候在执行
  • 6、系统不会为线程分配内存,线程组之间只能共享所属进程的资源
  • 7、线程只拥有在运行中必不可少的资源(如程序计数器、栈)
  • 8、线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
  • 9、每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问。

10. 为什么要有线程?
  • 每个进程都有自己的地址空间,即进程空间。一个服务器通常需要接收大量并发请求,为每一个请求都创建一个进程系统开销大、请求响应效率低,因此操作系统引进线程。

11. 实际应用中,如何在进程与线程之间做选择?
  • 1、需要频繁创建销毁的优先使用线程。因为进程创建、销毁一个进程代价很大,需要不停的分配资源;线程频繁的调用只改变 CPU 的执行;
  • 2、线程的切换速度快,需要大量计算,切换频繁时,用线程;
  • 3、耗时的操作使用线程可提高应用程序的响应;
  • 4、线程对 CPU 的使用效率更优,多机器分布的用进程,多核分布用线程;
  • 5、需要跨机器移植,优先考虑用进程;
  • 6、需要更稳定、安全时,优先考虑用进程;
  • 7、需要速度时,优先考虑用线程;
  • 8、并行性要求很高时,优先考虑用线程。

12. 什么是上下文切换?
  • 当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

13. 什么是守护线程?
  • 守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。

14. 如何设置守护线程?
  • Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。
  • 关于守护线程的几点注意:
  • ①setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行;
    ②守护线程创建的线程也是守护线程;
    ③守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题。

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

16. 如何避免线程死锁?
  • 加锁顺序(线程按照一定的顺序加锁);
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁);
  • 死锁检测。

17. 死锁与活锁的区别?
  • 1、死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去;
  • 2、活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败;
  • 3、活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

18. 如何解决活锁?
  • 解决活锁的一个简单办法就是在下一次尝试获取资源之前,随机休眠一小段时间。

19. 什么是饥饿?
  • 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

20. 导致饥饿的原因?
  • 1、优先级线程吞噬所有的低优先级线程的 CPU 时间;
  • 2、其他线程总是能在它之前持续地对该同步块进行访问,线程被永久堵塞在一个等待进入同步块;
  • 3、其他线程总是抢先被持续地获得唤醒,线程一直在等待被唤醒。

21. java中线程有哪些状态?
  • 1、NEW(初始化状态)
  • 2、RUNNABLE(可运行 / 运行状态)
  • 3、BLOCKED(阻塞状态)
  • 4、WAITING(无限时等待)
  • 5、TIMED_WAITING(有限时等待)
  • 6、TERMINATED(终止状态) 在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态(休眠状态)。即只要 Java 线程处于这三种状态之一,就永远没有 CPU 的使用权。

22. java中线程状态之间如何转换?
  • 1、线程从new变为runnable:
    ①使用start()方法。
  • 2、runnable变为waiting:
    ①调用Object.wait();
    ②调用Thread.join() ;
    ③调用 LockSupport.park() 。
  • 3、waiting变为runnable:
    ①调用 Object.notify()、Object.notifyAll() ;
    ②调用join()方法的线程执行完后,waiting状态的线程就会变为runnable();
    ③调用 LockSupport.unpark(Thread t)可唤醒线程t。
  • 4、runnable变为blocked:
    ①synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,等待的线程会从 runnable 转变到 blocked状态。
  • 5、blocked变为runnable:
    ①当等待的线程获得 synchronized 隐式锁时,就又会从 blocked转变到 runnable 状态。
  • 6、runnable变为timed_waiting:
    ①Thread.sleep(long millis);
    ②Object.wait(long timeout);
    ③Thread.join(long millis);
    ④LockSupport.parkNanos(Object blocker, long deadline);
    ⑤LockSupport.parkUntil(long deadline);
    ⑥timed_waiting 和 waiting 状态的区别,仅仅是调用的是超时参数的方法。
  • 7、runnable变为terminated:
    ①线程执行完 run() 方法后,会自动转变到 terminated 状态;
    ②执行 run() 方法时异常抛出,也会导致线程终止;
    ③Thread类的 stop() 方法已经不建议使用。

23. 创建线程有哪几种方式?
  • 1、重写 Thread 类的 run() 方法:
    ①new Thread 对象匿名重写 run() 方法;
    ②继承 Thread 对象,重写 run() 方法;
  • 2、实现 Runnable 接口,重写 run() 方法:
    ①new Runnable 对象,匿名重写 run() 方法;
    ②实现 Runnable 接口,重写 run() 方法;
  • 3、实现 Callable 接口,使用 FutureTask 类创建线程:
    ①实现 Callable 接口;创建实现Callable接口的类myCallable;以myCallable为参数创建FutureTask对象;将FutureTask作为参数创建Thread对象;调用线程对象的start()方法;
  • 4、使用 Executors 工具类创建线程池。

24. runnable与callable的共同点和区别?
  • 1、相同点:都是接口;都可以编写多线程程序;都采用Thread.start()启动线程;
  • 2、主要区别:Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果;Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。

25. 什么是线程池?
  • 线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

26. 为什么要使用线程池?线程池有什么优点?
  • 1、降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
  • 2、提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 4、附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
  • 5、总结:使用线程池框架 Executor 能更好的管理线程、避免频繁创建和销毁线程,提高系统资源使用率。

27. 线程池有哪些状态?
  • 1、RUNNING:
    ①线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
  • 2、SHUTDOWN:
    ①不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
  • 3、STOP:
    ①不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
  • 4、TIDYING:
    ①SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
    ②线程池在SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。③线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
  • 5、TERMINATED:
    ①线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。

28. 如何创建线程池?
  • 1、newSingleThreadExecutor:
    ①创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • 2、newFixedThreadPool:
    ②创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
  • 3、newCachedThreadPool:
    ③创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  • 4、newScheduledThreadPool:
    ④创建一个参数多个的线程池。此线程池支持定时以及周期性执行任务的需求。

29. 如何停止一个线程池?
  • JDK 1.8 中,线程池的停止一般使用 shutdown()、shutdownNow()、shutdown() + awaitTermination(long timeout, TimeUnit unit) 方法。

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

31. 为什么使用 Executor 框架?
  • 1、每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。
  • 2、所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池。

32. 在 Java 中 Executor 和 Executors 的区别?
  • 1、Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求;
  • 2、Executor 接口对象能执行我们的线程任务;
  • 3、ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值;
  • 4、使用 ThreadPoolExecutor 可以创建自定义线程池;
  • 5、Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。

33. 线程池中 submit() 和 execute() 方法有什么区别?
  • 1、接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务;
  • 2、返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有;
  • 3、异常处理:submit()方便Exception处理。

34. 如果你提交任务时,线程池队列已满,这时会发生什么?
  • 1、如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务;
  • 2、如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。

35. 线程的生命周期和五种基本状态是什么?
  • 1、创建(new):Java 刚创建出来的 Thread 对象就是 new状态,不会被操作系统调度执行。
  • 2、可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
  • 3、运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  • 4、阻塞(blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。阻塞的情况分三种:
    ①等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
    ②同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    ③其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • 5、死亡(dead,也叫terminated):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

36. 线程的run()和start()有什么区别?
  • 1、启动一个线程需要调用 Thread 对象的 start() 方法;
  • 2、调用线程的 start() 方法后,线程处于可运行状态,此时可以由 JVM 调度并执行,这并不意味着线程就会立即运行;
  • 3、run() 方法是线程运行时由 JVM 回调的方法,无需手动写代码调用;
  • 4、直接调用线程的 run() 方法,相当于在调用线程里继续调用方法,并未启动一个新的线程。

37. 如何使多个线程同时启动?
  • 可以 wait()、notify() 实现;也可以使用发令枪 CountDownLatch 实现。

38. 为什么我们不能直接调用run()方法?
  • start()方法来启动一个线程,真正实现了多线程运行。run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接调用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

39. 什么是 Callable 和 Future?
  • Callable 接口类似于 Runnable,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

40. Java 中用到的线程调度算法是什么?
  • 有两种调度模型:分时调度模型和抢占式调度模型。
    1、分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
    2、Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

41. 什么时候会发生线程的调度?
  • 线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
    1、线程体中调用了 yield 方法让出了对 cpu 的占用权利;
    2、线程体中调用了 sleep 方法使线程进入睡眠状态;
    3、线程由于 IO 操作受到阻塞;
    4、另外一个更高优先级线程出现;
    5、在支持时间片的系统中,该线程的时间片用完。

42. 与线程同步以及线程调度相关的方法有哪些?
  • 1、wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • 2、sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
  • 3、notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
  • 4、notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。

43. wait()方法和sleep()的区别?
  • 1、sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法;
  • 2、sleep() 方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread “Thread-0” java.lang.IllegalMonitorStateException;
  • 3、sleep() 会休眠当前线程指定时间,释放 CPU 资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait() 方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态;
  • 4、JDK1.8 sleep() wait() 均需要捕获 InterruptedException 异常。

44. Thread 类中的 yield 方法有什么作用?
  • 1、使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。

45. sleep()方法和yield()有什么区别?
  • 1、sleep() 方法给其他线程运行机会时不考虑线程的优先级;yield() 方法只会给相同优先级或更高优先级的线程运行的机会;
  • 2、线程执行 sleep() 方法后进入阻塞状态;线程执行 yield() 方法转入就绪状态,可能马上又得得到执行;
  • 3、sleep() 方法声明抛出 InterruptedException;yield() 方法没有声明抛出异常;
  • 4、sleep() 方法需要指定时间参数;yield() 方法出让 CPU 的执行权时间由 JVM 控制。

46. 具体调用wait()的方式是什么?wait()在循环还是if块调用?
  • 1、wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。

47. 使用对象的wait()方法需要注意什么?
  • 1、wait() 方法是线程间通信的方法之一;
  • 2、必须在 synchronized 方法或 synchronized 修饰的代码块中使用,否则会抛出 IllegalMonitorStateException;
  • 3、只能在加锁的对象调用 wait() 方法;
  • 4、加锁的对象调用 wait() 方法后,线程进入等待状态,直到在加锁的对象上调用 notify() 或者 notifyAll() 方法来唤醒之前进入等待的线程。

48. notify() 和 notifyAll() 有什么区别?
  • 1、notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
  • 2、notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

49. 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
  • Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

50. 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
  • 当线程A需要调用对象的 wait()方法的时候,这个线程A必须拥有该对象的锁,时间片用完后,线程A就会释放这个对象的锁,并进入等待状态,直到其他线程调用这个对象上的 notify()或者notifyall()方法。同样的,当线程B需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以只能在同步方法或者同步块中被调用。

51. 为什么 Thread 类的 sleep()和 yield ()方法是静态的?
  • Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

52. 如何停止一个正在运行的线程?
  • 在java中有以下3种方法可以终止正在运行的线程:
    1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止;
    2、使用stop方法强行终止,但是不推荐这个方法;
    3、使用interrupt方法中断线程。

53. interrupt、interrupted 和 isinterrupted 方法的区别?
  • 1、interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常;
  • 2、interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了;
  • 3、isInterrupted:查看当前中断信号是true还是false。

54. 如何在两个线程间共享数据?
  • 在两个线程间共享变量即可实现共享。

55. Java 如何实现多线程之间的通讯和协作?
  • 可以通过中断 和 共享变量的方式实现线程间的通讯和协作。

56. Java 中的锁是什么?
  • 在并发编程中,经常会遇到多个线程访问同一个共享变量,当同时对共享变量进行读写操作时,就会产生数据不一致的情况。

57. Java 中的锁有哪些?
  • Java 中常见的锁有
    ①synchronized(自动锁);
    ②可重入锁 java.util.concurrent.lock.ReentrantLock;
    ③可重复读写锁java.util.concurrent.lock.ReentrantReadWriteLock。

58. Java 中的锁使用注意事项:
  • 1、synchronized 修饰代码块时,最好不要锁定基本类型的包装类;
  • 2、synchronized 修饰代码块时,要线程互斥地执行代码块,需要确保锁定的是同一个对象,这点往往在实际编程中会被忽视;
  • 3、synchronized 不支持尝试获取锁、锁超时和公平锁;
  • 4、ReentrantLock 一定要记得在 finally{} 语句块中调用unlock() 方法释放锁,不然可能导致死锁;
  • 5、ReentrantLock 在并发量很高的情况,由于自旋很消耗 CPU 资源;
  • 6、ReentrantReadWriteLock 适合对共享资源写操作很少,读操作频繁的场景;可以从写锁降级到读锁,无法从读锁升级到写锁。

59. 什么是可重入锁?
  • 可重入锁指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。为了避免死锁的发生,JDK 中基本都是可重入锁。

60. synchronized 关键字的作用?
  • 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。

61. synchronized 关键字的用法有哪些?
  • 1、修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 2、修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 3、修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
  • 4、总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能。

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

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

64. synchronized 和 Lock 的区别?
  • 1、实现层面不一样。synchronized 是 Java 关键字,JVM层面实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁;
  • 2、是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要在 finally {} 代码块显式地中释放锁;
  • 3、是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时;
  • 4、获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功;
  • 5、功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率。

65. synchronized和ReentrantLock区别是什么?
  • 1、synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果;
  • 2、synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
  • 3、synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
  • 4、synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
  • 5、synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
  • 6、synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
  • 相同点:都可以做到同一线程,同一把锁,可重入代码块。

66. Lock 与 ReadWriteLock 的区别?
  • ReadWriteLock 定义了获取读锁和写锁的接口,读锁之间不互斥,非常适合读多、写少的场景。

67. 什么是线程同步和线程互斥?
  • 1、当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
  • 2、线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

68. 实现线程同步的方法有几种?
  • 1、同步代码方法:sychronized 关键字修饰的方法;
  • 2、同步代码块:sychronized 关键字修饰的代码块;
  • 3、使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制;
  • 4、使用重入锁实现线程同步:reentrantlock类是可重入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义。

69. 同步方法和同步块,哪个是更好的选择?
  • 同步的范围越小越好原则:
    1、同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁;
    2、同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

70. volatile关键字的作用是什么?
  • 1、保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见;
  • 2、通过禁止编译器、CPU 指令重排序和部分 happens-before 规则,解决有序性问题。

71. volatile如何实现可见性?
  • 1、在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令;
  • 2、Lock 前缀的指令会引起 CPU 缓存写回内存;
  • 3、一个 CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效;
  • 4、volatile 变量通过缓存一致性协议保证每个线程获得最新值;
  • 5、缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改;
  • 6、当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存。

72. volatile如何实现有序性?
  • 1、三个 happens-before 规则实现:
    ①对一个 volatile 变量的写 happens-before 任意后续对这个 volatile 变量的读
    ②在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作
    ③happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C
  • 2、内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令)禁止重排序:
    ①在程序运行时,为了提高执行性能,在不改变正确语义的前提下,编译器和 CPU 会对指令序列进行重排序;
    ②Java 编译器会在生成指令时,为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的指令重排序;
    ③编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令;
    ④存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

73. 什么叫线程安全?
  • 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

74. volatile关键字能保证线程安全吗?
  • 单纯使用 volatile 关键字是不能保证线程安全的:
    1、volatile 只提供了一种弱的同步机制,用来确保将变量的更新操作通知到其他线程;
    2、volatile 语义是禁用 CPU 缓存,直接从主内存读、写变量。表现为:更新 volatile 变量时,JMM 会把线程对应的本地内存中的共享变量值刷新到主内存中;读 volatile 变量时,JMM 会把线程对应的本地内存设置为无效,直接从主内存中读取共享变量;
    3、当把变量声明为 volatile 类型后,JVM 增加内存屏障,禁止 CPU 进行指令重排。

75. 在 Java 程序中怎么保证多线程的运行安全?
  • 1、使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
  • 2、使用自动锁 synchronized。
  • 3、使用手动锁 Lock。

76. 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?
  • 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

77. 线程类的构造方法、静态块是被哪个线程调用的?
  • 线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
  • Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的;
  • Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的。

78. 什么是 happens-before 原则?
  • 我们编写的程序都要经过优化后(编译器和处理器会对我们的程序进行优化以提高运行效率)才会被运行,优化分为很多种,其中有一种优化叫做重排序,重排序需要遵守happens-before规则,不能说你想怎么排就怎么排,happens-before规则如下:
  • 1、程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。(这里涉及到 CPU 指令重排,所以需要加入内存屏障保证有序性)
  • 2、管程锁定规则:对一个锁的解锁操作,先行发生于后续对这个锁的加锁操作。这里必须强调的是同一个锁。
  • 3、volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 4、线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 5、线程 join() 规则:被调用 join() 方法的线程的所有操作先行发生于 join() 的返回。
  • 6、传递性规则:操作 a 先发生于操作 b,操作 b 先发生于操作 c,则操作 a 先发生于操作 c。
  • 7、对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法。

79. 为什么代码会重排序?
  • 在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
    1、在单线程环境下不能改变程序运行的结果;
    2、存在数据依赖关系的不允许重排序。
    需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

80. happens-before规则和as-if-serial规则的区别?
  • 1、happens-before关系保证正确同步的多线程程序的执行结果不被改变;as-if-serial语义保证单线程内程序的执行结果不被改变。
  • 2、happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的;as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。
  • 3、happens-before 和 as-if-serial 这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

81. JDK中Atomic开头的原子类实现原子性的原理是什么?
  • 1、JDK Atomic开头的类,是通过 CAS 原理解决并发情况下原子性问题;
  • 2、CAS 包含 3 个参数,CAS(V, E, N)。V 表示需要更新的变量,E 表示变量当前期望值,N 表示更新为的值。只有当变量 V 的值等于 E 时,变量 V 的值才会被更新为 N。如果变量 V 的值不等于 E ,说明变量 V 的值已经被更新过,当前线程什么也不做,返回更新失败;
  • 3、当多个线程同时使用 CAS 更新一个变量时,只有一个线程可以更新成功,其他都失败。失败的线程不会被挂起,可以继续重试 CAS,也可以放弃操作;
  • 4、CAS 操作的原子性是通过 CPU 单条指令完成而保障的。JDK 中是通过 Unsafe 类中的 API 完成的;
  • 5、在并发量很高的情况,会有大量 CAS 更新失败,所以需要慎用。

82. 什么是CAS?
  • Java 并发机制实现原子操作有两种: 一种是锁,一种是CAS。CAS是Compare And Swap(比较并替换)的缩写。java.util.concurrent.atomic中的很多类,如(AtomicInteger AtomicBoolean AtomicLong等)都使用了CAS。
  • CAS是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。

83. 什么是乐观锁和悲观锁?
  • 乐观锁(Optimistic Lock):线程每次在处理共享数据时都不会上锁,在更新时会通过数据的版本号等机制判断其他线程有没有更新数据。乐观锁适合读多写少的应用场景;
  • 悲观锁(Pessimistic Lock):线程每次在处理共享数据时都会上锁,其他线程想处理数据就会阻塞直到获得锁。

84. 乐观锁和悲观锁适应场景?
  • 乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量;
  • 在写比较多的场景下,乐观锁会因为版本不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁。

85. CAS会产生什么问题?
  • 1、ABA 问题:
    比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
  • 2、循环时间长开销大:
    对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
  • 3、只能保证一个共享变量的原子操作:
    当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

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

87. synchronized和volatile的区别是什么?
  • 1、synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量;
  • 2、synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性;
  • 3、synchronized 线程阻塞,volatile 线程不阻塞;
  • 4、volatile 本质是告诉 jvm 当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞;
  • 5、volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

88. Java 中能创建 volatile 数组吗?
  • 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

89. volatile 变量和 atomic 变量区别是什么?
  • 1、volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的;
  • 2、而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

90. volatile 能使得一个非原子操作变成原子操作吗?
  • 1、关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步;
  • 2、虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。

91. 多线程锁(synchronized锁)的升级原理是什么?
  • 在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
  • 无状态锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
  • 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放;偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。
  • 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能;当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
  • 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态;重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

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

93. 说说对于sychronized同步锁的理解?
  • 1、每个 Java 对象都有一个内置锁;
  • 2、线程运行到非静态的 synchronized 同步方法上时,自动获得实例对象的锁;
  • 3、持有对象锁的线程才能运行 synchronized 同步方法或代码块时;
  • 4、一个对象只有一个锁;
  • 5、一个线程获得该锁,其他线程就无法获得锁,直到第一个线程释放锁。任何其他线程都不能进入该对象上的 ;synchronized 方法或代码块,直到该锁被释放;
  • 6、释放锁是指持锁线程退出了 synchronized 同步方法或代码块;
  • 7、类可以同时拥有同步和非同步方法;
  • 8、只有同步方法,没有同步变量和类;
  • 9、在加锁时,要明确需要加锁的对象;
  • 10、线程可以获得多个锁;
  • 11、同步应该尽量缩小范围。

94. 在监视器(Monitor)内部,是如何做线程同步的?
  • 1、在 java 虚拟机中,每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联,为了实现监视器的互斥功能,每个对象都关联着一把锁;
  • 2、一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

95. 什么是不可变对象,它对写并发应用有什么帮助?
  • 1、不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects);
  • 2、不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等;
  • 3、只有满足如下状态,一个对象才是不可变的;
    ①它的状态不能在创建后再被修改;
    ②所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出);
  • 4、不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

96. 线程 B 怎么知道线程 A 修改了变量?
  • 1、volatile 修饰变量
  • 2、synchronized 修饰修改变量的方法
  • 3、wait/notify
  • 4、while 轮询

97. Java中实现线程通信方式有哪些?
  • 1、对象的 wait(long timeout)、wait(long timeout, int nanos)、wait() 方法,组合对象的 notify()、notifyAll();
  • 2、显示锁:Lock.newCondition()、Condition await 系列方法、Condition signal()、signalAll();
  • 3、信号量:Semaphore acquire 系列方法、release()系列方法。

98. Java 中怎么获取一份线程 dump 文件?
  • 1、Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。
  • 2、在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。
  • 3、在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。

猜你喜欢

转载自blog.csdn.net/qq_38132105/article/details/107418917