Interview preparation -- java并发编程

创建线程
  • 继承Thread 重新run方法
  • 实现Runnable 重些run方法
  • 实现Callable 重新call方法,配合FutureTask
并发编程三大特点
原子性
  • 原子性指一个操作不可分割,不可中断,一个线程在执行时候,另外一个线程不会影响到他。
保证并发原子性
  • synchronized:在执行代码块或者方法上追加synchronized,

    • 会在代码指令执行开始之前增加一个monitorenter指令,获取到锁资源
    • 在代码指令执行完成后增加一个monitorexit指令,释放掉锁资源
  • CSA:比较替换,在修改之前先比较是否与预期一致,一致才修改

    • CAS是CPU的一个指令,在JAVA中没有具体实现,只能通过Unsafe类执行
    • 问题一:CAS不能解决ABA问题,例如需要比较的值是A,但是期间变量被修改了N次最后一次正好是A
    • 问题二:只能保证对一个变量的原子性,多行代码无法实现
  • Lock锁:ReenTrantLoc

  • ThreadLocal:空间换时间,

可见性
  • 可见性问题给予CPU功能出现的,CPU处理速度快,相对于CPU的指令执行速度来说,一次磁盘数据读取,或者一次祝内存数据读取的速度太慢,因此CPU提供了高速缓存技术,在CPU内部提供了寄存器,L1, L2,L3级缓存,每次去主内存获取数据,都不止是获取那一跳需要的数据,而是会获取一块数据,并且将数据存储到CPU的三级缓存中,每一级的读取速度与容量成反比,速度 L1 > L2 > L3, 容量L1<L2<L3。
    在这里插入图片描述
  • 以上CPU架构设计就代来了问题,在每个线程工作内存都是独立的,会告知,每个线程在修改时候都是只修改自己的工作内存(CPU三级缓存)独立执行。此时并没有同步到主内存,会导致工作内存与主内存不一致的问题
解决可见性问题
  • Volatile修饰的成员变量:

    • Volatile 内存语意:
    • Volatile被写入:当Volatile变量被修改,JMM会将当前线程对对应的CPU缓存及时的刷新到主内存,并且将其他高速缓存中此变量的值设置为无效,CPU会根据缓存一执行协议来对缓存中数据做同步,以此达到各个线程内存中数据的一致性
    • Volatile被读:当读一个Volatile,JMM会将对应的CPU缓存中的内存设置为无效,此时必须去主内存中重新读共享变量。
  • Volatile实现,加了Volatile的属性,在编译后会追加一个Lock前缀,CPU执行这个指令时候,如果带有Lock前缀,会做如下两件事:

    • 将当前处理器缓存行数据写回到主内存
    • 这个写回的数据,在其他CPU内核缓存中数据设置为无效。
  • Synchronized:获取到锁后,将内部涉及到的变量从CPU缓存中移除,会强制让当前线程去主内存获取一次数据,释放锁后,会立刻将线程内存中的数据刷新到主内存

  • Lock:

  • final:final修饰的属性,运行期间不能修改,类加载的时候在初始化阶段就已经决定了值。

有序性
  • 在java文件编译,转化为CPU指令,执行指令时候为了提升效率,在不影响最终结果前提下,会对指令重排序
  • 目的:尽可能发挥CPU性能
  • 使用Volatile:volatile修饰的属性不会出指令重排序问题,volatile会在两个操作之间增加内存屏障,这个内存屏障可以避免上下执行的其他指令进行重排序。
JMM存在的问题
  • JMM(Java Memory Model) 是一种规范,他定义Java程序各种内存访问行为规则。JMM定义规范主要为解决并发线程中线程安全问题,但是设计的时候页存在一些问题,比如上面说的并发编程三大特点也正是JMM设计规范所存在的问题。
    • 原子性:JMM中复合操作存在线程安全问题,例如++i,先取值,+1,在将新值赋给i,三个操作非线程安全
    • 可见性:线程A对属性s的操作后,线程B并不能立刻能看到,因为A修改的只是CPU高速缓存中的s对于的缓存行
    • 有序性:JMM无法保证代码有序执行,因为在变异成汇编后,CPU会在保证代码逻辑一致的前提先对指令进行冲排序,因此JMM无法保证有序性。
深入Synchronized
  • 类锁:如果使用同步方法,static修饰方法,此时使用的是当前类.class 作为锁
  • 对象锁:对于普通方法加的锁,使用的就是当前对象 this 作为锁。
Synchronized 锁优化
  • jdk1.6中对Synchronized做的优化
  • 锁消除:在synchronized修饰的代码中,不存在操作临界资源的情况,会触发锁消除,即写了synchronized也不会触发。极端情况,一个方法啥都没做,加锁了
  • 锁膨胀:如果一个循环中频繁做锁消除与释放,会带来很大消耗,锁膨胀就是将锁范围扩大,避免频繁锁竞争和获取锁资源带来的不必要消耗。
  • 锁升级:
    • 无锁,匿名偏向:当前对象刚创建的时候的锁状态是无锁或者匿名偏向
    • 偏向锁:如果当前锁资源只有一个线程频繁获取锁,只需要判断当前对象中锁指向对象是否当前线程即可:
      • 是同一个线程,直接获取
      • 不是,基于cas方式,尝试获取偏向锁指向当前线程,如果获取不到,触发锁升级到轻量级锁
    • 轻量级锁:会采用自旋CAS方式获取锁资源
      • 成功则拿走锁资源
      • 一定次数自旋不成功,锁升级
    • 重量级锁:就是传统synchronized,拿不到资源就挂起当前线程。
Synchronized实现原理

在这里插入图片描述

  • Synchronized的实现是基于对象实现的,在对象头mark word中存储了锁标识信息,并且有线程占用锁时候会存储线程ID,因此总流程如下
    • 当对象刚创建的时候,此时线程ID记录为空因此是匿名偏向锁或者说无锁状态
    • 当有线程过来,JVM会探测锁标识为是 01 并且线程ID记录为空,那边将当前线程ID记录在MarkWrod中,并且修改 偏向锁标识位为1
    • 当有其他线程来竞争尝试获取锁时候,JVM会先检测MarkWord中记录的线程ID是否为当前线程,如果是则获取锁成功,如果不是JVM会将锁升级为轻量级锁,修改锁标识位00,并且JVM尝试用CAS自旋的方式获取锁
    • 当CAS方式获取锁10次后没有获取到,会将锁升级为重量级锁,然后挂起线程(自旋锁默认的次数为 10 次可以通过 -XX:PreBlockSpin 来更改)

请添加图片描述

ReentrantLock 和Synchronized
  • 区别 ReentranLock基于AQS实现, synchronized基于ObjectMonitor实现
  • 如果竞争激烈,推荐用ReentrantLock,不存在锁升级,synchronized存在锁升级,但是不存在降级
  • ReentranLock功能更全:
    • ReentranLock支持公平锁,非公平锁实现
    • ReentranLock可以指定等待锁资源的事件
AQS概述
  • AQS是AbstractQueuedSynchronizer抽象类,AQS是JUC包下的一个基础类,很多并发功能都是通过AQS实现的,比如ReentranLock,ThreadPoolExecutor,阻塞队列,CounDownLatch,Semaphore,CyclicBarrier等都是基于AQS实现的
  • AQS内部关键属性如下下:
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
 	static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;        
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
       
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;

        volatile Node next;

        volatile Thread thread;
        Node nextWaiter;
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
  ......
  }
  • 关键state,用volatile修饰,采用CAS方式修改int类型的State变量
  • AQS中维护了一个双向链表,Head,tail首尾节,每个阶段都是Node对象
ReentratLock为例说明加解锁流程
  • 加锁流程
    请添加图片描述

  • 释放锁流程
    请添加图片描述

AQS常见问题
AQS中为什么要有一个虚拟的head节点
  • 因为在ReentrantLock的实现中,是通过AQS对节点操作来实现加锁与释放锁,在AQS链表节点中存在waitStatus状态,默认情况是0,如果当前节点后继节点挂起,则将当前节点标记为-1。这个-1的状态就是未来避免重复唤醒或者释放资源。
  • 当线程挂起等待后,会生成一个Node节点在AQS的双向队列中,挂起线程是通过释放锁或者释放资源的线程去唤起,因此他需要找到第一个需要被唤醒的线程,如果没有虚拟head节点的-1 设置,他只能从head开始查找,遍历整个双向队列
  • 因此第一个虚拟Head节点有两个作用
    • 第一用来标记后面有节点是挂起的以此来防止重复唤醒线程
    • 第二避免不必要的循环遍历操作(双向链表中所有节点都是移除状态,只有唤起节点和head节点,此时如果没有head标记需要循环整个链表,是不必要的)
读写锁实现原理
  • ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就执行逻辑,如果没有拿到还是到AQS的队列中排队
    • 读锁操作:基于state的高16位进行操作 00000000 00000001 00000000 00000000 ,有一个读
    • 写锁操作:基于state的低16位进行操作 00000000 00000000 00000000 00000001 ,有一个写
写锁重入
  • 读写锁中写锁的重入方法,基本和ReentrantLock一致,没有什么区别,依然是对status进行+1操作,只要确认持有锁资源的线程是当前线程即可,只不过之前ReentranLock冲入次数是整个state的整数部分,而读写锁是低16位
读锁重入
  • 读锁是共享锁,读锁在获取锁操作的时候,是对state的高16位进行+1操作,因为读锁是共享锁,所以同一个世界有多个读线程获取锁资源,多个读操作在持有读锁的时候,无法知道每个线程分别重入了几次,为了记录这一点,每个读操作线程都会有一个ThreadLocal 去记录本线程读锁重入的次数
写锁饥饿问题
  • 读锁是共享锁,当有线程持有读锁,再来一个线程需要获取读资源时候,直接对state修改即可。但是读资源获取到读锁后,来一个写线程,此时如果有大量的线程需要获取读锁而请求锁资源,如果可以绕过写直接拿资源就会造成写的无限期等待,因此解决问题还是排队哦
  • 读锁在拿到读资源后,如果再有读线程需要获取读锁资源,需要AQS排队,如果队列的前面有需要写锁资源的线程,那么后续读线程是无法拿到锁资源的,持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源。
阻塞队列
生产者消费者概念
  • 生产者消费者模式最合适的数据结构就是队列结构,让生产者消费者两者解耦,一个只想队列offer数据,一个只从队列pull数据
JUC阻塞队列存取基础
  • JUC中阻塞队列的基类是BlockingQueue,其中定义的方法如下

  • 生产者添加数据方法

 boolean add(E e); // 添加数据到队列,如果队列满,抛异常
boolean offer(E e); // 添加数据到队列,如果满了,返回false
  boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException; //同上,只是在队列满了后会阻塞timeout时间
 void put(E e) throws InterruptedException; //添加数据到队列,如果满了,挂起线程,等待有空位置在添加进入
  • 消费者获取数据方法
E remove(); //从队列中移除数据,如果为空,抛异常
 E poll();	//	从队列中移除数据,如果为空,返回null
 E poll(long timeout, TimeUnit unit) 
        throws InterruptedException; 		// 从队列冲移除数据,如果为空,等待timeout时间,等生产者添加,在获取
E take() throws InterruptedException;	//从队列中移除数据,如果队列为空,线程挂起,一直等到有生产数据到队列为止
JUC中队列
  • ArrayBlockingQueue 一个由数组结构组成的有界队列
  • LinkedBlockingQueue 一个由链表结构组成的有界队列
  • PriorityBlockingQueue 一个支持优先级排序的无界队列
  • DelayQueue 一个使用优先级队列实现的无界阻塞队列
  • SynchronizedQueue 一个不存储元素的阻塞队列
  • LinkedTransferQueue 一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列
PriorityBlockingQueue
  • PriorityBlockingQueue 是优先级队列,不满足FIFO,会将添加的数据进行排序,排序方式基于二叉堆实现,
  • 如果自定义对象必须实现Comparable接口才可以添加到优先级队列中
  • PriorityBlockingQueue是基于数组实现的二叉堆
  • 二叉堆结构特性
    • 二叉堆事一棵完整的二叉树
    • 任意一个节点都大于父节点(最小堆),或者小于父节点(最大堆)
    • 基于同步的方式,可以定义出小顶堆和大顶堆

请添加图片描述

线程池
  • 使用场景

    • 为了提升效率,将可以解偶的业务用一步的方式去执行,比如发短信,发邮件等
    • 讲一个比较大的任务,分阶段去执行,分别交给多个线程,之后在汇总。
  • 线程池作用:

    • 降低资源消耗:通过重复利用已经创建的线程降低线程创建销毁造成的消耗
    • 提高响应速度:当任务到达时候,任务无需等待线程创建直接从线程池获取立即执行
    • 提高线程的可管理性:管理线程进统一分配调度监控,做到合理运用
线程池核心参数
  • int corePoolSize : 核心线程数
  • int maximumPoolSize : 最大线程数
  • long keepAliveTime : 非核心线程线程池工作线程空闲后存活时间
  • TimeUnit unit : 非核心线程存活时间单位 (DAYS,HOURS等)
  • BlockingQueue workQueue : 任务队列,线程不够先放入队列
  • ThreadFactory threadFactory : 用于设置线程的工厂
  • RejectedExecutionHandler handler: 饱和策略
    • AbordPolicy:直接抛异常
    • CallerRunPolicy:只用调用者所在线程来运行任务
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
    • DiscardPolicy:不处理直接丢弃
    • Ploicy:自定义模式,可以将任务放数据库获取其他自定义操作
线程池工作流程

在这里插入图片描述

JDK自带线程池
  • JDK基于Executors提供了多种线程池
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • 固定最大线程,核心线程,适用于满足资源管理的需求,并且想需要限制当前线程数的情况,适用于负载比较重的服务
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • 最大线程,核心线程数都是1,任务投递过来只会有一个任务被处理,后续的都排队到阻塞队列中
  • 适用于需要保证任务顺序执行的场景,他能保证任意时间点都只有一个线程工作,天然具有线程安全属性
newCacheThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 核心线程0, 最大线程Integer.MAX_VALUE,只要提交任务就执行,默认空闲存活时间60s
  • 是一个无大小限制的线程池,因为使用的是SynchronousQueue 队列,一次只存储一个生产者,每次任务来都会重新生成一个新线程,适用于执行很多短期的异步任务,或者负载叫轻量级的服务器
  • 存在的风险点在于,如果需要执行的任务比较久,从而导致不断创建线程并且被占用导致OOM
newSchedulerThreadPool
 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
      return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
}
  • 定时任务类型的线程池有两种类,第一种保护若干线程,第二种只包含一个线程,都可以以一定周期去执行一个任务,或者延迟多久执行一次任务
newWorkStealingPool
 public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
  • JDK提供的newWorkStealingPool 与其他线程池有很大区别,其他事用ThreadPoolExecutor实现的,这个是用基于 ForkJoinPool构建出来的。
  • ThreadPoolExecutor核心在于只有一个阻塞队列存放当前任务
  • ForkJoinPool核心特点,每个线程都有一个自己的任务队列,当一个特别大的任务过来,ForkJoin会将大任务拆分成多个小任务,放到当前线程的阻塞队列中,当有其他空闲,那么可以去处理有任务的线程的阻塞队列中剩下的任务
  • 主要思想是工作窃取,分而治之,让每个看出的工作时间都被填满

请添加图片描述

ConcurrentHashMap
HashMap的线程安全问题
  • 在JDK1.7 中HashMap是线程不安全的,可能出现并发扩容的操作

  • JDK1.7中的HashMap在扩容数据迁移的时候,采用的是头插法,导致节点的next指针会有变化

  • 先迁移完的线程可能会导致其他线程在扩容时候,扩容到最后,将最开始的节点重新插入到头节点位置,导致指针位置变化,从而形成环形链表

  • 如下源码:

  • 首先线程一开始扩容并且已经完成指针指向,但是还没开始迁移数据
    请添加图片描述

  • 接着线程二,在线程一之前完成数据的迁移

  • 请添加图片描述

  • 此时可以看到,B节点的指针指向已经变了,由B — 〉 null 变成了 B —〉 A,这里就是问题所在

  • 接着A线程完成他自己的数据迁移,

请添加图片描述

  • 如上,在A 完成自己的数据迁移后,原本B —〉null的,因为上一步线程二的改动导致B指向A,因此指针线程找B.next找到的是A,他会将A重新压入头节点的位置

请添加图片描述

JDK1.7 中ConcurrentHashMap结构
  • ConcurrentHash 使用锁分段技术,将数据一段一段的存储,每一段数据单独配置一把锁。
  • 在ConcurrentHashMap中定义了一个内部类Segment,Segment是ReentrantLock的子类,是一个可重入锁
  • 一个ConcurrentHashMap中包含一个Segment数组,Segment结构和HashMap类似,是一种数组 + 链表的结构,从而形成了每个Segment守护着一个HashEntry数组里面的元素,当对某个Segment里面的某个HashEntry修改时候,需要先获取这个Segment对应的锁

在这里插入图片描述

JDK1.8中的ConcurrentHashMap
  • 在JDK1.7 中虽然用了锁分段,但是锁粒度还是维持在数组层,一个锁还是对应N多个数据,当数量增大,对于同一个Segment中的操作效率会变得很差。因此有了JDK1.8中的优化

  • JDK1.8 中的ConcurrentHashMap舍弃了Segment结构,还是用原来的数组 + 链表模式,利用CAS + synchronized的方式共同保证线程安全性

    • CAS:在put数据进入Map中时候,如果当前没有Hash冲突,那么直接通过CAS的方式放入数组第一位
    • synchronized:如果在put数据进Map时候,如果当前存在Hash 冲突,那么会用Synchronized锁住当前Hash位置的链表头节点,也就是在数组Hash位置上的数据
  • JDK1.8 中HashMap 与ConcurrentHashMap 其他优化

    • 因为数组的读取时间复杂度是O(1) ,链表的查询时间复杂度O(n),如果链表过长会导致查询效率低
    • JDK1.8中对HashMap的存储结构做了一定的优化,当链表长度 >= 8 并且 数组长度 >= 64 的时候,会将链表转成红黑树,红黑树的查询时间复杂度O(log2 n)

在这里插入图片描述

  • CAS与synchronized的源码如下
 final V putVal(K key, V value, boolean onlyIfAbsent) {
      ......
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //不存在冲突时候,用CAS方式给数组中赋值
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //存在Hash冲突,用synchronized锁住当前HashEntry的当前桶
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            //链表方式
                           .....
                        }
                        else if (f instanceof TreeBin) {
                        //红黑树模式
                        ......
            }
        }
        addCount(1L, binCount);
        return null;
    }
CopyOnWriteArrayList
  • CopyOnWriteArrayList是一个线程安全的ArrayList,通过ReentrantLock 来保证线程安全
  • 写数据时候,先获取lock锁,接着复制出一个副本数组,将数据插入到副本数组中,最后将副本数组赋值给CopyOnwriteArrayList中的Array
    • 因为每次添加数据都会有一个副本的额外内存消耗,如果写多并且数组数据量特别大,那边会导致内存占用巨大
    • 因为CopyOnwriteArrayList 是通过副本操作方式,因此在副本没落到array属性中时候,是无法查询到的,因此CopyOnwriteArrayList 是弱一致性的
JUC并发工具
CountDownLatch
  • CountDownLatch提供一个核心计数器功能,CountDownLatch接收一个int类型参数作为计数器,如果要等待N个点完成,就传入N

  • 当我们调用CountDownLatch 的countDown方法时候,N就会减少1,CountDownLatch 的await就会阻塞当前线程,直到N变为0。

  • CountDownLatch 内部类Sync继承了AQS, CountDownLatch 就是基于AQS实现的计数器。AQS就是一个state属性,以及AQS双向链表

  • 当线程A调用Await方法,如果state == 0 那么直接唤醒在AQS队列中阻塞的线程,如果state > 0 那么将当前线程封装成AQS中Node节点添加到AQS双向队中

CyclicBarrier
  • CyclicBarrier:可循环使用的屏障。要做的事情就是让一组线程到达一个屏障时候被阻塞,直到最后一个线程到达屏障点时候,屏障才会打开,所有白屏障拦截的线程会被唤醒
  • CyclicBarrier实现并没有基于AQS,是基于ReentrantLock 锁机制实现了对屏障点 – 的工作,以及挂起
  • 在调用await方法后,首先会获取一个ReentrantLock 锁,如果屏障点不归0 ,那么久用当前线程调用lock.newCondition().await方法阻塞住,如果屏障点归0 ,那么直接调用lock.newCondition().signalAll();释放所有阻塞线程。
  • CyclicBarrier中还有一个重要的 reset 方法,可以让计数器重置,他会释放已经在阻塞的线程,并且将计数器state置为0
Semaphore
  • Semaphore:信号量,用来控制同时访问特定资源的线程数量

  • Semaphore 初始化方法也是一个int类型,表示能同时访问的线程个数

  • Semaphore 的底层实现也是基于AQS,初始化AQS中的state=n,用来维护计数器,

  • 如果一个线程需要获取1个或者多个资源,直接查看state标识资源数是否足够,如果足够 则 -1,如果资源不足够,将当前线程挂起并且封装成一个AQS中的一个Node放入AQS的双向链表中,直到有线程释放资源才可以被唤醒

  • Semaphore 中有公平,非公平方式,在构造函数有一个fair的boolea值,

    • 公平Semaphore 体现在:当有线程释放资源时候,AQS会查看双向队列中释放有阻塞线程,如果有,去Head.next 第一个线程
    • 非公平方式会基于共享锁的方式去CAS请求state-1操作,那个线程执行成功则继续执行,如果资源获取失败,依然挂起线程等下下一次竞争
AQS中PROPAGSATE 节点
  • 在JDK1.8 中AQS中定义了一个PROPAGSATE = -3的常量
 static final int PROPAGATE = -3;
JDK1.5的BUG
  • 四个线程获取信号量资源情况,如下,当1,2,或者到资源,3,4 线程AQS阻塞,如下情况

请添加图片描述

  • 接着向下t1释放资源,然后t3获取资源后 t2在释放资源,此时会存在一个t4 线程挂起状态,因为执行点的问题,导致线程t4 是没有线程去唤醒他。

请添加图片描述

  • 此处有一个时间差,本了一个一个资源释放,应该是t1,唤醒t3,t2唤醒t4,但是在t3获取到资源但是没有释放的间隙,t3此时Node中线程不为null,因此t2 不会执行对后续节点的唤醒,导致有资源但是不能唤醒的bug
JDK1.8中变化

请添加图片描述

  • JDK1.8中增加了一个PROPAGSATE 状态,在t2节点释放后,会CAS给state+1,获取head节点,如果状态=0,那么此时不会直接改为-1,而是改为PROPAGSATE =-3。之后的判断中会通过节点状态-3 来对后续节点进行唤醒。
  • 解决办法就是增加了一个PROPAGSATE 状态的判断,释放要唤醒传播
热门面试题
@sun.misc.Contended注解的作用
  • 在ConcurrentHashMap中CounterCell方法就是被Contended修饰的一个方法
@sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }
  • JDK 1.8 才引入的一个注解,目的是为了解决伪共享问题(解决缓存行同步带来的性能问题)
  • 因为在CPU对象做写操作之前,会将主内存中的对象数据缓存到CPU高速缓存中(L1,L2,L3)
  • 并且CPU中高速缓存的最小存储但也是缓存行一个缓存行是64字节,当我们需要操作的变量K 与其他线程需要操作的变量A 同处于一个缓存行时候,其他线程对A进行修改,会导致缓存行失效那么我们在操作K的时候不得不再次同数据总线中同步数据造成无意义的一个操作
  • @Contended 的作用就是让我们的变量独占一个缓存行这样就排出了其他变量修改造成缓存行失效的影响
  • @Contended 将一个缓存行的后面N位置填充上7个没有意义的数据,以此方法来做到独占
CAS优点,缺点
  • CAS是compare adn swap,比较和交换,在Java中是基于unsafe类提供的对CAS的支持java会帮我们实现CAS的汇编指令。

  • 并发编程中游部分CAS的实现,例如AtomicLong等

  • CAS的缺点,CAS只能保证一个变量的原子性操作,无法对多行代码,或者带块实现原子性

  • CAS优点是不用加锁

  • CAS问题

    • ABA问题:当我们比较的一瞬间,已经有线程对对象做了修改,只是最后一次修改的值正好还原了初始值,这种情况CAS是无法判断出的,依然会修改,其实是不符合原子性操作的。 解决ABA问题可以用版本号的方式,Java中提供AtimicStampReference每次比较不仅仅判断原值,还会判断版本号
    • 自旋时间过长:我们可以指定CAS循环多少次,如果超过次数就挂起,或者直接失败,参考Synchronized中锁升级策略
ThreadLocal 内存泄漏问题
  • 首先Thread中有一个成员变量 ThreadLocalMap,他就是ThreadLocal里面的一个内部类

  • ThreadLocal的作用就是,当我线程要并发操作一个对象时候,我有几个线程就定义几个ThreadLocal,并且将要操作对象放入自己的ThreadLocal中,这样用空间换时间的做法,我们分别操作自己的对象即可

  • 而ThreadLocalMap是一个类似数组的实现,他的Key就是单前的ThreadLocal,而且是用 WeakReference 修饰的,是弱引用,弱引用对象的特点是只要GC就一定会被回收

  • 此处用弱引用是为了防止在ThreadLocal对象失去引用后如果引用是强引用会导致ThreadLocal对象无法被回收,如下图中,如果t1,t2 引用已经被回收,如果key的单曲的引用是强引用,那ThreadLocal1 与ThreadLocal2 还是引用可达的,不能被回收

  • 内存泄露原因:

    • 接着,因为是弱引用,GC后,key其实就已经被回收了,如果此时线程没有被回收,就会导致内存泄露,因为我无法通过key找到value值,因为线程对象的引用还在也就无法被回收,
    • 规避的方法,用完ThreadLocal后直接remove掉这个Entry即可

请添加图片描述

Java中锁分类
  • 公平锁, 非公平锁
    • 公平锁:Java中有RentrantLock, RentrantReadWriteLock 既可以公平也可以不公平,不公平的情况,如果A拿到锁,B尝试获取失败排队,C也过来,如果是公平,他会直接排队到B后面,等待B获取后在获取,如果是非公平那么C会先获取一次锁能获取到就插队成功,否则还是排队到B后面
    • 非公平锁:synchronized就是非公平锁,CAS谁拿到了锁偏向就是谁的锁持有者
  • 悲观锁,乐观锁:
    • 悲观锁:Synchronized,RentrantLock, RentrantReadWriteLock都是悲观锁,拿不到就挂起
    • 乐观锁:CAS就是乐观锁,我总认为下次能拿到因此一直尝试获取锁
  • 互斥锁,共享锁:
    • 互斥锁::只有一个线程可以持有,例如写锁,一个线程持有写锁,其他线程不管是读/写都需要等待
    • 共享锁:共享锁可以被多个线程一起持有,例如读锁可以多个线程一起读
  • 可重入锁,不可重入锁
    • 可重入锁:当前线程A获取到锁后,A继续想要获取锁,可以直接获取
    • 不可重入:即使A已经获取到锁,A想要重新再次获取,是无法获取到的,因为锁被占用即使占用锁的是当前线程A也需要等待这次释放后才能再次获取
Synchronized实现原理

在这里插入图片描述

  • Synchronized 的是现实基于对象头中的信息实现的的,对象头MarkWrod中存储了对象的基本状态比如分代年龄,锁状态,Hashcode等信息
  • 无锁状态:当我们对象刚创建的时候,此时是是无锁状态,只会在对象头中对锁类型进行标记,001 表示无锁
  • 偏向锁状态:当有线程来获取锁的时候,我们将锁标记改为偏向锁,并且将当前线程的指针存储对象头中
  • 轻量级锁:当有线程竞争锁时候,会通过CAS的方式修改线程指针,此时我们不仅仅要修改锁状态,还会会将线程指针修改为指向线程栈中的LockRecord
  • 重量级锁:修改锁状态,重量级锁的实现是通过C++实现的,C++中创建了一个ObjectMonitor,在内部定义了多个熟悉,比例enterList用来存储竞争锁资源的对象信息。
AQS唤醒节点时候为啥从后往前找
  • AQS在唤醒节点的时候,会先从后遍历,有两个原因
    • 在AQS添加需要排队获取锁对象到线程中时候,调用addWart方法,在添加节点到队列尾部的时候,会先将当前节点指向尾部节点,然后才会将tail节点指向单曲节点,此时 当前节点的pre节点的next并没有指向当前节点而是指向null,双向链表的结构还差一步才完成,因此如果CPU执行的时候,如果从头向后遍历可能会漏掉一个节点
      在这里插入图片描述

    • 同样在取消节点的过程中,我们通过当前要取消的节点找到往前找到第一个有效的节点 (waitStatus <=0, 1 是取消),也就是说,我们优先修改的还是 node.prev指针,因此,时效性最高的就是prev指针,因此我们会从后往前遍历的方式是

JUC提供的线程池
  • newFiledThreadPool。固定容量的,
  • newSingleThreadPool 单例的,只有一个线程,其他都排队
  • newCacheThreadPool 无限容量的每次来都立刻执行,schedulerQueue
  • newSchedulerThreadpool 定时任务线程池
  • newWorkerStrealingThreadPool 基于 forkJoinPool的工作窃取算法,每个线程都有自己的阻塞队列
线程池状态
  • 在ThreadPoolExecutor中核心属性ctl是AtomicInteger类型的,是一个32位的整形
  • 它存储了两部分信息,第一部分信息是存储的当前线程池的状态,第二部分信息存储的是当前线程的数量,ctl利用位分割的方式存储两部分数据在一个参数中。
  • ctl中高位的三位存储的线程池状态
  • ctl 后面29位存储的是单曲线程池的线程数量
  • 线程池状态总共有5中:
    • RUNNING:允许状态,可以正常接受新提交的任务,并且处理阻塞队列中任务
    • SHUTDOWN:不在接受新的任务,但是会执行完当前的任务以及线程池中的任务,执行shutdown方法后状态
    • STOP:立刻停止当前执行的线程,并且不会执行线程池中线程,shutdownNow 方法后状态
    • TIDYING:停止线程之前的一个过渡状态,代表线程池即将要关闭
    • TERMINATED:在TIDYING之后执行terminated方法后变为最终关闭状态,terminated 是一个空的方法,我们可以自己扩展实现做一些我们需要的操作。
添加工作线程的流程
  • 添加工作线程主要是在addWorker方法中,两个流程
    • 第一步,判断线程池状态 以及 线程池的中单前线程的个数
      • 首先通过ctl对象的高三位判断是否RUNNING状态,如果不是则返回false
      • 在判断ctl低29位是否已经到最大值,如果是,则返回false
      • 如果没有达到最大值,用CAS的方式给ctl值做+1操作
    • 第二部,将当前要执行的任务封装成一个worker对象并且添加到HashSet中,整个过程是通过ReentrantLock 锁来实现线程安全的。
线程池为啥要添加一个空任务的非核心线程池
  • 有两个原因要添加空任务非核心线程:
  • 第一个原因,当我们线程池启动时候核心线程参数设置的是0 此时,我们添加一个任务到队列,会发现并没有工作线程来执行,我们必须等队列填满才会开始新增工作线程,导致任务执行延后。因此为了避免这种情况,我们添加任务到队列之前会先判断是否有存活的工作线程,如果没有则添加一个空任务的非核心线程
  • 第二个原因,在线程池中有一个线程存活时间,是用来设置非核心线程的,但是有另外一个参数allowCoreThreadTimeOut设置可以让存活时间对核心线程也生效,当我们添加任务时候正好碰到线程都过期关闭了,也会造成有任务,没有执行线程的情况,因此同样添加之前先检查,没有存活线程则创建一个空任务非核心线程。
线程池执行完毕为何要执行shutdown
  • 利用线程池添加任务时候,addworker方法是利用worker中 的thread属性来启动线程,

    • 当我们启动时候,核心线程是一直会存活不会被GC回收
    • 因此持有这些核心线程的worker对象也一定是不会被GC回收的
    • 并且Worker对象是ThreadPoolExecutor的一个内部类,因此当前ThreadPoolExecutor也不能被回收,导致内存泄漏
  • shutdown 或者shutDownNow方法中

    • shutdown方法会先将当前线程池的状态改为shutdown,不在接受新的任务,并且同时将存活的并且没有任务的线程先关闭,然后等当前线程的任务执行完并且队列中任务执行完才会将剩下存活的线程关闭
    • shutdownnow 方法类似,他先将状态改为stop状态,这个更直接,他会将现有的存活的所有线程直接中断,并且阻塞队列中的任务直接丢弃
    • 因此等存活线程都关闭后,就能被GC回收,worker对象没有指向了,worker就能被回收,从而ThreadPoolExecutor就能被会输。
如何设置线程池参数
  • 因为在不同的项目中所要执行的任务是各个不同的,有IO密集型的,有CPU密集型,也有混合型任务,我们无法通过单一的某个方式来统一设置一个线程池的统一参数模式
  • 唯一的办法我们可以更具的我们具体的业务场景来进行压力测试,我们在测试环境设置好之后,用压测工具来对接口进行压测,并且不断的调整线程池的参数设置,比如核心线程,最大线程,线程饱和策略。
ConcurrentHashMap 1.8 中优化
  • 首先结构优化,当链表个数>8 数组个数> 64 的时候,会讲链表转红黑树,一个O(n)一个O(logn)
  • 其次,1.7 中concurrentHanshMap用的是锁分段技术,一个segment管理一个entry数组,但是锁力度还是不够细,导致高并发热点数据时候还是低性能,在1.8 中通CAS+ synchronized的方式如果没有冲突直接cas修改数组,否则通过synchronized的方式添加到链表
  • 还要就是计数器,ConcurrentHashMap中1.7 是用的tomicLong的方式通过CAS进形累加,如果并发的进形一个成功其他的都占用CPU资源,1.8 中利用了一个LongAdd的模式,会根据当前CPU情况来制定某几个区域来累加,比如多个CAS累加有一个成功其他的CAS累加会额外分出一块区域来存储累加数据,最后计算会有一个合并各个区域的过程。能提供更高效率同时减少CPU损耗;
ConcurrentHashMap 散列算法
  • 在ConcurrentHashMap中的hash算法并不直接获取key的hash值
    • 会先取key的hash值
    • 第一个特点高低位异或:接着让key的hash值的高16 位与低16位进行异或操作:这个做法的目的在于因为我们的数组一步都是16 ,32 等情况二进制是10000,导致只有低位的四位会参与取模操作,进高低位异或后,就能让高位地位同时参与计算,能让key散列的更平均
    • 第二特点数组长度必须2的n次方:同时在与数组长度进形取模的时候,会将数组长度n -1 得到的数在和hash值取余,因此我们要保证n必须是2 的倍数,还是未来尽可能的打散数据分布
      • 比如如果数组长度17 那么是10001 -1 = 10000 那么只有高位参与hash,如果是16,10000-1 = 01111 有四位参与Hash,还是尽可能打散数据分布。
    • 第三个特点,异或高低位后的数据和 HASH_BITS 取 与操作,为了保证hash后的值是正数,因为负数在ConcurrentHashMap中有特殊的定义 -1 扩容,-2红黑树 -3 索引预留
ConcurrentHashMap 读取数据的流程
  • 首先更具Key进行Hash计算得到hash值,然后先判断当前key对应的value是否在数组上
  • 如果key对应的value不为空,
    • 如果当前数组上entry节点的hash值与key的一样,则直接返回value,如果不一样我直接查询当前数组节点上的链表遍历,直到找到为止。
    • 如果当前数组上entry节点的hash值是特殊值:都会基于他的find方法去查。
      • 当是-1 的时候,表示正在扩容
      • 当是 -2 的时候,表示他是一颗红黑树
      • 当是-3 的时候,表示是预留位置
ConcurrentHashMap中的计数器
  • ConcurrentHashMap中计数器的作用是记录当前Map中存储的数据总数,有两个部分组成:
    • 计数器每次添加一个数据+1
    • 计数器监测当前是否需要扩容
  • 在1.7 的时候,我们计数器通常用AtomicLong来实现,他是一个基于CAS的线程安全的算法,但是在搞并发情况下,当前时刻只有一个能更新成功,其他的继续CAS直到success,这种情况下非常耗费CPU资源
  • 在1.8的时候引入了一个Contended修饰的CounterCall对象,
    • Contended 的作用是,让对象在操作系统内核高速缓存中单独占用一个缓存行来避免伪共享的情况
    • 也就是我们还是CAS,但是如果我当前的线程CAS没有成功,那么我们会单独添加到一个CounterCall数组中,这样的CounterCall会有多个,这样就分摊了CAS的对象,成功率更高。最后通过对CounterCall数组中数据累加得到最终结果。
wait,sleep 的区别
  • wait方法作用是,让等待获取锁的线程从获取锁队列中移动到等待锁队列,并且让出锁资源

  • notify会随机唤醒等待锁队列中的某个线程,加入到锁获取队列中,并且竞争资源

  • 所以wait,notify,notifyall,方法需要在synchronized 修饰的方法或者代码块中执行,

  • 区别:

    • sleep 是Thread中的静态方法,wait是Object中的方法
    • sleep执行后,线程处于WAIT_TIME状态,时间到了自动唤醒,Wait执行后属于WAITING状态,现需要手动换消息
    • sleep可以在持有锁,或者不持有锁的时候执行,wati方法必须在持有锁的时候执行。
多线程异步获取执行结果的多种方式
  • 第一种方式:
  • 通过Java8 中的parallelStream异步分发任务,parallelStream 与普通的异步线程池相比,在集合对象调用parallelStream 的时候,这个步骤是阻塞执行的,只有在单曲集合中所有的对象都执行完,才会继续执行下一行代码,因此我们可以用一个线程安全的集合去存储parallelStream 中处理的结果数据。
  • java 8 的parallelStream 是对集合中元素进行一个并行的map,而线程池则会使用jvm原生的forkjoinPool线程池提供的worker来完成多线程并行执行。并且forkjoinPool 中线程数与当前机器内核数一致。

在这里插入图片描述

  • 如上,当有N多个list集合同时有parallelStream来并行处理的时候,parallelStream会安list调用顺序阻塞执行,但是如果我们QPS达到一定上线,并且是一个IO密集型操作,会导致第一个LIst处理过程中所有线程都等待IO操作无法完成。导致其他list也阻塞,这个时候还不如用单线程处理。因此我们可以提升forkJoinPool的一个线程总数,直接通过
Djava.util.concurrent.ForkJoinPool.common.parallelism=20System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
  • 第二种方式:
  • future异步获取结果:future是Jdk1.5 就引入了支持异步线程提交之后跟踪手机对应结果的能力。
  • 通过实现Callable 重写call方法,并且配合FutureTask 来完成异步执行
  • 通过futureTask.get()的方式阻塞获取结果信息
  • 相比与parallelStream,我可以单独控制每一个线程的阻塞获取结果的一个时机
 //1. 创建MyCallable
        MyCallable myCallable = new MyCallable();
        //2. 创建FutureTask,传入Callable
        FutureTask futureTask = new FutureTask(myCallable);
        //3. 创建Thread线程
        Thread t1 = new Thread(futureTask);
        //4. 启动线程
        t1.start();
        //5. 做一些操作
        //6. 要结果
        Object count = futureTask.get();
  • 第三种:
  • ComplatableFuture 异步获取执行结果
  • 上面两种方式,每一个异步都是单独执行,如果我们有更负责场景,比如,异步线程之间的数据相互依赖,有组合关系,或者依赖其他线程的结果条件判断后异步执行。就需要用到jdk1.8新引入的ComplatableFuture
  • 第一种模式:线程A需要等B完成之后在执行,如下
 CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
            return "1";
        }).thenRunAsync(() -> {
            System.out.println("2线程执行");
        });
  • 第二种线程b等a执行完,拿a结果在执行
 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "1";
        }).thenApplyAsync(v -> v + "字符串拼接");
        System.out.println(future.get());
  • 第三种:线程c需要等a,b 执行完后,得到两个结果,在执行,一下代码种只有最后一个future13.get是阻塞的。
CompletableFuture<Integer> future11 = CompletableFuture.supplyAsync(() -> {
            return 1;
        });
        CompletableFuture<Integer> future12 = CompletableFuture.supplyAsync(() -> {
            return 2;
        });
        CompletableFuture<Integer> future13 = future11.thenCombineAsync(future12,(v1,v2) -> {
            return v1 + v2;
        });
        System.out.println(future13.get());
抽象类与接口
  • 抽象类:

    • 包含抽象方法的类一定是抽象类,但是抽象类不一定有抽象方法。
    • 抽象类中的抽象方法必须是public 或者 protected ,默认public
    • 抽象类中可以包括方法,属性,构造方法,但是构造方法不能用于实例化
  • 接口:

    • 接口可以包含变量,方法;变量都是public static final类型,jdk1.8 之前方法都是public abstract 类型
    • 一个类可以实现多个接口
    • jdk1.8 对接口增加新特性:
      • 默认方法 :jdk1.8允许接口有自己的非抽象方法实现,必须用default关键字修饰。定义了default 的方法可以不被子类实现。但是只能被实现的子类调用;
      • 静态方法:jdk1.8 中允许接口中定义静态方法,但是只能通过接口名直接调用
  • 区别:

    • 都不能被直接实例化
    • 接口的实现,或者抽象类的继承,都必须实现他们定义的抽象方法才能被实例化
  • 不同点:

    • 接口中方法有abstract ,default,static 方法,没有普通方法,抽象类中可以定义与实现基础的方法
    • 一个类可以实现多个接口,而一个类只能实现一个抽象类
    • 接口是自上而下的一个设计,抽象类是自下而上的一个设计
    • 接口中属性都是public static final,抽象类中成员变量默认是default,也可以自定义,可以被子类重定义。
控制线程顺序执行
  • 方法一,通过Thread.join方法,挂起当前调用join方法的线程知道被调用的线程执行完在只:

    • 例如 t2中调用 t1.join,那么t2 会挂起,等到t1执行完之后在继续执行
  • 方法二:通过CountDownLatch控制顺序执行

    • CountDownLatch允许一个或者多个线程一直等待直到指定数量的其他线程执行完后在执行。例如,T1,T2,T3,调用countDown方法,T4 调用await,那么T4要等T1,T2,T3执行完才会执行。(T1,T2, T3 是随机顺序的)
  • 方法三:通过newSingleThreadExecutor控制顺序

    • newSingleThreadExecutor是一个单线程的线程池,等待的任务放入队列中所有的任务只能排队执行,以此达到顺序控制目的。
HashMap构造函数传入初始大小为12
  • HashMap构造方法中会将初始值大小设置为2n 次方大小,如下算法:
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • 将cap的大小转成最近的2 的n次幂的值,12 变 16, 17 变32,变成比当前值大的一个最大的2的n次幂
ZipKin采样率
  • ZipKin提供了几种采样策略
    • CountingSample 计数采样:比如多少请求收集一个,例如30 收集一个
    • BoundarySampler概率采集,最小值是0.0001,最大1 ,0 表示不采样
    • RateLimitingSampler 限速采样,比如限制每一秒钟最多10个
  • 一般设置0.1 就可以,采样10%,但是并非只有10%的日志被保存,10%只是对zipkin有效,只是在zipkin界面显示0.1的日志而已,并不是后台日志也都是只收集0.1。
  • 而logback会把我们所有请求信息全部记录下来,不会遗漏,这样的话就可以顺利排错。
HashMap数组长度设置为2的N次幂
  • 有两个原因
  • 第一个,设置为2的N次幂的时候,hash % (length-1) 等价于 hash & (length-1) 操作,而位运算比取模操作的效率高,在重复N次情况下能体现出一个优化的价值
  • 第二个,设置2的N次幂 在做散列算法的时候,让hash的高 16 与低16 作异或同于和 (length-1)作& ,length - 1 就将100000 转成011111,这样参与散列算法的位数更多,能更好地分散元素在数组中的位置,减少冲突的概率。
深拷贝 & 浅拷贝
  • 浅拷贝:

    • 背复制对象的所有变量与原有变量的值相同,而所有对象对其他对象的引用还是指向原来的对象,即只对对象本身进行复制,而不会对引用对象进行复制
    • 简单说:浅拷贝只复制所考虑的对象,而不复制引用对象
  • 深拷贝:

    • 深拷贝是一个整个独立对象的拷贝,会拷贝所有属性,并且拷贝属性所指向的动态分配内存。当对象和他引用的对象一起拷贝时候,即深度拷贝,深度拷贝相对浅拷贝来说速度慢效率低
    • 简单说:深拷贝要把复制对于所引用的对象都复制一遍。
  • 深拷贝方式:

    • 构造方法:在构造方法中直接new 对于的引用对象
    • 重载Clone & 实现Cloneable:Object父类有Clone方法拷贝,重写并改为public,接着所有引用类都必须实现Cloneable接口来通知jvm这个类是可以拷贝的类
    • Apache Commons lang 序列化方式深度拷贝:org.apache.commons.lang3.SerializationUtils.clone方法拷贝
    • Jackson序列化方式:
主线程中捕获子线程中的异常
  • 主线程可以通过Thread类提供的方法来捕获子线程中的异常,如下:
    • 在创建子线程时候,为子线程设置一个UncaughtExceptionHandler(未捕获异常处理器),即在子线程中抛出未捕获的Exception的时候能得到通知
    • UncaughtExceptionHandler接口中只有一个方法uncauthtException方法,当线程抛出异常没捕获时候,会调用这个handler中实现的方法uncauthtException,我们可以在此处完成日志,邮件告警等操作
    • 如下代码
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 子线程执行的代码
                throw new RuntimeException("子线程发生异常");
            }
        });
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                // 在此处理子线程抛出的异常
                System.out.println("子线程抛出异常:" + e.getMessage());
            }
        });
        thread.start();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java的WebSocket缓存是指在WebSocket通信过程中,服务器和客户端之间可以缓存一些数据,以便在需要时进行快速访问和处理。通过使用WebSocket的缓存功能,可以提高通信的效率和性能。 在Java中,可以通过以下步骤来实现WebSocket缓存: 1. 配置WebSocket端点:在应用程序中创建一个新的Java类,并使用注解`@ServerEndpoint("/websocket")`来指定WebSocket服务端的端点。这将创建一个WebSocket端点,用于处理客户端的连接请求。 2. 编写WebSocket服务端代码:在WebSocket服务端代码中,可以使用`javax.websocket`包提供的API来处理WebSocket连接和消息。可以使用`@OnOpen`注解来处理客户端连接事件,使用`@OnMessage`注解来处理接收到的消息,使用`@OnClose`注解来处理客户端关闭连接事件。 3. 实现缓存逻辑:在WebSocket服务端代码中,可以使用Java的数据结构(如Map、List等)来实现缓存逻辑。可以将接收到的消息存储在缓存中,并在需要时从缓存获取数据进行处理。可以根据具体需求选择合适的数据结构和算法来实现缓存功能。 4. 发送和接收缓存数据:在WebSocket服务端代码中,可以使用`Session`对象来发送和接收消息。可以使用`session.getBasicRemote().sendText(message)`方法来发送消息,使用`session.getBasicRemote().sendObject(object)`方法来发送对象。可以使用`session.getBasicRemote().sendPong(ByteBuffer.wrap(data))`方法来发送Pong消息,使用`@OnMessage`注解来接收消息。 下面是一个简单的示例代码,演示了如何在Java中实现WebSocket缓存: ```java import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.util.HashMap; import java.util.Map; @ServerEndpoint("/websocket") public class WebSocketServer { private static Map<String, String> cache = new HashMap<>(); @OnOpen public void onOpen(Session session) { // 处理客户端连接事件 } @OnMessage public void onMessage(String message, Session session) { // 处理接收到的消息 cache.put(session.getId(), message); } @OnClose public void onClose(Session session) { // 处理客户端关闭连接事件 cache.remove(session.getId()); } } ``` 请注意,上述代码只是一个简单的示例,实际的WebSocket缓存实现可能需要更复杂的逻辑和数据结构。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值