Java-多线程-八股文

  1. 线程安全的理解?
  2. 线程安全说的是,当多个线程并发访问互斥资源时,读写互斥资源的代码逻辑能正常处理,获得正确结果,不会互相干扰的情况。
    
  3. 守护线程的理解?
  4. 守护线程是与普通线程相区分的概念,用户一般使用的就是普通线程,普通线程有自身独立的生命周期,而守护线程的生命周期取决于普通线程,只有当所有相关的普通线程都结束时,守护线程才会结束。
    守护线程是为所有普通线程提供服务的线程
    
  5. threadlocal的底层原理
  6. threadlocal:在线程里面多保存了一份map结构,由于变量存在线程内部,从而保证不同线程互相不干扰。
    现在大多数用的都是线程池,由于线程都是复用的,而threadlocal往线程里面添加了一个map,因此,需要代码里面手动回收移除threadlocal保存的数据,否则,线程一直复用,数据量会一直累加至OOM。
    
  7. threadlocal的应用场景
  8. threadlocal是基于副本的隔离机制,保证了共享变量的修改安全
    1.线程上下文传递:在不修改方法签名的情况下,将threadlocal设置为成员变量,既能全类通用,又能线程安全,还不需要修改方法签名
    2.数据库的连接管理:同一段处理的多线程并发时,由于数据库连接用的是threadlocal,因此,数据库连接可以互相不影响[否则成员变量的话,同一段处理会形成共用,非成员变量的话,又无法形成线程池的复用]
    3.事务管理,事务上下文通过这个隔离互相不影响
    
  9. 并发、并行与串行之间的区别?
  10. 并行:多条处理流同时执行,例如多端口同时工作,多条线路同时传送数据
    串行:多条处理流排队执行。
    并发:例如微机系统上有多个进程存活,从宏观上,多个进程上同时工作,是个并行的流程,但是,从微观底层上来看,多个进程是由CPU通过时间片轮转,逐个调度执行的,是个串行的流程,这个就是并发。
    
  11. Java死锁应如何避免?-√
  12. Java死锁由四个因素导致:
    • 资源是互斥资源 每次仅可由单个线程持有
    • 在获得所有资源之前,线程自身不释放已持有资源
    • 所需资源在其他线程上,且不可被剥夺
    • 循环依赖,A持有B所需资源,B持有A所需资源
    应当打破第四点因素,即可避免死锁发生。 即将所有线程持有资源的顺序固定,避免产生循环依赖 每个锁的持有时间应当设置过期时间,确保锁最终一定会被释放
  13. 线程池底层工作原理-√
  14. num:线程数量
    当有任务需求时,
    当num<poolSize时,优先创建线程
    当num>poolSize时,且queue没满时,将任务添加到队列中排队
    当maxpoolsize>num>poolSize时,且queue已满时,增加线程处理任务
    当maxpoolsize==num,且queue已满时,不再新增线程,会按照handler所指定策略处理新任务。
    同时会检查,是否有线程的空闲时间大于keepalivetime,如果有,终止对应线程
    
  15. 线程池中阻塞队列的作用?线程池为何先添加队列而不是先创建线程-√
  16. 阻塞队列可以通过阻塞保留那些想要入队的任务;阻塞队列自带阻塞和唤醒功能,不需要一直占有CPU资源。
    因为创建新线程需要获取全局锁,影响其他线程的效率,且线程数量不足有可能是临时性不足,因此,优先排队等待。
    
  17. 线程池如果阻塞队列都满了,需要新开线程,执行的是新任务还是队头任务?
  18. 阻塞队列中的队头任务由核心线程调度
    
    新开线程会处理阻塞队列之外的新任务
    
  19. ReentrantLock中的公平锁与非公平锁的底层实现
  20. 加锁[竞争锁]:
    公平锁:新线程直接进入到队列
    非公平锁:先直接竞争锁,竞争不到再进入队列
    
    唤醒:无论公平锁还是非公平锁都会直接唤醒队列第一个获取锁
    
  21. ReentrantLock的tryLock()与lock()区别
  22. tryLock()可能加锁成功[返回真],也可能加锁不成功[返回假],非阻塞加锁
    lock()阻塞性加锁,一直执行,直到加锁成功
    
  23. CountDownLatch与Semphore的区别和底层原理
  24. CountDownLatch共两个核心方法:await() 以及  countDown()
    Semphore 也是两个核心方法:acquire()  以及  release()
    
    CountDownLatch 更强调依赖关系:只有前置函数调用了countDown()以后,将信号量置0以后,await线程才会执行
    Semphore 更强调共享资源数量:例如只有三台打印机,那么就只会有三个线程能够acquire成功,否则就只能排队等待[保存中间库那个功能 感觉用这个效果更好 逻辑更清晰]
    
  25. sychronized的偏向锁、轻量锁以及重量锁
  26. sychronized语法,某个线程持有锁的时候,锁就已经是偏向锁
    当某个线程持有锁,且有其他线程也在等待锁释放,锁就升级为轻量锁
    某个线程持有锁,其他线程会自旋尝试获取锁,自旋次数达到一定程度,线程仍然没有获取到锁时,轻量锁就升级为重量锁
    
  27. ReentrantLock与synchronized的区别?-√
  28. 含义:ReentrantLock是一个类;synchronized是一个关键字
    是否公平:ReentrantLock可以加公平锁,也可以加非公平锁;synchronized只能是非公平锁。
    synchronized自动加锁释放锁,ReentrantLock需要手动加锁释放锁
    synchronized是JVM层面的锁,ReentrantLock是API层面的锁
    
  29. 什么是可重入锁
  30. 可重入锁,即线程抢到锁以后,再去竞争同一把锁的时候,不需要等待,仅需要记录重入次数
    可重入锁主要是避免线程死锁的问题,自己去等待自己已经抢到的锁,这是不合理的。
    
  31. 对AQS的理解,AQS如何实现可重入锁-※
  32. AQS是一个用于JAVA线程同步的框架。
    AQS内部维护了一个状态变量state和一个线程双向队列。
    在可重入锁的场景下,state表示加锁次数,当state减少至0时,代表持有锁的线程释放锁了,就唤醒下一个线程竞争锁。
    
  33. Java中的CAS机制
  34. CAS是Java中Unsafe类中的方法,全称是compareAndSwap,用于绑定两个操作原子性操作[判断是否为期望值,如果是期望值就更新目标值]
    使用场景:AtomicInteger 该类就包含了CAS的实现,可用于共享资源的修改
    
  35. 一个线程调用两次start()
  36. 线程按5态模型而言,共启动、排队[就绪]、冻结、运行、结束,一般情况下,单个线程仅能启动一次,因为仅有未启动线程可以启动,运行、排队与冻结线程不可重复启动。
    
  37. 线程创建的三种方式
  38. 公共代码:
    @Component
    public class ShareLock {
        public int lockNum = 0;
        public int lockCircleNum = 3;
    }
    
    public class Count{//三种方式都要修改类签名 但是方法内容不变
        private final int printNum = 3;
        private ShareLock item = null;
        private int state = 100;
        private char content = 100;
        public Count(ShareLock itemInput,int stateInput,char contentInput) {
            this.item = itemInput;
            this.state = stateInput;
            this.content = contentInput;
        }
        public void run(){
            for(int i=0,countNum=0;countNum<this.printNum;i++){
               if(item.lockNum==state){
                   synchronized (this){
                       System.out.println(content);
                       item.lockNum = (item.lockNum+1+item.lockCircleNum)%item.lockCircleNum;
                   }
                   countNum+=1;
               }
            }
        }
    }
    
    
    1. 继承Thread类
    2. public class Count --->  public class Count extends Thread
      
      @Component
      public class DebugMain {
          @Autowired
          public ShareLock shareLock;
          public void debug() {
              Count countA = new Count(this.shareLock,0,'A');
              Count countB = new Count(this.shareLock,1,'B');
              Count countC = new Count(this.shareLock,2,'C');
              countA.start();
              countB.start();
              countC.start();
          }
      }
      
    3. 实现Runnable接口
    4. public class Count --->  public class Count implements Runnable
      
      @Component
      public class DebugMain {
          @Autowired
          public ShareLock shareLock;
          public void debug() {
              Count countA = new Count(this.shareLock,0,'A');
              Count countB = new Count(this.shareLock,1,'B');
              Count countC = new Count(this.shareLock,2,'C');
              new Thread(countA).start();
              new Thread(countB).start();
              new Thread(countC).start();
          }
      }
      
    5. 实现Callable和Future创建线程
    6. public class Count implements Callable<String> {
          private final int printNum = 3;
          private ShareLock item = null;
          private int state = 100;
          private char content = 100;
          public Count(ShareLock itemInput,int stateInput,char contentInput) {
              this.item = itemInput;
              this.state = stateInput;
              this.content = contentInput;
          }
          public String call(){
              for(int i=0,countNum=0;countNum<this.printNum;i++){
                 if(item.lockNum==state){
                     synchronized (this){
      //                   System.out.println(content);
                         item.lockNum = (item.lockNum+1+item.lockCircleNum)%item.lockCircleNum;
                     }
                     countNum+=1;
                 }
              }
              SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              return Thread.currentThread().getName()+">>>>>"+this.content+"<<<<<<"+sdf.format(System.currentTimeMillis());
          }
      }
      
      @Component
      public class DebugMain {
          @Autowired
          public ShareLock shareLock;
          public void debug() throws ExecutionException, InterruptedException {
              SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              Count countA = new Count(this.shareLock,0,'A');
              Count countB = new Count(this.shareLock,1,'B');
              Count countC = new Count(this.shareLock,2,'C');
              System.out.println(Thread.currentThread().getName()+">>>>>11111111<<<<<<"+sdf.format(System.currentTimeMillis()));
              FutureTask<String> stringFutureTaskA = new FutureTask<>(countA);
              FutureTask<String> stringFutureTaskB = new FutureTask<>(countB);
              FutureTask<String> stringFutureTaskC = new FutureTask<>(countC);
              new Thread(stringFutureTaskA).start();
              System.out.println(Thread.currentThread().getName()+">>>>>222222222<<<<<<"+sdf.format(System.currentTimeMillis()));
              new Thread(stringFutureTaskB).start();
              System.out.println(Thread.currentThread().getName()+">>>>>33333333<<<<<<"+sdf.format(System.currentTimeMillis()));
              new Thread(stringFutureTaskC).start();
              System.out.println(Thread.currentThread().getName()+">>>>>44444444<<<<<<"+sdf.format(System.currentTimeMillis()));
              System.out.println(stringFutureTaskA.get());
              System.out.println(Thread.currentThread().getName()+">>>>>5555555555<<<<<<"+sdf.format(System.currentTimeMillis()));
              System.out.println(stringFutureTaskB.get());
              System.out.println(Thread.currentThread().getName()+">>>>>66666666<<<<<<"+sdf.format(System.currentTimeMillis()));
              System.out.println(stringFutureTaskC.get());
              System.out.println(Thread.currentThread().getName()+">>>>>77777777<<<<<<"+sdf.format(System.currentTimeMillis()));
          }
      }
      
      由于callable要验证回调,因此,代码增加了返回值以及外层打印与内层打印,打印结果如下:[对调用方而言,最终结果打印的地方依然是串行的,因此,如果正式使用时,可能更多情况下,还需要在Runnable实现接口,run里面去调用这种callable才能保证不影响原来的主进程]
      
      http-nio-8383-exec-1>>>>>11111111<<<<<<2023-07-12 19:36:27
      http-nio-8383-exec-1>>>>>222222222<<<<<<2023-07-12 19:36:27
      http-nio-8383-exec-1>>>>>33333333<<<<<<2023-07-12 19:36:27
      http-nio-8383-exec-1>>>>>44444444<<<<<<2023-07-12 19:36:27
      Thread-13>>>>>A<<<<<<2023-07-12 19:36:28
      http-nio-8383-exec-1>>>>>5555555555<<<<<<2023-07-12 19:36:28
      Thread-14>>>>>B<<<<<<2023-07-12 19:36:28
      http-nio-8383-exec-1>>>>>66666666<<<<<<2023-07-12 19:36:28
      Thread-15>>>>>C<<<<<<2023-07-12 19:36:28
      http-nio-8383-exec-1>>>>>77777777<<<<<<2023-07-12 19:36:28
      
  39. Thread和Runnable的区别?(3)
  40. 1.定义: runnable是接口 thread是类,这个类也是实现了该接口的一个扩展而已
    2.用法:继承thread以后可直接启动;实现接口以后还是需要new一个thread才能启动
    3.限制:类只能单继承,接口可以多实现,如果已有继承关系,就只能实现接口
    
  41. 1 W 条数据 分10个线程 分别录入数据库 如何保证 10个线程组成的这个任务的事务性?
  42. 10个线程都写入临时表
    每个线程都通过标记来记录成功或失败,10个标记写入Redis[setnx]或者单机上的共享变量[CAS机制]
    总体上用countdownlatch 当10个依赖都搞定的时候 才会执行第11个 第11个去判断前10个标记
    10个标记都OK 利用一条SQL插入最终的表 或者 记录表增加这个临时表
    有线程失败了 直接返回失败 并删除临时表
    
posted @ 2023-07-16 17:50  356a  阅读(14)  评论(0编辑  收藏  举报