JUC的面试笔记

JUC

java中创建线程的方法

  • 继承Thread类并且重写run方法,调用继承的start方法(它是一个native方法,启动一个新的线程,并执行run方法)
    • Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。
  • 实现Runable接口并且重写run方法,创建Thread的对象在构造器中传入目标对象,调用Thread的对象的start方法(该方式的的优点:避免了单继承的局限性,优先使用接口,方便共享资源)
    • 不会抛出异常
    • 没有返回值
  • 实现Callable接口并且重写call方法,创建FutureTask对象在构造器中传入目标对象,创建Thread对象在构造器中传入FutureTask对象对象,调用Thread的对象的start方法。 (通过FutureTask对象的get方法可以获取call方法的返回值)
    • 当调用get方法的时候会抛出ExecutionException和InterruptedException异常
    • 有返回值
  • 使用线程池Executors的四大方法来创建线程(用于缓存线程,不需要重复创建和销毁线程)
    • ExecutorService的execute方法没有返回值(传入实现Runnable接口的类)
    • ExecutorService的submit方法有返回值为Future类,Future类的get方法可以获取call方法的返回值(一般传入实现Callable接口的类)

线程池

在这里插入图片描述

线程池的的主要工作:

控制运行的线程数量,处理过程中将任务放入队列中,然后在线程创建后启动这些任务。如果线程数量超过了最大数量,超出数量的线程需要排队等候,等待其它线程执行完毕,再从队列中取出任务来执行。

线程池的优点:

  • 线程复用(降低资源消耗、提高响应速度)
  • 控制线程的并发数量
  • 管理线程的生命周期

线程池的组成(四个部分):

  • 线程池管理器:用于创建并管理线城池(避免创建过多的线程)
  • 工作线程:线程池中的线程
  • 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  • 任务队列:用于存放待处理的任务,提供一种缓存机制

线程池的四大方法:

底层调用的都是ThreadPoolExecutor类,一般都是由这个类自定义创建线程池

  • 允许请求队列的长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
    • Executors.newSingleThreadExecutor():只有返回一个线程,当这个线程池中的线程死后或者发生异常时会重新启动一个线程来接着执行任务
    • Executors.newFixedThreadPool(int):执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
  • 允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
    • Executors.newCachedThreadPool():执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。 可扩容,遇强则强。终止并从缓存中移除哪些已有60秒钟未使用的线程。
    • Executors.newScheduledThreadPool() :创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

线程池的七大参数:

  • corePollSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:空闲的线程保留的时间
  • TimeUnit:空闲线程的保留时间单位
  • BlockingQueue< Runnable >:阻塞队列,存储执行的任务
  • ThreadFactory:线程工厂,用来创建线程
  • RejectedExecutionHandler:拒绝策略(队列已满,而且任务量大于最大线程)
    • ThreadPoolExecutor.AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常
    • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务 (重复此过程)
    • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务(意思就是让主线程处理这个任务)

ThreadPoolExecutor的底层原理:

  • 在创建了线程池后,开始等待请求
  • 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    • 如果正在运行的线程数量小于coolPollSize的数量,则会马上进行创建线程运行这个任务
    • 如果正在运行的线程数量大于或等于coolPollSize的数量,则将会把这个任务放入队列中
    • 如果这个时候队列满了并且正在运行的线程数量小于maximumPoolSize,则还会创建非核心线程立刻运行这个任务
    • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,则线程池会启动饱和拒绝策略来执行
  • 当一个线程完成任务时,它会从队列中调用一个任务来执行
  • 当一个线程没有接受请求超过一定的时间(keepAliveTime)时,线程会判断:
    • 如果当前运行的线程数大于coolPollSize,则这个线程就会被停掉,最终的会收缩到corePollSize的大小

线程是否越多越好:

  • CPU密集型程序:针对于计算为主的程序,线程数量最好等于电脑cpu的数量
  • IO密集型:针对于磁盘或网络传输文件为主的程序,线程数量最好等于IO的任务数量

为什么要使用Executor框架

Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。可以限制线程的数量并且可以回收再利用这些线程。

  • 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、 耗资源的。
  • 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
  • 使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
  • Executor 和 Executors 的区别:
    • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
    • Executor 接口对象能执行我们的线程任务。
    • ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
    • Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。

什么是 FutureTask

  • 在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。

  • 一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装

    • 调用Runnable对象,实际上时返回的Callable对象,下面是FutureTask 的底层代码

      第一步
      public FutureTask(Runnable runnable, V result) {
              this.callable = Executors.callable(runnable, result);
              this.state = NEW;       // ensure visibility of callable
          }
      
      第二步
       public static <T> Callable<T> callable(Runnable task, T result) {
              if (task == null)
                  throw new NullPointerException();
              return new RunnableAdapter<T>(task, result);
          }
      
      第三步
       static final class RunnableAdapter<T> implements Callable<T> {
              final Runnable task;
              final T result;
              RunnableAdapter(Runnable task, T result) {
                  this.task = task;
                  this.result = result;
              }
              public T call() {
                  task.run();
                  return result;
              }
          }
      
  • 由于 FutureTask 也是实现了 Runnable接口所以它可以提交给 Executor线程池来执行。

如何停止一个线程:

  • 正常运行结束

  • 使用"退出标志"退出线程(将run()的所有内容放在while(flag)中))

  • 如果子线程有休眠时,使用interrupt方法进行立即打断,会抛出InterruptedException异常,然后通过try catch捕获异常通过break跳出循环,才能正常结束run方法。

    • public class ThreadSafe extends Thread {
       public void run() {
       while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
       try{
              Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
                }catch(InterruptedException e){
               e.printStackTrace();
               break;//捕获到异常之后,执行 break 跳出循环
            }
           }
         }
      }
      
  • stop方法或者destroy方法结束线程(一般不推荐使用,容易导致死锁)

  • 虚拟机退出,所有的进程都会结束

notify()和notifyAll()方法的区别

都是Object类的方法

  • notify可能会导致死锁,唤醒一个正在等待相应对象锁的线程
    • notify方法的正确使用场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中.
  • notifyAll不会导致死锁,唤醒所有正在等待相应对象锁的线程,互相争夺该对象锁。

sleep()和wait()方法的区别

  • 来自不同的类
    • wait来自Object类
    • sleep来自Thread类
  • 有没有释放锁资源
    • wait释放锁资源,可以使其他线程使用同步代码块和同步方法
      • wait(100L):进入WaitSet等待队列,当超时后会进入就绪队列或者在该规定期间内也可以被notify或者notifyAll唤醒进入就绪队列,重新获取锁资源,等待CPU分配资源去调度。
      • wait():进入WaitSet等待队列,只有被notify或者notifyAll唤醒时,才会进入就绪队列,重新获取锁资源,等待CPU分配资源去调度。
    • sleep不会释放锁资源,占用CPU资源,不让其它线程去使用。
  • 使用范围不同
    • wait、notify、notifyAll只能在同步方法或者同步代码块中使用
      • 原因:一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()或notifyAll()方法。为了调用wait()、notify()或者notifyAll()方法,线程必须先获得那个对象的锁。也就是说,线程必须在某个对象的同步块或者同步方法里调用wait()或者notify(),JVM是这么实现的,当你调用这三个方法的时候它首先要检查下当前线程是否是锁的拥有者,不是则抛出IllegalMonitorStateException
    • sleep可以在任何地方使用
  • 是否需要捕获异常
    • wait、notify、notifyAll不需要捕获异常
    • sleep必须捕获异常

sleep、yield、join的区别

  • sleep方法(线程睡眠):使线程进入指定毫秒数的停滞状态,指定时间内,该线程肯定不会被执行,相当于该程序进入不可运行状态,该方式不会释放锁,但该方法允许较低优先级别的线程获得运行机会。
  • yield方法(线程让步):使线程进入就绪状态,把运行机会给相同或优先级别更高的线程,但实际中该线程可再次被线程调度器选中。相当于让出线程的占有权并且释放锁,但让出的时间不能设定。
  • join方法(线程插入): 在线程a中调用线程b的join(),此时线程a进入阻塞状态并且释放锁,直到线程b完全执行完以后,线程a才结束阻塞状态。 (在那个线程中调用join就阻塞那个线程)

为什么wait、notify、notifyAll这些方法不在thread类里面

  • java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。由于wait、notify、notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

volatile的作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备类以下性质:

  • 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他 线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主内存中。

  • 禁止指令重排:当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行。(避免多线程情况下程序出现乱序的现象)

  • 不能保证原子性:多线程情况下会出现数据覆盖的问题。(原子性代表要么同时成功,要么同时失败,不能被另外一个线程直接切入,如果直接切入就会回滚)

    • 如何保证原子性呢,两种方式:

      • 用synchronized关键字来修饰方法,保证数据的原子性。

      • 使用并发包下的类

        private volatile static AtomicInteger num = new AtomicInteger();
        

volatile为什么可以保证可见性

  • 通过MESI缓存一致性协议和总线嗅探:根据JMM内存模型,所有的共享变量都存储在主内存中,每个线程还存在自己的工作内存(线程内部的局部变量和所需要的使用的共享变量的副本),每个线程操作数据会先从主内存中读取共享变量的数据到自己的工作内存,操作完会通过总线立刻刷新回主存,根据MESI缓存一致性,其他CPU也会通过总线嗅探,强制自己工作内存的数据失效,重新读取,嗅探机制会不断的占用总线带宽,导致总线流量激增,就会导致总线风暴

    • 线程解锁前:必须把volatile修饰的共享变量的值刷新回主内存(store存储)
    • 线程加锁前:必须读取主内存中volatile修饰的共享变量的最新值到自己的工作内存(load加载)
    • 加锁和解锁是同一把锁
  • JMM内存模型

    • 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类 型的变量来说,load、store、read和write操作在某些平台上允许例外)

      • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
      • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定
      • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便 随后的load动作使用
      • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
      • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
      • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的 变量副本中
      • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存 中,以便后续的write使用
      • write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内 存的变量中

在这里插入图片描述

Thread类的start方法与run方法有什么区别

  • 当调用start方法的时候会重新创建一个新的线程,并且运行run方法。
  • 当直接调用run方法的时候是在原来的线程中调用,没有新的线程启动。

Java中interrupted 和 isInterruptedd方法的区别

  • 当线程处于阻塞状态使用:interrupted() 会将中断状态清除。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。该方法执行后会抛出InterruptedException异常(在抛出异常之前,都会清除中断标志位,因此在抛出异常之后调用isInterrupted()方法将会返回false)然后通过try catch捕获异常通过break跳出循环,才能正常结束run方法。 (那个线程调用Thread.sleep(long mills)方法那个线程就休眠)
  • 当线程处于非阻塞状态时使用:isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识,isInterrupted()默认值为false

synchronized锁和lock锁的区别

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1 阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去(使用tryLock获取锁),如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平锁,而Lock锁可重入、可判断、公平和非公平锁(两者皆可)
    • synchronized不可中断,除非抛出异常或者正常运行完成
    • ReentrantLock可中断(通过调用tryLock超时方法或者lockInterruptibly方法)
      • tryLock 能获得锁就返回 true,不能就立即返回 false;tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false;如果锁不可用,不会导致当前线程被禁用,当前线程继续往下执行代码。
      • lock 能获得锁就返回 true,不能的话一直等待获得锁,当前线程并不继续向下执行。
      • lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
  • ReentrantLock锁可以绑定多个条件Condition对象,获取等待通知对象,该对象与当前锁绑定,该对象用来实现分组需要唤醒的线程,可以精确的唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  • 尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能

java多线程之间的通信方式

  • 采用synchronized锁:wait、notify、notifyAll (object对象唤醒是随机的)
  • 采用lock锁:await、signal、signalAll (可以通过condition对象,唤醒指定条件的线程)
  • Semaphore 的acquire方法、release方法

线程的生命周期

当线程启动以后,它不可能一致霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

  • 新建状态(NEW):当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
  • 就绪状态(Runnable):当线程对象调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
  • 运行状态(Running):如果处于就绪状态的线程获得CPU,开始执行run方法的线程执行体,则该线程处于运行状态
  • 阻塞状态(Blocking):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入就绪(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。
  • 死亡状态(DEAD):线程结束,或者程序结束,或者因异常退出了run方法

java的后台线程

java中两种线程:守护线程(后台线程) 非守护线程(用户线程)

  • 守护线程是专门给"所有的"用户线程(非守护线程)提供服务的线程,不是专门给一个线程提供服务
  • gc垃圾回收线程就是典型的守护线程,当JVM中没有任何的用户线程执行,守护线程就会自动的断开
  • 使用守护线程中产生的线程也是守护线程
  • 不要把I/O,file操作交给守护线程,因为它随时可能中断(没有用户线程执行的时候)
  • 对于线程池Executors,就算你把它设置为守护线程,也会变为非守护线程
  • 使用thread.setDaemon(true)(设置该线程为守护线程时,一定要放在启动线程之前(执行start方法之前),否则会出现IllegalThreadStateException异常,主线程结束时该线程会立即执行)
  • Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程(服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程)

java锁的分类

  • 可重入锁(也叫递归锁):在同一个线程中在外层方法获取锁的时候,在进入内层方法会自动获取锁
  • 公平锁与非公平锁:
    • 公平锁:指多个线程按照申请锁的顺序来获取锁
    • 非公平锁:指多个线程获取锁的顺序不是按照锁的申请顺序,当线程进来之后直接常识获取锁,如果获取不到自动到队尾等待(synchronized和ReentrantLock默认使用的是非公平锁,非公平锁的实际执行效率要远远超过公平锁)
  • 共享锁与独享锁:
    • 共享锁(也叫读锁):该锁可以被多个线程所持有
    • 独占锁(也加写锁):指该锁一次只能被一个线程所持有(对于synchronized和ReentrantLock都是独占锁)
    • 对于ReentrantReadWriteLock其读锁(readLock)是共享锁,写锁(writeLock)是独占锁,读锁的共享锁可保证并发读是非常高效的
      • 读-读 (可以共存)
      • 读-写 (不可以共存)
      • 写-写 (不可以共存)
  • 悲观锁与乐观锁
    • 悲观锁:认为写多读少,认为别人会修改数据,则每次读写的时候就会加锁,一律会对代码块进行加锁(对于synchronized和ReentrantLock都是悲观锁)
    • 乐观锁:认为读多写少,不会认为别人会修改数据,则每次读写的不会加锁,修改的时候才会去比较与预期值是否相同(在解决ABA问题时用到了AtomicStampedReference类的compareAndSet方法)
  • 分段锁:分段加锁是一种思想(在ConcurrentHashMap的JDK1.7版本使用分段锁,锁的是链表的结点)
  • 自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。(可以设置一个自旋等待时间,当超过这个时间后就会变为阻塞状态)
    • JDK1.6 中-XX:+UseSpinning 开启,-XX:PreBlockSpin=10 为自旋次数; JDK1.7 后,去掉此参数,由 jvm 控制
  • 死锁:死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去。如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否者就会因为争夺有限的资源而陷入死锁。
    • 产生死锁的必要条件:
      • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
      • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
      • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
      • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    • 活锁表示由于某些条件没有满足,导致一直在不停的改变状态(在尝试和失败之间来回切换),但是可以自行解锁。死锁代表一直在互相等待,没有外力干涉,不会向下推进。
    • 饥饿表示一个或多个线程无法获得CPU资源,导致一直无法执行的状态。
      • 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
      • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
      • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方 法),因为其他线程总是被持续地获得唤醒。
  • synchronized的四种锁状态(JDK1.6之后):无锁、偏向锁、轻量锁、重量锁

Semaphore信号量

Semaphore 是一种基于计数的信号量。它可以设定一个阈值,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。

Semaphore 可以用来构建一些对象池,资源池之类的, 比如数据库连接池(多个请求过来抢占连接资源),也可以实现互斥锁(设置两个阈值)

Semaphore主要用来抢占资源

public class SemaphoreDemo {
    public static void main(String[] args) {
// 模拟资源类,有3个空车位
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) { // 模拟6个车
            new Thread(()->{
                try {
                    // 一个线程调用acquire获得信号量减1,如果获取不到则会一直等待下去,直到有线程释放信号量,或者超时(用于实现多个资源的互斥作用)
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");
                            TimeUnit.SECONDS.sleep(3); // 停3秒钟
                    System.out.println(Thread.currentThread().getName()+" 离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //一个线程调用release释放这个位置信号量加1,然后唤醒等待的线程(用于实现并发线程的控制)
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}


执行结果:
1 抢到了车位
2 抢到了车位
3 抢到了车位
3 离开了车位
1 离开了车位
2 离开了车位
5 抢到了车位
6 抢到了车位
4 抢到了车位
6 离开了车位
5 离开了车位
4 离开了车位

Semaphore 与 ReentrantLock 区别

  • 锁的获取与释放:

    • Semaphore调用acquire()或者ReentrantLock调用.lockInterruptibly()默认可响应中断式锁,当线程中断时,会抛出InterruptedException异常

    • Semaphore通过acquire()与 release()方法来获得和释放临界资源

    • ReentrantLock通过lock()与unlock()方法来获取锁和释放锁

    • Semaphore和ReentrantLock都提供了公平锁和非公平锁

    • Semaphore的tryAcquire方法和ReentrantLock的tryLock方法的作用一致

    • Semaphore和ReentrantLock都需要手动释放锁,必须在finally代码块中完成

CountDownLatch计数器

CountDownLatch主要用来等待所有的线程执行完毕后(计数器归零),我在执行。

应用场景:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Start");
                // 计数器-1(调用CountDown的方法的线程不会阻塞)
                countDownLatch.countDown(); 
            },String.valueOf(i)).start();
        }
        //阻塞当前线程,等待计数器归零才会唤醒该线程,继续向下执行
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+" End");
    }
}

//执行结果
1 Start
2 Start
3 Start
4 Start
5 Start
6 Start
main End

CyclicBarrier

CyclicBarrier的主要作用是等待期待的数值达到之后,才会执行自己的CyclicBarrier中的方法(等待公司人员开会,人齐了之后才会展开会议)

CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier很有用。因为该 barrier在释放等待线程后可以重用,所以称它为循环的 barrier。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // CyclicBarrier(int parties, Runnable barrierAction)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙成功");
        });
        for (int i = 1; i <= 7; i++) {
            final int tempInt = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集了 第"+ tempInt +"颗龙珠");
                try {
                    cyclicBarrier.await(); // 使cyclicBarrier对象等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

执行结果:
Thread-0收集了 第1颗龙珠
Thread-1收集了 第2颗龙珠
Thread-2收集了 第3颗龙珠
Thread-3收集了 第4颗龙珠
Thread-4收集了 第5颗龙珠
Thread-5收集了 第6颗龙珠
Thread-6收集了 第7颗龙珠
召唤神龙成功

Thread线程的方法

  • sleep():强迫一个线程睡眠N毫秒。
  • isAlive(): 判断一个线程是否存活。
  • join(): 等待线程终止。
  • activeCount(): 程序中活跃的线程数。
  • enumerate(): 枚举程序中的线程。
  • currentThread(): 得到当前线程。
  • isDaemon(): 一个线程是否为守护线程。
  • setDaemon(): 设置一个线程为守护线程。 (用户线程和守护线程的区别在于,是否等待主线 程依赖于主线程结束而结束)
  • setName(): 为线程设置一个名称。
  • wait(): 强迫一个线程等待。
  • notify(): 通知一个线程继续运行。
  • setPriority(): 设置一个线程的优先级。
  • getPriority()::获得一个线程的优先级。

进程与线程

**进程:**程序分配资源的单位(一个进程中可以有一个线程或者多个线程)

**线程:**调度和执行的单位(线程所使用的资源为进程的资源)

**程序计数器:**是一个专用的寄存器, 用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

上下文切换

  • 挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处
  • 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
  • 跳转到程序计数器锁指向的位置(即跳转到进程被中断时的代码行),以恢复该进程

引起上下文切换的原因

  • 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务
  • 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务
  • 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
  • 用户代码挂起当前任务,让出 CPU 时间
  • 硬件中断

线程复用的原理

Thread类实现了Runnable接口重写了run方法,该方法底层代码中调用的Runnable接口的run方法

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  • 每一个 Thread 的类都有一个 start 方法。 当调用 start 启动一个新的线程时 Java 虚拟机会调用该类的 run 方法。
  • 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。
  • 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。 循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

阻塞队列

阻塞队列:

  • 在队列中没有数据的情况下,获取元素的所有线程将会被自动阻塞,直到其他线程往空的队列插入新的元素时,阻塞的线程会被自动唤醒。
  • 在队列中数据满的情况下,添加元素的所有线程将会被自动阻塞,直到其他线程移除队列中元素时,阻塞的线程会被自动唤醒。

阻塞队列的作用:

  • 我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一切都是BlockingQueue自动操作的

阻塞队列的主要方法:

  • 抛出异常(针对add和remove方法)
    • 当阻塞队列满时,再往队列里add插入元素会抛出IllegalStateException:Queue full异常
    • 当阻塞队列空时,再往队列里remove移除元素会抛出NosuchElementException异常
  • 特殊值(针对offer和poll方法)
    • 执行插入方法offer,成功返回true失败返回false
    • 执行移除方法poll,成功返回队列的元素,队列里没有就返回null
  • 一致阻塞(针对put和take方法)
    • 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put进数据或者线程响应中断退出
    • 当阻塞队列空时,消费者线程试图从队列里take元素,如果有则返回数据,队列会一直阻塞消费者线程直到队列可用
  • 超时退出(针对有时间限制offer和poll方法)
    • 当阻塞队列满时,队列会阻塞生产者线程一定的时间,超过限时后生产者线程会退出
    • 当阻塞队列空时,队列会阻塞消费者线程一定的时间,如果有则返回数据,否则超过限时后消费者线程会退出

阻塞队列的架构图:

在这里插入图片描述

ArrayBlockingQueue(公平、非公平)

  • 用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。 默认情况下是不公平的访问队列

  • 所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列

    ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
    

LinkedBlockingQueue(两个独立锁提高并发)

  • 基于链表的有界阻塞队列,同 ArrayBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。
  • 而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。LinkedBlockingQueue 会默认一个类似无限大小的容量 (Integer.MAX_VALUE)

PriorityBlockingQueue(当优先级相同是使用compareTo 排序实现优先)

  • 是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。
  • 可以实现Comparable接口来实现 compareTo()方法给指定元素进行自然排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数为Comparator接口的实例来对元素进行定制排序。需要注意的是不能保证同优先级元素的顺序。

DelayQueue(缓存失效时调用、定时任务调度 )

  • 是一个使用优先级队列实现的延迟无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:
    • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦从DelayQueue中获取元素,则表示缓存有效期到了
    • 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行(TimerQueue就是用DelayQueue实现的)

SynchronousQueue(不存储数据、可用于传递数据的场景)

  • 是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。(SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和 ArrayBlockingQueue)

LinkedTransferQueue(两个独立锁提高开发效率)

  • 是一个由链表结构组成的无界阻塞 TransferQueue 队列 。 相对于其他阻塞队列,LinkedTransferQueue 多了transfer和tryTransfer方法。
    • transfer方法:如果当前有消费者线程正在等待接收元素(当消费者使用take方法或者带时间限制的poll方法时),transfer方法可以直接把生产者传入的元素立刻传给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者线程调用了,transfer方法才会结束。
    • tryTransfer方法:则是用来测试生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则会返回 false;如果有消费者等待接收元素,则会返回true。 tryTransfer 方法无论消费者是否接收,方法会立即返回。而transfer方法是必须等到该元素被消费者消费了才会结束。
      • 对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。

LinkedBlockingDeque(两个独立锁提高开发效率)

  • 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
  • 在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在 “工作窃取”模式中。

阻塞队列的实现原理和使用场景

  • 实现原理:BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue 放入元素时,如果队列已满,则线程被阻塞;当消费者线程试图从中取出一个元素时,如果队列为空, 则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控制线程之间的通信。
  • 阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列(相当于序列化),然后解析线程不断从队列取数据解析(相当于反序列化)。

线程调度算法

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

两种调度模型:

  • **分时调度模型:**是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片
  • **抢占式调度模型:**是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。(最好不要使用,防止线程饥饿)

线程组

ThreadGroup 类,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。(线程组是为了方便线程的管理)

Java Concurrency API 中有哪些原子类(atomic classes)

1、原子操作指不可中断的一个或者一系列操作,在java中可以通过锁和循环CAS的方式来实现原子操作。

2、java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程进入,这只是一种逻辑上的理解。

  • 原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • 解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(通 过引入一个 int 来累加来反映中间有没有变过)

什么是并发容器和同步容器

**同步容器:**可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,一个线程拿上锁在运行,而其他的阻塞线程在等待锁释放,因此它们将会串行执行。(例如Vector、HashTable、Collections中的synchronizedSet、synchronizedList方法)

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

多线程同步和多线程互斥

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

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

线程间的同步方法可分为两类:

  • 内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态(内核模式下的方法:事件、信号量、互斥量)
  • 用户模式就是不需要切换到内核态,只在用户态完成操作(用户模式下的方法:原子操作、临界区)

java中怎么唤醒一个阻塞的线程

  • wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、notify 方法必须在synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值