线程QA

Q&A

1. 线程与进程的区别?

  进程是操作系统分配资源的最小单元,线程则是操作系统调度的最小单元。一个程序至少有一个进程,一个进程至少有一个线程。

2. 上下文切换?

  操作系统通过将单个CPU的时间片分配给多个进程来达到并行处理的效果,多线程使用单个或少于线程数CPU时,需要轮转使用CPU。

  不同线程切换CPU时,因为当前线程任务并没有完成,需要保存线程的运行状态,以便再下次切回时能够继续执行,在此过程中发生的切换数据即上下文切换,其目的是为了方便线程从中断点恢复执行。

3. 什么是线程调度器和时间分片?

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

  时间分片(Time Slicing)是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。

4. 线程之间是如何通信的?

  共享变量,中断。当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。Object类中wait() otify() otifyAll()方法可以用于线程间通信关于资源的锁的状态。

5. notify()与notifyAll()区别?

  notifyAll()可以唤醒所有等待的线程,notify()只能唤醒一个

6. wait()与sleep()区别?

  等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。wait()方法会释放CPU执行权和占有的锁。

  sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列,与yield相比,它会使线程较长时间得不到运行。

  yield()方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。

  wait和notify必须配套使用,即必须使用同一把锁调用;

  wait和notify必须放在一个同步块中调用wait和notify的对象必须是他们所处同步块的锁对象。

7. 为什么wait和notify方法要在同步块中调用?

  Java API强制要求这样做,非同步块内调用会抛出IllegalMonitorStateException异常。

  当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。避免wait和notify之间产生竞态条件。

8. 为什么弃用stop()?

  

9. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

  一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法。

10. 为什么Thread类的sleep()和yield ()方法是静态的?

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

11. 如何确保main()方法所在的线程是Java程序最后结束的线程?

  可以使用Thread类的join()方法来确保所有程序创建的线程在main()方法退出前结束。

12. 为什么你应该在循环中检查等待条件?

  处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

13. 什么是ThreadLocal变量?

  ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。

14. 在java中守护线程和本地线程区别?

  java中的线程分为两种:守护线程(Daemon)和用户线程(User)。

任何线程都可以设置为守护线程和用户线程,两者的区别:

  唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。只要有任何非守护线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为守护线程。 注意: 后台进程在不执行finally子句的情况下就会终止其run()方法。比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。

扩展:

  Thread Dump打印出来的线程信息,含有daemon字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows下的监听Ctrl+break的守护进程、Finalizer守护进程、引用处理守护进程、GC守护进程。

15. Java中interrupted 和 isInterrupted方法的区别?

  interrupt方法用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

  interrupted 查询当前线程的中断状态,并且清除原状态。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了。

  isInterrupted 仅仅是查询当前线程的中断状态

16. 什么是Callable和Future?

  Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable。

  Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

17. java中有几种方法可以实现一个线程?

  • 继承 Thread 类

  • 实现 Runnable 接口

  • 实现 Callable 接口 + FutureTask,需要实现的是 call() 方法

  • 线程池

18. 为什么在Java中不推荐使用线程组?

  在未捕获异常的处理器章节有提到现在不推荐使用线程组,因为有更好的线程池,线程组自己不是线程安全的,且其stop等方法会造成死锁等安全问题。前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。

  创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。

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

  • 使用共享变量的方式,在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。

  • 使用interrupt方法终止线程,如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法,或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法,或者调用了DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用stop()方法,而是使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。

20. 什么是Executors框架?

  Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。

21. 为什么要使用Executor线程池框架 ?

  1. 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
  2. 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
  3. 直接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

22. 使用Executor线程池框架的优点?

  1. 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
  2. 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
  3. 框架中已经有定时、定期、单线程、并发数控制等功能。

  Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。

  Executor 接口对象能执行我们的线程任务。

  ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

  使用ThreadPoolExecutor 可以创建自定义线程池。

  Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果。

   综上所述使用线程池框架Executor能更好的管理线程、提供系统资源使用率。

23. Java中用到的线程调度算法是什么?

  采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优先级上,如非特别需要,尽量不要用,防止线程饥饿。

  计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权.

  有两种调度模型:分时调度模型和抢占式调度模型。

  分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。

  java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。

24. 什么是FutureTask?使用ExecutorService启动任务。

  在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

25. 什么是并发容器的实现?

  何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。

  可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。

  并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。

26. 多线程同步和互斥有几种实现方法,都是什么?

  线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

  线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

  线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

27. 为什么代码会重排序?

  在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  在单线程环境下不能改变程序运行的结果;

  存在数据依赖关系的不允许重排序

  需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

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

  同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

  同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

29. 什么是Java Timer 类?如何创建一个有特定时间间隔的任务?

  java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。

  java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。

  目前有开源的Qurtz可以用来创建定时任务。

30. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

  当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。

  但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把run方法当作普通方法去执行。

31. 如何确保线程安全?

  在Java中可以有很多方法来保证线程安全——同步,使用原子类(atomic concurrent classes),实现并发锁,使用volatile关键字,使用不变类和线程安全类。

32. 怎么检测一个线程是否拥有锁?

  在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。


参考博客和文章书籍等:

https://zhuanlan.zhihu.com/p/58117761

https://zhuanlan.zhihu.com/p/58265081

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容