• JUC并发编程学习总结


    1.实现多线程本质只有一种

    都是构造thread类。

    2.如何正确停止线程

    • 方法一:
      run()强制try/catch
    • 方法二:
      在catch语句块中调用thread.currentThread().interrupt()函数,再次中断

    3. 错误的停止方法

    stop(),suspend()和resume()方法被弃用

    4.wait/notify/notifyAll方法的使用注意事项

    • 为什么wait必须在synchronized保护的同步代码中使用?
      原因:
      在使用wait()方法时,必须把wait()方法写在synchronized保护while代码块中,并始终判断执行条件是否满足。
      如果满足就往下继续执行,如果不满足就执行wait()方法,而在执行wait()方法之前,必须持有对象的monitor锁,也就是通常所说的synchronized锁
      调用wait就是释放锁,释放锁的前提是必须先获得锁,先获得锁才能释放锁。

    • 为什么wait/notify/notifyAll被定义在Object类中,而sleep定义在Thread类中?
      原因:
      因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
      因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

    • wait/notify和sleep方法的异同?
      它们都可以让线程阻塞。
      它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
      不同点:
      wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
      在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
      sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
      wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
      以上就是关于 wait/notify 与 sleep 的异同点。

    5. 哪些场景需要额外注意线程安全问题?

    第一种场景:访问共享变量或共享资源时

    • 访问static静态变量,访问共享的缓存等等
      第二种场景
    • 依赖时序的操作(“检查与执行”并非原子性操作)
      第三种场景
    • 不同数据之间存着相互绑定关系的情况(ip和端口)
      第四种场景
    • 在我们使用其他类时,如果对方没有声明自己时线程安全的,那么这种情况下对其他类进行多线程的并发操作,就有可能回发生线程安全问题。(比如:arrayList线程不安全)

    6.为什么使用线程池比手动创建线程好在哪里?

    1. 反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么久有可能导致创建和销毁线程消耗的资源比线程只想任务本身消耗的资源还要大。
    2. 过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同事还会导致系统的不稳定。

    7.线程池有哪 4 种拒绝策略?

    线程池会在一下两种情况下会拒绝新提交的任务:

    第一种:我们调用shutdown等方法关闭线程池后,即使可能线程池内部依然有没执行完的任务正在只想,但是由于线程池已经关闭,此时如果再向线程池内提交任务,就会遭到拒绝。

    第二种:线程池没有能力继续处理新提交的任务,也就是工作任务非常饱和的时候。

    四种拒绝策略

    • CallerRunsPolicy 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
    • AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
    • DiscardPolicy - 直接丢弃新的,其他啥都没有
    • DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

    8.有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?

    • FixedThreadPool
    • CachedThreadPool
    • ScheduledThreadPool
    • SingleThreadExecutor
    • SingleThreadScheduledExecutor
    • ForkJoinPool

    FixedThreadPool

    第一种线程池叫作 FixedThreadPool,它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。

    CachedThreadPool

    第二种线程池是 CachedThreadPool,可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。

    ScheduledThreadPool

    第三个线程池是 ScheduledThreadPool,它支持定时或周期性执行任务。比如每隔 10 秒钟执行一次任务,而实现这种功能的方法主要有 3 种,如代码所示:

    ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
     
    service.schedule(new Task(), 10, TimeUnit.SECONDS);
     
    service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
     
    service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第一种方法 schedule 比较简单,表示延迟指定时间后执行一次任务,如果代码中设置参数为 10 秒,也就是 10 秒后执行一次任务后就结束。

    第二种方法 scheduleAtFixedRate 表示以固定的频率执行任务,它的第二个参数 initialDelay 表示第一次延时时间,第三个参数 period 表示周期,也就是第一次延时后每次延时多长时间执行一次任务。

    第三种方法 scheduleWithFixedDelay 与第二种方法类似,也是周期执行任务,区别在于对周期的定义,之前的 scheduleAtFixedRate 是以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,而不管任务需要花多久执行;而 scheduleWithFixedDelay 方法以任务结束的时间为下一次循环的时间起点开始计时。

    SingleThreadExecutor

    第四种线程池是 SingleThreadExecutor,它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。

    SingleThreadScheduledExecutor

    第五个线程池是 SingleThreadScheduledExecutor,它实际和第三种 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程,如源码所示:

    new ScheduledThreadPoolExecutor(1)
    
    • 1

    它只是将 ScheduledThreadPool 的核心线程数设置为了 1。
    img

    ForkJoinPool

    ForkJoinPool 非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。

    9. 线程池常用的阻塞队列有哪些?

    线程池中用到了以下几种阻塞队列:

    1. ArrayBlockingQueue(有界队列)
    2. LinkedBlockingQueue(没有容量限制的队列,无界队列)
    3. SynchronousQueue(不会存储的队列)
    4. PriorityBlockQueue()
    5. DelayedWorkQueue(延迟执行任务,每隔一定时间执行任务)

    10.为什么不应该自动创建线程池?

    1. FixedThreadPool对应LinkedBlockingQueue,如果线程处理速度比较慢,那么请求过多,大量的堆积任务回占用大量的内存,并发生OOM异常。
    2. SingleThreadPool对应LinkedBlockingQueue队列,也是造成任务堆积产生对应的OOM异常
    3. CachedThreadPool对应SynchronousQueue,会产生大量的线程,造成内存不足和资源的浪费
    4. ScheduledThreadPool和SingleThreadScheduledExecutor是一样的采用DelayedWorkQueue,如果队列种存放过多的任务,就可能导致OOM

    合适的线程数量是多少?CPU 核心数和线程数的关系?

    耗时IO型任务
    线程数=cpu核心数 * (1+平均等待时间/平均工作时间)
    cpu密集型任务
    线程数应该是cpu核心数的2倍以上

    如何正确关闭线程池?shutdown 和 shutdownNow 的区别?

    shutdown

    第一种方法叫作 shutdown(),它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。

    调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。

    isShutdown

    第二个方法叫作 isShutdown(),它可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 或者 shutdownNow 方法。

    这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。

    isTerminated

    第三种方法叫作 isTerminated(),这个方法可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了。

    比如我们上面提到的情况,如果此时已经调用了 shutdown 方法,但是还有任务没有执行完,那么此时调用 isShutdown 方法返回的是 true,而 isTerminated 方法则会返回 false。

    直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。

    awaitTermination

    第四个方法叫作 awaitTermination(),它本身并不是用来关闭线程池的,而是主要用来判断线程池状态的。

    比如我们给 awaitTermination 方法传入的参数是 10 秒,那么它就会陷入 10 秒钟的等待,直到发生以下三种情况之一:

    等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true 等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false 等待期间线程被中断,方法会抛出 InterruptedException 异常 调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle。

    shutdownNow

    最后一个方法是 shutdownNow(),它和 shutdown() 的区别就是多了一个 Now,表示立刻关闭的意思,不推荐使用这一种方式关闭线程池。

    在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。

    shutdown 和 shutdownNow 的区别?

    • shutdown 会等待线程池中的任务执行完成之后关闭线程池,而 shutdownNow 会给所有线程发送中断信号,中断任务执行,然后关闭线程池
    • shutdown 没有返回值,而 shutdownNow 会返回关闭前任务队列中未执行的任务集合(List)

    线程池实现“线程复用”的原理?

    源码

       public void execute(Runnable command) {
            if (command == null)
                throw new NullPointerException();
           /**
    分三步进行:     
        1. 如果少于 corePoolSize 线程正在运行,尝试
        以给定命令作为第一个启动一个新线程
        任务。 对 addWorker 的调用以原子方式检查 runState 和
        workerCount,因此可以防止会添加的错误警报
        在不应该的时候线程,通过返回 false。
              
        2.如果一个任务可以成功入队,那么我们还需要
        仔细检查我们是否应该添加一个线程
        (因为自上次检查以来现有的已死亡)或
        池在进入此方法后关闭。 所以我们
        重新检查状态,如果有必要则回滚入队
        停止,如果没有则启动一个新线程。
    
        3. 如果我们不能排队任务,那么我们尝试添加一个新的
        线。 如果它失败了,我们知道我们已经关闭或饱和
        所以拒绝任务。
    **/
            int c = ctl.get();
            if (workerCountOf(c) < corePoolSize) {
                if (addWorker(command, true))
                    return;
                c = ctl.get();
            }
           //可运行,且队列可放入
            if (isRunning(c) && workQueue.offer(command)) {
                int recheck = ctl.get();
                //不可运行状态,说明线程池被关闭,添加任务队列的任务执行拒绝策略
                if (! isRunning(recheck) && remove(command))
                    reject(command);
                else if (workerCountOf(recheck) == 0)
                    //是running状态,如果监测到线程数为0,新增线程
                    addWorker(null, false);
            }
           //队列已满,线程数已经是maxPoolsize,其他的还没有进入队列的执行拒绝策略
            else if (!addWorker(command, false))
                reject(command);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    不断的重复执行run方法来达到线程的复用

    final void runWorker(Worker w) {
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            w.firstTask = null;
            w.unlock(); // allow interrupts
            boolean completedAbruptly = true;
            try {
                while (task != null || (task = getTask()) != null) {
                    w.lock();
                    // If pool is stopping, ensure thread is interrupted;
                    // if not, ensure thread is not interrupted.  This
                    // requires a recheck in second case to deal with
                    // shutdownNow race while clearing interrupt
                    if ((runStateAtLeast(ctl.get(), STOP) ||
                         (Thread.interrupted() &&
                          runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                        wt.interrupt();
                    try {
                        beforeExecute(wt, task);
                        Throwable thrown = null;
                        try {
                            task.run();
                        } catch (RuntimeException x) {
                            thrown = x; throw x;
                        } catch (Error x) {
                            thrown = x; throw x;
                        } catch (Throwable x) {
                            thrown = x; throw new Error(x);
                        } finally {
                            afterExecute(task, thrown);
                        }
                    } finally {
                        task = null;
                        w.completedTasks++;
                        w.unlock();
                    }
                }
                completedAbruptly = false;
            } finally {
                processWorkerExit(w, completedAbruptly);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    你知道哪几种锁?分别有什么特点?

    在Java中锁的种类主要有这些:

    • 公平锁/非公平锁
    • 可重入锁
    • 独享锁/共享锁
    • 互斥锁/读写锁
    • 乐观锁/悲观锁
    • 分段锁
    • 偏向锁/轻量级锁/重量级锁
    • 自旋锁
    • 可中断锁

    偏向锁/轻量级锁/重量级锁
    这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。锁是在资源中的,是要访问资源(如对象实例,Class类实例,属性变量,代码块等)的一部分,线程是要取得资源中的锁。

    • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
    • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
    • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    可重入锁/非可重入锁
    它指的锁的一种种类

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。

    • 对于ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
    • 对于Synchronized而言,也是一个可重入锁。synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,即已经获取该资源的锁的线程,如果该线程再次访问该资源,系统会判定当前资源的锁已经被线程持有(实际是被该线程自己持有)无法获取该锁,需要阻塞等待,然后该线程就会进入阻塞状态,而锁本来就在它这,阻塞状态锁也释放不了,也获取不了,就进入到了死锁状态。可重入锁的一个好处是可一定程度避免死锁,可重入锁可以使持有锁的线程重复访问资源,不会发生死锁。

    独享锁/共享锁
    它们指的锁的两种种类

    独享锁是指该锁一次只能被一个线程所持有。

    共享锁是指该锁可被多个线程所持有。

    • 对于ReentrantLock而言,其是独享锁。
    • 对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
    • 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的(操作系统中的读者-写者问题)。
    • 对于Synchronized而言,当然是独享锁。

    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

    公平锁/非公平锁
    它们指的锁的两种种类

    公平锁是指多个线程按照申请锁的顺序来获取锁。

    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。(非公平锁就是抢占式的)

    • 对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
    • 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

    乐观锁/悲观锁
    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

    悲观锁认为对于同一个数据的并发操作,一定是会发生修改的(或者增删改多,查少),哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

    乐观锁则认为对于同一个数据的并发操作,是不会发生修改的(或者增删改少,查多)。在更新数据的时候,会采用不断尝试更新的方式来更新数据。也就是先不管资源有没有被别的线程占用,直接取申请操作,如果没有产生冲突,那就操作成功,如果产生冲突,有其他线程已经在使用了,那么就不断地轮询。乐观的认为,不加锁的并发操作是没有事情的。就是通过设置多个版本,如果修改完之后发现有冲突再将版本返回到没修改的样子,乐观锁就是不加锁。

    从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

    • 悲观锁在Java中的使用,就是利用各种锁。
    • 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
  • 相关阅读:
    系统调用理论详解,Linux操作系统原理与应用
    缓存案例-架构真题(二十二)
    Nginx.conf设置nginx优化(二)
    竞赛 基于深度学习的动物识别 - 卷积神经网络 机器视觉 图像识别
    CSS 布局
    PyCharm - Project Interpreter (项目解释器)
    代码随想录刷题|买卖股票问题的总结
    Excel中怎么求排名
    马克思的手稿-第11届蓝桥杯Scratch选拔赛真题精选
    Android ViewPager2 + Fragment 联动
  • 原文地址:https://blog.csdn.net/weixin_43878732/article/details/126261397