如何保证线程安全?

通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。

  • synchronized关键字
  • lock接口
  • volatile+CAS【单纯的volatile是轻量级的同步机制保证可见性但是不具备原子性所以要配合CAS来实现线程安全】
  • atomic原子类

    线程的基本状态以及状态之间的关系?

  • 新建( new ):新创建了一个线程对象。

  • 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。
  • 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ),执行程序代码。
  • 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
    • 等待阻塞:运行( running )的线程执行 o . wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。
    • 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
    • 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
  • 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。

image.png

解释一下什么是线程池(thread pool)?

在面向对象编程中,创建和销毁对象是费时的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

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

    说明一下线程池有什么优势?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。
  • 提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

    说明同步和异步

    如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

    介绍一下线程同步和线程调度的相关方法。

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
  • 通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。

    当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

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

    简述一下线程的sleep()方法和yield()方法有什么区别?

  • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

  • 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
  • sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
  • sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    说明一下sleep() 和 wait() 有什么区别?

    sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
    wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

    Java中有几种方法可以实现一个线程?

    Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用四种方式来创建线程
    1)继承Thread类创建线程

  • 重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。

    1. package com.thread;
    2. public class FirstThreadTest extends Thread{
    3. int i = 0;
    4. //重写run方法,run方法的方法体就是现场执行体
    5. public void run()
    6. {
    7. for(;i<100;i++){
    8. System.out.println(getName()+" "+i);
    9. }
    10. }
    11. public static void main(String[] args)
    12. {
    13. for(int i = 0;i< 100;i++)
    14. {
    15. System.out.println(Thread.currentThread().getName()+" : "+i);
    16. //Thread.currentThread()方法返回当前正在执行的线程对象。
    17. //GetName()方法返回调用该方法的线程的名字。
    18. if(i==20)
    19. {
    20. new FirstThreadTest().start();
    21. new FirstThreadTest().start();
    22. }
    23. }
    24. }
    25. }

    2)实现Runnable接口创建线程

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。

    1. package com.thread;
    2. public class RunnableThreadTest implements Runnable{
    3. private int i;
    4. public void run(){
    5. for(i = 0;i <100;i++){
    6. System.out.println(Thread.currentThread().getName()+" "+i);
    7. }
    8. }
    9. public static void main(String[] args){
    10. for(int i = 0;i < 100;i++){
    11. System.out.println(Thread.currentThread().getName()+" "+i);
    12. if(i==20){
    13. RunnableThreadTest rtt = new RunnableThreadTest();
    14. new Thread(rtt,"新线程1").start();
    15. new Thread(rtt,"新线程2").start();
    16. }
    17. }
    18. }
    19. }

    3)使用Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值 ```java package com.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable{

  1. public static void main(String[] args){
  2. CallableThreadTest ctt = new CallableThreadTest();
  3. FutureTask<Integer> ft = new FutureTask<>(ctt);
  4. for(int i = 0;i < 100;i++){
  5. System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
  6. if(i==20){
  7. new Thread(ft,"有返回值的线程").start();
  8. }
  9. }
  10. try{
  11. System.out.println("子线程的返回值:"+ft.get());
  12. } catch (InterruptedException e){
  13. e.printStackTrace();
  14. } catch (ExecutionException e){
  15. e.printStackTrace();
  16. }
  17. }
  18. @Override
  19. public Integer call() throws Exception{
  20. int i = 0;
  21. for(;i<100;i++){
  22. System.out.println(Thread.currentThread().getName()+" "+i);
  23. }
  24. return i;
  25. }

}

  1. 4)使用线程池例如用Executor框架,线程池内部通过ThreadFactory创建
  2. <a name="7BQ01"></a>
  3. ## 用什么关键字修饰同步方法?
  4. synchronized关键字
  5. <a name="4908c218"></a>
  6. ## stop()和suspend()方法为何不推荐使用,请说明原因?
  7. 为什么不推荐用 stop()和 suspend()
  8. - stop这个方法将终止所有未结束的方法,包括run方法。当一个线程停止时候,他会立即释放所有他锁住对象上的锁。这会导致对象处于不一致的状态。假如一个方法在将钱从一个账户转移到另一个账户的过程中,在取款之后存款之前就停止了。那么现在银行对象就被破坏了。因为锁已经被释放了。当线程想终止另一个线程的时候,它无法知道何时调用stop是安全的,何时会导致对象被破坏。所以这个方法被弃用了。你应该中断一个线程而不是停止他。
  9. - suspend被弃用的原因是因为它会造成死锁。suspend方法和stop方法不一样,它不会破换对象和强制释放锁,相反它会一直保持对锁的占有,一直到其他的线程调用resume方法,它才能继续向下执行。假如有AB两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在或者相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时AB两个线程都不能继续向下执行而形成了死锁。
  10. <a name="m5FaU"></a>
  11. ## 启动一个线程是用run()还是start()?
  12. 启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。
  13. <a name="5HfMw"></a>
  14. ## 使用内部类实现线程设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。
  15. ```java
  16. public class Main{
  17. private int j;
  18. public static void main(String args[]){
  19. Main tt=new Main();
  20. Inc inc=tt.new Inc();
  21. Dec dec=tt.new Dec();
  22. for(int i=0;i<2;i++){
  23. Thread t=new Thread(inc);
  24. t.start();
  25. t=new Thread(dec);
  26. t.start();
  27. }
  28. }
  29. private synchronized void inc(){
  30. j++;
  31. System.out.println(Thread.currentThread().getName()+"-inc:"+j);
  32. }
  33. private synchronized void dec(){
  34. j--;
  35. System.out.println(Thread.currentThread().getName()+"-dec:"+j);
  36. }
  37. class Inc implements Runnable{
  38. public void run(){
  39. for(int i=0;i<100;i++){
  40. inc();
  41. }
  42. }
  43. }
  44. class Dec implements Runnable{
  45. public void run(){
  46. for(int i=0;i<100;i++){
  47. dec();
  48. }
  49. }
  50. }
  51. }

说明一下在监视器(Monitor)内部,是如何做到线程同步的?在程序又应该做哪种级别的同步呢?

监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

分析一下同步方法和同步代码块的区别是什么?

区别:

  • 同步方法默认用this或者当前类class对象作为锁;
  • 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。