一、什么是队列，队列有什么用？假设春运火车站只有一个售票窗口，但是买火车票的人非常多，如果大家一哄而上到窗口进行买票，估计售票员会发疯，因此聪明的人类发明了排队这种游戏规则，则先到先得的机制，在不断的发展过程中，排队的规则也慢慢完善，比如有些例外比如军人优先或者老弱病残优先则可以进行插队，通过这样的规则可以让面对人流高峰也可以平稳进行工作。这个规则运用到实际开发中也是如此，在程序中队列是一种特殊的线性表，遵循先入先出、后入后出的基本原则，一般来说，它只允许在队列的前端进行删除操作，而在队列的后端进行插入操作。但是java的某些队列运行在任何地方插入删除（如军人插队）在java中根据队列特性，队列可分为阻塞队列和非阻塞队列，有界队列和无界队列、单向链表队列和双向链表队列之分,二、阻塞/非阻塞队列2.1 阻塞队列入列(添加元素)时，如果元素数量超过队列总数，会进行等待（阻塞），待队列的中的元素出列后，元素数量未超过队列总数时，就会解除阻塞状态，进而可以继续入列；出列(删除元素)时，如果队列为空的情况下，也会进行等待（阻塞），待队列有值的时候即会解除阻塞状态，进而继续出列；阻塞队列优点 ：阻塞队列的好处是可以防止队列容器溢出；只要满了就会进行阻塞等待；也就不存在溢出的情况,只要是阻塞队列，都是线程安全的；实现类DelayQueue：一个支持延时获取元素的无界阻塞队列LinkedTransferQueue：一个由链表结构组成的无界阻塞队列。ArrayBlockingQueue：有界队列，阻塞式,初始化时必须指定队列大小，且不可改变；，底层由数组实现；SynchronousQueue：最多只能存储一个元素，每一个put操作必须等待一个take操作，否则不能继续添加元素PriorityBlockingQueue：一个带优先级的队列，而不是先进先出队列。元素按优先级顺序被移除，而且它也是无界的，也就是没有容量上限，虽然此队列逻辑上是无界的，但是由于资源被耗尽，所以试图执行添加操作可能会导致 OutOfMemoryError 错误；2.2 非阻塞队列不管出列还是入列，都不会进行阻塞，入列时，如果元素数量超过队列总数，则会抛出异常，出列时，如果队列为空，则取出空值；一般情况下，非阻塞式队列使用的比较少，一般都用阻塞式的对象比较多；阻塞和非阻塞队列在使用上的最大区别就是阻塞队列提供了以下2个方法：出队阻塞方法 ： take()入队阻塞方法 ： put()实现类ConcurrentLinkedQueue：单向链表结构的无界并发队列, 非阻塞队列，由CAS实现线程安全，内部基于节点实现ConcurrentLinkedDeque：双向链表结构的无界并发队列, 非阻塞队列，由CAS实现线程安全PriorityQueue: 内部基于数组实现，线程不安全的队列三、有界/无界队列有界和无界有界：有界限，大小长度受限制无界：无限大小，其实说是无限大小，其实是有界限的，只不过超过界限时就会进行扩容，就想ArrayList 一样，在内部动态扩容，最大值为Integer.MAX_VALUE四、单向/双向链表队列4.1 单向链表每个元素中除了元素本身之外，还存储一个指针，这个指针指向下一个元素；4.2 双向链表除了元素本身之外，还有两个指针，一个指针指向前一个元素的地址，另一个指针指向后一个元素的地址；四、 Java中队列的继承关系五、队列常用方法队列常用方法函数名函数介绍特殊说明add增加一个元索如果队列已满，则抛出一个IIIegaISlabEepeplian异常remove移除并返回队列头部的元素如果队列为空，则抛出一个NoSuchElementException异常element返回队列头部的元素如果队列为空，则抛出一个NoSuchElementException异常offer添加一个元素并返回true如果队列已满，则返回falsepoll移除并返问队列头部的元素如果队列为空，则返回nullpeek返回队列头部的元素如果队列为空，则返回nullput添加一个元素如果队列满，则阻塞take移除并返回队列头部的元素如果队列为空，则阻塞drainTo一次性取出队列所有元素)-PS知识点： remove、element、offer 、poll、peek 属于Queue接口的定义。六、代码示例假设现在需要售卖一趟车票共100张，并且有3个窗口开放售票，当有大量人购买时如何保障出票安全，我们使用ArrayBlockingQueue阻塞式有界队列来模拟常见代码。package com.queue; import cn.hutool.core.util.StrUtil; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class BlockingQueueTest { //售票员窗口 static final int SALESPERSON=3; //总票数100张 static AtomicInteger ticketTotal=new AtomicInteger(100); //假设大厅总排队人数最大可排1000人 static final int MAX_COUNT=1000; public static void main(String[] args) { // 阻塞-有界队列 BlockingQueue<String> queue=new ArrayBlockingQueue<String>(MAX_COUNT); for (int i = 0; i < SALESPERSON; i++) { //模拟启动三个工作窗口工作排队 new Thread(new Producer(queue)).start(); //模拟启动三个工作窗口出票 new Thread(new Consumer(queue)).start(); } } /** * 模拟系统排队出票 */ static class Consumer extends Thread{ BlockingQueue<String> queue; public Consumer(BlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (true){ try { if(ticketTotal.get()==0){ System.out.println("结束售票啦，收工下班"); return; } System.out.println(StrUtil.format("{},出票号：{}，大厅排队总人数：{}",queue.take(),ticketTotal.getAndDecrement(),queue.size())); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 模拟窗口并发出票发起请求 */ static class Producer implements Runnable{ BlockingQueue<String> queue; public Producer(BlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (true){ try { queue.put(StrUtil.format("出票窗口{}号,购票请求：{}",Thread.currentThread().getId(),ticketTotal.get())); } catch (InterruptedException e) { e.printStackTrace(); } } } } } 通过上述代码运行，我看可以看出，当26，23，20同时请求同一个100票号时，系统也可以再多线程下正常出票，有效解决并发和出错票等问题出票窗口26号,购票请求：100,出票号：100，大厅排队总人数：7 出票窗口23号,购票请求：100,出票号：98，大厅排队总人数：6 出票窗口23号,购票请求：100,出票号：97，大厅排队总人数：217 出票窗口20号,购票请求：100,出票号：99，大厅排队总人数：8 出票窗口23号,购票请求：100,出票号：96，大厅排队总人数：226 出票窗口23号,购票请求：100,出票号：94，大厅排队总人数：237 出票窗口20号,购票请求：100,出票号：95，大厅排队总人数：227 出票窗口26号,购票请求：100,出票号：92，大厅排队总人数：264 出票窗口20号,购票请求：100,出票号：93，大厅排队总人数：246 出票窗口20号,购票请求：100,出票号：90，大厅排队总人数：274 出票窗口23号,购票请求：100,出票号：91，大厅排队总人数：267 出票窗口20号,购票请求：97,出票号：88，大厅排队总人数：292 出票窗口20号,购票请求：97,出票号：89，大厅排队总人数：284 出票窗口20号,购票请求：97,出票号：86，大厅排队总人数：327 出票窗口26号,购票请求：98,出票号：87，大厅排队总人数：298 出票窗口23号,购票请求：97,出票号：85，大厅排队总人数：341 出票窗口20号,购票请求：97,出票号：83，大厅排队总人数：339 出票窗口20号,购票请求：97,出票号：84，大厅排队总人数：340 出票窗口26号,购票请求：97,出票号：81，大厅排队总人数：352 出票窗口23号,购票请求：97,出票号：82，大厅排队总人数：345 出票窗口23号,购票请求：97,出票号：79，大厅排队总人数：362 出票窗口20号,购票请求：97,出票号：80，大厅排队总人数：360 出票窗口20号,购票请求：97,出票号：77，大厅排队总人数：366 出票窗口26号,购票请求：97,出票号：78，大厅排队总人数：366 出票窗口26号,购票请求：97,出票号：75，大厅排队总人数：380 出票窗口20号,购票请求：97,出票号：76，大厅排队总人数：370 出票窗口20号,购票请求：97,出票号：73，大厅排队总人数：403 出票窗口23号,购票请求：97,出票号：74，大厅排队总人数：391 出票窗口20号,购票请求：97,出票号：72，大厅排队总人数：408 出票窗口23号,购票请求：97,出票号：70，大厅排队总人数：406 出票窗口26号,购票请求：97,出票号：71，大厅排队总人数：407 出票窗口20号,购票请求：97,出票号：69，大厅排队总人数：420 出票窗口20号,购票请求：97,出票号：68，大厅排队总人数：422 出票窗口20号,购票请求：97,出票号：66，大厅排队总人数：433 出票窗口26号,购票请求：97,出票号：65，大厅排队总人数：439 出票窗口23号,购票请求：97,出票号：67，大厅排队总人数：429 出票窗口23号,购票请求：97,出票号：63，大厅排队总人数：442 出票窗口20号,购票请求：97,出票号：64，大厅排队总人数：442 出票窗口26号,购票请求：97,出票号：61，大厅排队总人数：457 出票窗口20号,购票请求：97,出票号：62，大厅排队总人数：447 出票窗口23号,购票请求：97,出票号：59，大厅排队总人数：470 出票窗口20号,购票请求：97,出票号：60，大厅排队总人数：469 出票窗口26号,购票请求：97,出票号：57，大厅排队总人数：489 出票窗口20号,购票请求：97,出票号：58，大厅排队总人数：481 出票窗口23号,购票请求：97,出票号：55，大厅排队总人数：509 出票窗口20号,购票请求：97,出票号：56，大厅排队总人数：494 出票窗口23号,购票请求：97,出票号：53，大厅排队总人数：541 出票窗口26号,购票请求：97,出票号：52，大厅排队总人数：547 出票窗口26号,购票请求：97,出票号：54，大厅排队总人数：522 出票窗口26号,购票请求：97,出票号：50，大厅排队总人数：568 出票窗口23号,购票请求：97,出票号：51，大厅排队总人数：557 出票窗口23号,购票请求：97,出票号：49，大厅排队总人数：587 出票窗口23号,购票请求：97,出票号：47，大厅排队总人数：602 出票窗口26号,购票请求：97,出票号：48，大厅排队总人数：596 出票窗口23号,购票请求：97,出票号：45，大厅排队总人数：631 出票窗口26号,购票请求：97,出票号：46，大厅排队总人数：613 出票窗口23号,购票请求：97,出票号：43，大厅排队总人数：656 出票窗口26号,购票请求：97,出票号：44，大厅排队总人数：649 出票窗口23号,购票请求：97,出票号：41，大厅排队总人数：705 出票窗口26号,购票请求：97,出票号：42，大厅排队总人数：679 出票窗口23号,购票请求：97,出票号：39，大厅排队总人数：725 出票窗口26号,购票请求：97,出票号：40，大厅排队总人数：716 出票窗口26号,购票请求：97,出票号：37，大厅排队总人数：756 出票窗口26号,购票请求：97,出票号：38，大厅排队总人数：740 出票窗口26号,购票请求：97,出票号：35，大厅排队总人数：797 出票窗口23号,购票请求：97,出票号：36，大厅排队总人数：771 出票窗口23号,购票请求：97,出票号：33，大厅排队总人数：850 出票窗口26号,购票请求：97,出票号：34，大厅排队总人数：832 出票窗口26号,购票请求：97,出票号：31，大厅排队总人数：887 出票窗口26号,购票请求：97,出票号：32，大厅排队总人数：867 出票窗口26号,购票请求：97,出票号：29，大厅排队总人数：913 出票窗口23号,购票请求：97,出票号：30，大厅排队总人数：898 出票窗口23号,购票请求：97,出票号：27，大厅排队总人数：939 出票窗口26号,购票请求：97,出票号：28，大厅排队总人数：924 出票窗口26号,购票请求：97,出票号：26，大厅排队总人数：953 出票窗口26号,购票请求：97,出票号：25，大厅排队总人数：953 出票窗口26号,购票请求：97,出票号：22，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：23，大厅排队总人数：998 出票窗口23号,购票请求：97,出票号：24，大厅排队总人数：972 出票窗口23号,购票请求：97,出票号：20，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：21，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：18，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：19，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：16，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：17，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：14，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：15，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：12，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：13，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：10，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：9，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：11，大厅排队总人数：999 出票窗口26号,购票请求：97,出票号：7，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：8，大厅排队总人数：1000 出票窗口26号,购票请求：97,出票号：5，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：6，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：3，大厅排队总人数：999 出票窗口23号,购票请求：97,出票号：4，大厅排队总人数：999 结束售票啦，收工下班 出票窗口23号,购票请求：97,出票号：1，大厅排队总人数：999 结束售票啦，收工下班 出票窗口26号,购票请求：97,出票号：2，大厅排队总人数：999 结束售票啦，收工下班
Java中基本数据类型与对应的包装类型介绍
目录目录一. Java基本类型与包装类型介绍二 类型的自动装箱与拆箱举例说明一. Java基本类型与包装类型介绍Java中有8种基本数据类型，分别为：数字类型(6种) ：byte、short、int、long、float、double字符类型(1种) ：char布尔型(1种) ：boolean。这八种基本类型都有对应的包装类分别为：Byte、Short、Integer、Long、Float、DoubleCharacterBoolean基本类型位数字节默认值int3240short1620long6480Lbyte810char162'u0000'float3240fdouble6480dboolean1官方文档未明确定义，它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1位，但是实际中会考虑计算机高效存储因素。false二 类型的自动装箱与拆箱装箱与拆箱的含义：装箱：将基本类型用它们对应的引用类型包装起来；拆箱：将包装类型转换为基本数据类型；Java基本类型的包装类的大部分实现了常量池技术，即Byte,Short,Integer,Long,Character,Boolean；前面4种包装类默认创建了数值[-128，127]的相应类型的缓存数据，Character创建了数值在[0,127]范围的缓存数据，Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。其中仅浮点数类型的包装类Float与Double 没有实现常量池技术。Boolean常量池public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); // 其中TRUE与FALSE为定义的静态常量 }CharacterCache 缓存常量池private static class CharacterCache { private CharacterCache(){} static final Character cache[] = new Character[127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i); } }/** *此方法将始终缓存-128 到 127（包括端点）范围内的值，并可以缓存此范围之外的其他值。 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) //如果在缓存范围值内 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); // 不如不在范围内者使用new运算符创建一个堆内存返回新的对象 }举例说明例子1 Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 输出 true, 因为33在-128-127范围内使用的是缓存常量，即两个数值的内存地址一致 Integer i11 = 333; Integer i22 = 333; System.out.println(i11 == i22);// 输出 false // 超出范围，返回的都是new的新对象 Double i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 输出 false // Double未实现缓存常量池 例子2需要注意一旦使用new运算符创建对象，则将创建新的数据，不会使用缓存常量值，如以下Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);//输出 falseInteger i1=40；Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);，从而使用常量池中的对象。Integer i1 = new Integer(40);这种情况下会创建新的对象。例子3Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println("i1=i2 : " + (i1 == i2)); // true,两个变量使用的是同一个常量值 System.out.println("i1=i2+i3 : " + (i1 == i2 + i3)); // true, 因为"+"加号运算符不能使用在Integer上，所以会自动拆箱为基础类型int：40+0=40, 同时"="等号也无法使用在Integer上，则最终比较为40=40 System.out.println("i1=i4 : " + (i1 == i4));// false System.out.println("i4=i5 : " + (i4 == i5)); // false System.out.println("i4=i5+i6 : " + (i4 == i5 + i6)); //true,加法运算发自动拆箱后进行加法运行并进行比较 System.out.println("40=i5+i6 : " + (40 == i5 + i6));// true,加法运算发自动拆箱后进行加法运行并进行比较
目录目录两个等于号 ==equals()比较两个等于号 ==它的作用是判断两个对象的内存地址是不是相等，即判断两个对象是不是同一个对象。基本数据类型比较的是值，引用数据类型==比较的是内存地址equals()比较它的作用也是判断两个对象是否相等，它不能用于比较基本数据类型的变量。 equals()方法存在于 Object 类中，而 Object 类是所有类的直接或间接父类。Object 类 equals() 方法：public boolean equals(Object obj) { return (this == obj); }equals() 方法存在两种使用情况：情况 1：类没有覆盖 equals() 方法, 则通过 equals() 比较该类的两个对象时，等价于通过“==”比较这两个对象。使用的默认是 Object 类 equals() 方法。情况 2：类覆盖 equals() 方法。一般覆盖 equals() 方法用来比对两个对象的 内容 相等；若它们的内容相等，则返回 true(即认为这两个对象相等),比如字符串的String类比较则是覆盖重写equals方法。实例代码：public class test1 { public static void main(String[] args) { String str1 = new String("test"); // a 为一个引用 String str2 = new String("test"); // b为另一个引用,对象的内容一样 String str3 = "test"; // 放在常量池中 String str4 = "test"; // 从常量池中查找 if (str3 == str3) // true System.out.println("aa==bb"); if (str1 == str2) // false，非同一对象 System.out.println("a==b"); if (str1.equals(str2)) // true System.out.println("equals比较内容相等"); if (42 == 42.0) { // true System.out.println("true"); } } }特别说明String 中的 equals 方法是被重写过的，因为 Object 的 equals 方法是比较的对象的内存地址，而 String 的 equals 方法比较的是对象的值。当创建 String 类型的对象时，虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象，如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。String 类 equals()的重写方法：public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
文章目录索引文章目录索引ThreadLocal定义1、如何使用2.原理3.使用场景4.避免踩坑ThreadLocal定义该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物，因为访问某个变量（通过其 get 或 set 方法）的每个线程都有自己的局部变量，它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段，它们希望将状态与某一个线程（例如，用户 ID 或事务 ID）相关联。以上来源官方API。大概可以总结为两点：ThreadLocal提供get/set方法，可以访问属于当前线程的变量，也就是可以保证每个线程的变量不一样。ThreadLocal使用时通常定义为private static的。从字面意思理解，可能会将ThreadLocal认为是本地线程，其实ThreadLocal并不是线程，而是线程Thread的局部变量。1、如何使用首先，定义一个private static的ThreadLocal对象。private static ThreadLocal<String> threadLocal = new ThreadLocal<>();然后每个线程可以将当前线程需要存放在局部变量中，并且可以从中获取。public void setAndGet(String name) { threadLocal.set(name); String s = threadLocal.get(); }最后在使用完之后，需要将ThreadLocal中的值移除。public void remove() { threadLocal.remove(); }2.原理那么ThreadLocal是如何做到保证每个线程get出来的数据不一样的呢？我们通过源码来看一下。public void set(T value) { Thread t = Thread.currentThread(); // 通过当前线程获取出来一个ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }我们发现在set方法中，会创建一个ThreadLocalMap，然后将要设置的值放在这个Map中，而当前这个ThreadLocal对象作为key；然后再将这个ThreadLocalMap赋值给Thread的threadLocals里。如果去看Thread类的代码会发现，在Thread类中存在两个变量threadLocals和inheritableThreadLocals，它们的类型就是ThreadLocal.ThreadLocalMap。通过下图可以看出Thread，ThreadLocal，ThreadLocalMap三者的关系。这时候我们再看看get()和remove()方法的代码。public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }可以看出来，和我们上面图中的结构一样，get()方法就是从Thread中取出来ThreadLocalMap，然后通过ThreadLocal对象作为Key取出值；remove()方法则是取出ThreadLocalMap将ThreadLocal对应的数据移除。那么ThreadLocalMap到底是什么呢？是java.util.Map接口的子类吗？我们来看源码。static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; // Default to 0 // 省略其他代码 }通过源码发现，ThreadLocalMap并没有实现Map接口，也没有集成其他任何的Map类。是定义在ThreadLocal类中的一个静态内部类。而它的结构和HashMap结构极其相似。有一个Entry[]数组存放数据。而这个Entry类是继承自WeakReference类的子类，这一点和HashMap有所不同。ThreadLocalMap和HashMap结构有很多不同，但是还有一点和HashMap不一样，我们来看一下ThreadLocalMap的set方法。private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // 计算key对应放在tab中的下标 int i = key.threadLocalHashCode & (len-1); //循环遍历tab,首先获取到下标i对应的对象，如果不为空,则执行循环体 // 如果不是相同的threadLocal或者位置失效，则要寻找下一个位置。 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 判断与当前ThreadLocal是否是同一个对象，如果是则进行值替换，结束 if (k == key) { e.value = value; return; } // 如果k==null代表threadLocal的key失效了，将失效的key进行替换 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }代码中循环遍历tab,首先获取到下标i对应的对象，如果不为空,则执行循环体，如果在循环体中的两个条件都不满足，则会执行nextIndex()方法，这个方法的代码如下：private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }这个方法的逻辑就是 开放寻址法 。而HashMap则是通过 拉链法 和 rehash 来做的。3.使用场景通过上面的内容基本可以掌握ThreadLocal的基本用法，那么ThreadLocal主要在什么场景中使用呢。ThreadLocal的作用通过以上了解我们知道主要是用来做线程间数据隔离。那么在什么场景下能用到线程隔离呢？首先想到的就是SimpleDateFormat这个工具类，它不是线程安全的，可以通过ThreadLocal在每个线程中放一份，保证线程安全。还有比如说用户登录的session，或者token数据，只数据当前会话线程，也可以通过ThreadLocal存储。再比如在某些场景下，上下文数据在不同方法之间调用，传递起来非常麻烦，可以通过ThreadLocal存放，只需要在需要用到的地方获取就可以。除了这些场景，在某些框架源码中也会使用到，比如Spring中的事务也主要是通过ThreadLocal和面向切面编程AOP实现的，感兴趣的同学可以查看源码了解。4.避免踩坑内存泄漏ThreadLocalMap中的Entry的Key是一个弱引用，因此如果在使用后不调用remove方法清除掉会导致对应的value内存泄漏。所以在使用完以后一定要记得调用remove方法清除数据。
健康《程序员健康指南》- 链接：https://pan.baidu.com/s/1hb8xQCE3TrEjazF5ZCe6-w ，提取码：h6fh本书是为程序员量身制作的健康指南，针对头痛、眼部疲劳、背部疼痛和手腕疼痛等常见的问题，简要介绍了其成因、测试方法，并列出了每天的行动计划，从运动、饮食等方面给出详细指导，帮助程序员在不改变工作方式的情况下轻松拥有健康。之所以把这个放到第一位，是因为许多健康问题都是知道的越早越好，这样才能更好地预防Java 基础《Head First Java（第二版）》- 链接：https://pan.baidu.com/s/17LNLb-1s_k9wR1kK__s1eQ ，提取码：v3ae《Java核心技术卷一（第十版）》- 链接：https://pan.baidu.com/s/1l-pDMOkR832ceA5q2CVZ7g ，提取码：v88g《Java编程思想（第四版）》 - 链接：https://pan.baidu.com/s/1NwOrkSadZuL64DH43_5rKA ，提取码：jgorJava 进阶偏实战，需要打好上面的基础《Java8实战》- 链接：https://pan.baidu.com/s/1P6RBXi5PzRm4zuKgUr14sg ，提取码：q3lo《Java 并发编程实战》- 链接：https://pan.baidu.com/s/1m6ts-HSu2SgFVRgJKKPl6Q ，提取码：i7nz《实战Java高并发程序设计（第2版） 》- 链接：https://pan.baidu.com/s/1tSPNtR1CNGea6gKfXnBklA ，提取码：dad3《Java性能权威指南》- 链接：https://pan.baidu.com/s/1OlHlJ4qyPHQe5QMB6qkLmw ，提取码：vwca《深入理解Java虚拟机：JVM高级特性与最佳实践（第3版）》- 链接：https://pan.baidu.com/s/1ZQi72jiqQzHlFhdkMqJtXg ，提取码：h7n7Java 优化下面主要讲解了Java开发常见的一些规范，使代码更加整洁高效《码出高效：Java开发手册》- 链接：https://pan.baidu.com/s/19sttAcC01G0wg1nqkfmprA ，提取码：zemp《Effective Java（第三版）》- 链接：https://pan.baidu.com/s/1RDaiVNViHvTVVunMkpFecg ，提取码：m9qe《嵩山版 Java 开发手册》- 链接：https://pan.baidu.com/s/1Rgh5tA_yPE-nV9MjPE572w ，提取码：62iw《代码整洁之道》《程序员修炼之道 从小工到专家》- 链接：https://pan.baidu.com/s/1lVoRCEr9NJR2I5rPneXK0g ，提取码：fxwe计算机《TCP-IP详解(卷一、二、三)》- 链接：https://pan.baidu.com/s/1D7luuYvEfTjaC7bh8Tm1Hg ，提取码：pmcj《深入理解计算机系统（原书第3版）》- 链接：https://pan.baidu.com/s/15F6MOyryXUI_l7DLoNbgiA ，提取码：7x0u算法《算法图解》- 链接：https://pan.baidu.com/s/1qbJAJbLInXo-rlPBGLOFUA ，提取码：k9nj《算法珠玑(Java版)》- 链接：https://pan.baidu.com/s/1EQj2sQk1Ys-EU8mwHho_MA ，提取码：7fxt《Leetcode 前 300 题算法题解析（Java）》- 链接：https://pan.baidu.com/s/1BGuMZW2ec4P_cyiv6sicww ，提取码：2jty设计模式《Head First 设计模式》- 链接：https://pan.baidu.com/s/1rxH5Ll38VpHl_j6ts6Q-oQ ，提取码：z6xg《Design-Pattern包教不包会(设计模式包教不包会)》- 链接：https://pan.baidu.com/s/1SDIsamZCv7JKY1zTuNLKtg ，提取码：81ci《设计模式 Java版本》- 链接：https://pan.baidu.com/s/1ikneBCgvAoIk5A-M6snIPg ，提取码：7b98《设计模式：可复用面向对象软件的基础》- 链接：https://pan.baidu.com/s/1CPJUAJp_oryQnKxWCbn8tA ，提取码：7qtiSpring 全家桶《Spring实战（第4版）》- 链接：https://pan.baidu.com/s/1CC1mAD4kZ57SmQRA8NEDXg ，提取码：ei6v《Spring Boot实战 》- 链接：https://pan.baidu.com/s/1QPfW43uN1uTJnsdKzUBDog ，提取码：aukv《Spring Boot 2企业应用实战》- 链接：https://pan.baidu.com/s/1s31xMCJ3WIjN2Koc-hSlyg ，提取码：3apd《JavaEE开发的颠覆者 Spring Boot实战》- 链接：https://pan.baidu.com/s/1TOmImwqszUDYmXc9uALaWg ，提取码：blpp《SpringBoot实战系列》- 链接：https://pan.baidu.com/s/1PPvY5goAI8L3SChfYSaAiw ，提取码：j26wMaven 依赖管理《Maven实战 高清》- 链接：https://pan.baidu.com/s/1MUZOKKjv5F7XY32aUIDaog ，提取码：xj6wMySQL 数据库《MySQL技术内幕（第5版）》- 链接：https://pan.baidu.com/s/1Dv6HXvhmyiGA2Gsn7NBo3w ，提取码：d7pj《MySQL技术内幕：InnoDB存储引擎（第2版）》- 链接：https://pan.baidu.com/s/1-dRbRaERiEJGIg3tgg00_A ，提取码：t4rh《高性能MySQL（第3版）》- 链接：https://pan.baidu.com/s/1yjHTlNcJLTaXjUAv2al0HA ，提取码：v037《Effective MySQL之SQL语句最优化》- 链接：https://pan.baidu.com/s/1IVWZw_JfL8y0KQ290gHT6w ，提取码：cbiy《SQL反模式》- 链接：https://pan.baidu.com/s/1C_jnScaSBw72ET_Qkh5phQ ，提取码：qspw《深入MySQL实战》- 链接：https://pan.baidu.com/s/14IP44JpJruedpOilLTiUog ，提取码：ejia《mybatis-plus 实践及架构原理》- 链接：https://pan.baidu.com/s/1bBVCVy6_YRM0aMeNOAveww ，提取码：joexRedis 数据库《redis设计与实现(第二版)》- 链接：https://pan.baidu.com/s/19mtOtYKLFWoY9fpP4v7MsA ，提取码：7cmx《Redis深度历险：核心原理和应用实践》- 链接：https://pan.baidu.com/s/1tVTucPsmwY2M5HJSNvhpCg ，提取码：oyiyRabbitMQ 消息队列《RabbitMQ实战 高效部署分布式消息队列》- 链接：https://pan.baidu.com/s/11btAl5txQLPd6DcT3hX0FA ，提取码：lmz5《深入RabbitMQ》- 链接：https://pan.baidu.com/s/1F4iCPSJGhBBQzaebC_E_BA ，提取码：nhp0架构《架构探险：从零开始写分布式服务框架》- 链接：https://pan.baidu.com/s/1_Eo4pqAekHVyBlJ7EIayZA ，提取码：lc2t《大型网站技术架构 核心原理与案例分析》- 链接：https://pan.baidu.com/s/1zt4Ee4sUnO4H654ZeuRxTg ，提取码：w1rb
篇我来向大家介绍一种兼容所有缓存中间件的方案，不论我们是使用 Redis 还是 Ehcache，都不需要关心如何操作 Redis 或者 Ehcache，这套方案统统帮你搞定。这套方案就是大名鼎鼎的 Spring Cache。什么？你没有听过，没关系，本篇带你一起探索。文章目录索引文章目录索引一、揭开 Spring Cache 的面纱1.1 现有缓存方案的痛点1.2 Spring Cache 介绍1.3 Spring Cache 有什么功效1.4 Spring Cache 的原理是什么？1.5 缓存注解二、使用缓存2.1 引入 Spring Cache 依赖2.2 配置使用哪种缓存2.3 测试缓存2.4 自定义配置类2.5 自定义 key2.6 自定义条件2.7 更新注解2.8 删除缓存注解三、 总结一、揭开 Spring Cache 的面纱1.1 现有缓存方案的痛点试想一种场景：用户 A 打开 APP，进入到了秒杀商品的详情页，那这个商品数据我们会先去数据库查询，然后返回给客户端。因为有大量用户短时间内进入到了详情页，所以可以把活动列表缓存起来，直接读缓存就可以了。那下次再查询商品时，直接去缓存查询就可以了。如果秒杀商品下架了，缓存的数据不会用到了，就把缓存删掉就可以了。上面几步看起来也没啥问题，但是放缓存，删除缓存这两步是需要我们去手动写代码实现的。有没有一种方式不用写操作缓存的代码？假如现在用的缓存中间件是 Redis，领导说要换成 Ehcache，操作缓存的代码是不是又得重新撸一遍？总结下上面场景的痛点：需要手写操作缓存代码，如添加缓存、更新缓存、删除缓存。切换缓存组件并不容易，或者说没有对缓存层进行抽象封装，依赖具体的缓存中间件。哪有没有一种方案可以帮助解决上面的两个痛点呢？这就是今天要介绍的 Spring Cache。1.2 Spring Cache 介绍Spring Cache 是 Spring 提供的一整套的缓存解决方案。虽然它本身并没有提供缓存的实现，但是它提供了一整套的接口和代码规范、配置、注解等，这样它就可以整合各种缓存方案了，比如 Redis、Ehcache，我们也就不用关心操作缓存的细节。Spring 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术，并支持使用注解来简化我们开发。Cache 接口它包含了缓存的各种操作方式，同时还提供了各种xxxCache缓存的实现，比如 RedisCache 针对 Redis，EhCacheCache 针对 EhCache，ConcurrentMapCache 针对 ConCurrentMap，具体有哪几种，后面实战中会介绍。1.3 Spring Cache 有什么功效每次调用某方法，而此方法又是带有缓存功能时，Spring 框架就会检查指定参数的那个方法是否已经被调用过，如果之前调用过，就从缓存中取之前调用的结果；如果没有调用过，则再调用一次这个方法，并缓存结果，然后再返回结果，那下次调用这个方法时，就可以直接从缓存中获取结果了。1.4 Spring Cache 的原理是什么？Spring Cache 主要是作用在类上或者方法上，对类中的方法的返回结果进行缓存。那么如何对方法增强，来实现缓存的功能？学过 Spring 的同学，肯定能一下子就反应过来，就是用 AOP（面向切面编程）。面向切面编程可以简单地理解为在类上或者方法前加一些说明，就是我们常说的注解。Spring Cache 的注解会帮忙在方法上创建一个切面（aspect），并触发缓存注解的切点（poinitcut），听起来太绕了，简单点说就是：Spring Cache 的注解会帮忙在调用方法之后，去缓存方法调用的最终结果，或者在方法调用之前拿缓存中的结果，或者删除缓存中的结果，这些读、写、删缓存的脏活都交给 Spring Cache 来做了，是不是很爽，再也不用自己去写缓存操作的逻辑了。1.5 缓存注解Spring 提供了四个注解来声明缓存规则。@Cacheable，@CachePut，@CacheEvict，@Caching。大家先有个概念，后面我们再来看怎么使用这些缓存注解。二、使用缓存2.1 引入 Spring Cache 依赖在 pom 文件中引入 spring cache 依赖，如下所示： org.springframework.boot spring-boot-starter-cache 2.2 配置使用哪种缓存Spring Cache 支持很多缓存中间件作为框架中的缓存，总共有 9 种选择：caffeine：Caffeine 是一种高性能的缓存库，基于 Google Guava。couchbase：CouchBase 是一款非关系型 JSON 文档数据库。generic：由泛型机制和 static 组合实现的泛型缓存机制。hazelcast：一个高度可扩展的数据分发和集群平台，可用于实现分布式数据存储、数据缓存。infinispan：分布式的集群缓存系统。jcache：JCache 作为缓存。它是 JSR107 规范中提到的缓存规范。none：没有缓存。redis：用 Redis 作为缓存simple：用内存作为缓存。我们还是用最熟悉的 Redis 作为缓存吧。配置 Redis 作为缓存也很简单，在配置文件 application.properties 中设置缓存的类型为 Redis 就可以了， 如：spring.cache.type=redis当然，别忘了还要在 pom 文件中 引入 Redis 的依赖，不然用不了 Redis。 org.springframework.boot spring-boot-starter-data-redis 2.3 测试缓存那基础的配置已经做好了，现在就是看怎么使用 Spring Cache 了。（1）启动类上添加 @EnableCaching注解。本文案例就是在 启动类 PassjavaQuestionApplication 添加 @EnableCaching 注解。（2）指定某方法开启缓存功能。在方法上添加 @Cacheable 缓存注解就可以了。@Cacheable 注解中，可以添加四种参数：value，key，condition，unless。首先我们来看下 value 参数。下面的代码出于演示作用，用了最简单的逻辑，test 方法直接返回一个数字，连数据库查询都没有做。不过没关系，我们主要验证 Spring Cache 是否对方法的结果进行了缓存。@RequestMapping("/test") @Cacheable({"hot"}) public int test() { return 222; }大家注意，@Cacheable 注解中小括号里面还含有大括号，大括号里面还有 “hot” 字符串，这个 hot 字符串你可以把它当作一个缓存的名字，然后将 test 方法返回的结果存到 hot 缓存中。我们也可以用 value="hot" 的方式。第一次调用 test 方法前，既没有 hot 缓存，更没有 test 的结果缓存。调用 test 方法后，Redis 中就创建出了 hot 缓存了，然后缓存了一个 key，如下图所示：第二次调用 test 方法时，就从缓存 hot 中将 test 方法缓存的结果 222 取出来了，为了验证没有执行 test 中的方法，大家可以在 test 方法中打下 log 或者断点。最后的验证结果肯定是没有走 test 方法的，而是直接从缓存中获取的。那我们再来测试一个方法，方法名改为 test2，且请求路径也改为 test2 了。@RequestMapping("/test2") @Cacheable({"hot"}) public int test2() { return 456; }大家觉得这两个方法的结果都会缓存吗？还是只会缓存第一个被调用的方法。经过测试，执行第一个 test 方法后，再执行 test2 方法，缓存结果一直是 222 不会变。因为他们的 key 都是 默认的 SimpleKey[]，所以两个方法对应的缓存的 key 都叫这个，所以得到的缓存值是一样的。（3）加上数据库查询的测试。有的同学可能觉得上面的测试太简单了，test 方法里面啥都没做，还缓存啥呢，完全没必要啊。没关系，大家的顾虑是对的，我们来加上数据库查询，安排~先说下场景：前端需要查询某个题目的详情，正常逻辑是查询数据库后返回结果。假定这个查询操作非常频繁，我们需要将题目详情进行缓存。我们先看看常规 Redis 缓存方案：先从 Redis 缓存中查看缓存中是否有该题目，如果缓存中有，则返回缓存中的题目；如果没有，就从数据库中查。查询出题目后，就用 Redis 存起来，然后返回。这里就要写操作 Redis 的代码了：查询 Redis 缓存、更新 Redis 缓存。 // 查询缓存，假定该题目详情缓存的 key=question1 redisTemplate.opsForValue().get("question1"); // 更新缓存 redisTemplate.opsForValue().set("question1", questionEntity); 那如果用 Spring Cache 注解的话，上面两行代码可以直接干掉了。如下所示，加一个 @Cacheable 注解搞定。 @Cacheable({"question", "hot"}) public QuestionEntity info(Long id) { return getById(id); // 查询数据库操作 }其中 question 和 hot 是缓存的名字，我们可以将结果放到不同的缓存中。结论：如果没有指定请求参数，则缓存生成的 key name，是默认自动生成的，叫做 SimpleKey[]。如果指定了请求参数，则缓存的 key name 就是请求参数，比如上面 info 方法，key 等于我传入的 id = 1。缓存中 key 对应的 value 默认使用 JDK 序列化后的数据。value 的过期时间为 -1，表示永不过期。2.4 自定义配置类上面保存的缓存数据都是默认设置，我们也可以自己定义配置，如下所示，在配置文件 application.properties 添加如下配置：# 使用 Redis 作为缓存组件 spring.cache.type=redis # 缓存过期时间为 3600s spring.cache.redis.time-to-live=3600000 # 缓存的键的名字前缀 spring.cache.redis.key-prefix=passjava_ # 是否使用缓存前缀 spring.cache.redis.use-key-prefix=true # 是否缓存控制，防止缓存穿透 spring.cache.redis.cache-null-values=true然后需要加一个配置类：MyCacheConfig。可以在我的开源项目 passjava 获取完整源码。RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { }2.5 自定义 key然后我们可以指定 key 的值，可以在 @Cacheable 注解里面加上 key 的值 #root.method.name。这是一种特有的表达式，称作 SpEL 表达式，这里代表用方法名作为缓存 key 的名字。@Cacheable(value = {"hot"}, key = "#root.method.name") 接下来就是见证奇迹的时刻，调用 test 方法和 test2 方法，发现有两个不同的 key，一个是 passjava_test1，另外一个 passjava_test2，它们的 key 就是前缀 passjava_ + 方法名 组成。SpEL 表达式还有很多其它规则，如下所示：可以根据项目需要选择合适的表达式来自定义 key。2.6 自定义条件除了设置缓存条目的 key，我们还可以自定义条件来决定是否将缓存功能关闭。这里就要用到 @Cacheable 另外两个属性：condition 和 unless，它俩的格式还是用 SpEL 表达式。对应的四个属性总结如下：代码示例如下：@Cacheable(value = "hot", unless = "#result.message.containss('NoCache')") 当放回的结果 message 字段包含有 NoCache 就不会进行缓存。2.7 更新注解@CachePut 也是用来更新缓存，和 @Cacheable 非常相似，不同点是 @CachePut 注解的方法始终都会执行，返回值也会也会放到缓存中。通常用在保存的方法上。保存成功后，可以将 key 设置保存实例的 id。这个怎么做呢？之前我们说过 key 可以通过 SpEL 表达式来指定，这里就可以搭配 #result.id 来实现。这里还是用个例子来说明用法：创建题目的方法，返回题目实例，其中包含有题目 id。@RequestMapping("/create") @CachePut(value = "hot", key = "#result.id") public QuestionEntity create(@Valid @RequestBody QuestionEntity question){ return IQuestionService.createQuestion(question); }保存的 id 是自增的，值为 123，所以缓存中的 key = passjava_123。2.8 删除缓存注解@CacheEvict 注解的方法在调用时不会在缓存中添加任何东西，但是会从从缓存中移除之前的缓存结果。示例代码如下：@RequestMapping("/remove/{id}") @CacheEvict(value = "hot") public R remove(@PathVariable("id") Long id){ IQuestionService.removeById(id); return R.ok(); }删除条目的 key 与传递进来的 id 相同。我测试的时候传的 id = 123，经过前缀 passjava_组装后就是 passjava_123，所以将之前缓存的 passjava_123 删除了。重复执行也不会报错。注意：@CacheEvict 和 @Cacheable、@CachePut 不同，它能够应用在返回值为 void 的方法上。@CacheEvict 还有些属性可供使用，总结如下：三、 总结本文通过传统使用缓存的方式的痛点引出 Spring 框架中的 Cache 组件。然后详细介绍了 Spring Cache 组件的用法：五大注解。 @Cacheable、@CachePut、@CacheEvict、@Caching,、@CacheConfig。如何自定义缓存条目的 key。如何自定义 Cache 配置。如何自定义缓存的条件。参考资料：www.passjava.cnSpring in Action版权声明: 本文为 InfoQ 作者【悟空聊架构】的原创文章。原文链接:【https://xie.infoq.cn/article/26c54c246279306df0c291327】。