有用的和不为人知的Java特性
在日常java开发中,在jdk8以前 集合的操作比较麻烦,特别是对集合空的判断,需要写一些重复相似的代码去做判断,但是在jdk8以后,concurrent 包下有丰富的集合接口,简化了之前使用集合的复杂度.这里说一些有用的几个特性且容易被忽略的.
延迟队列
在开发中如果需要把元素加入到一个队列集合中,但是希望它能够延迟执行,那这里就推荐使用 DelayQueue 队列.这样在加入队列的时候设置这个元素和对象的过期时间就可以了,过期时间到了,就会从队列中出来.这是需要实现Delayed接口,重写掉getDelay和compareTo函数 代码如下:
@Data public static class DelayedEvent implements Delayed { private long startTime; private String message; public DelayedEvent(long startTime, String message) { this.startTime = startTime; this.message = message; } @Override public long getDelay(TimeUnit unit) { long diff = this.startTime - System.currentTimeMillis(); return unit.convert(diff, TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { return (int) (this.startTime - ((DelayedEvent) o).startTime); } }
调用代码如下,加入元素后,10S之后出对列:
final DelayQueuedelayQueue = new DelayQueue<>(); final long timeFirst = System.currentTimeMillis() + 10000; delayQueue.offer(new DelayedEvent(timeFirst, "hello word")); log.info("add done"); log.info(delayQueue.take().getMessage());
结果如下:
09:42:02.732 [main] INFO com.test.util.queue - add done
09:42:12.729 [main] INFO com.test.util.queue - hello word
时间格式日期
在jdk16以前,如果要根据当前时间判断是上午还是下午,那则需要自己实现函数,在函数里面根据一天不同时段做判断,在jdk14以后,可以用下面函数实现:
这样随着时间的变化,返回的时间提示也在动态变化.
String timeHit = DateTimeFormatter.ofPattern("B").format(LocalDateTime.now());
System.out.println(timeHit);
DateTimeFormatter 支持的字母列表如下:
结果返回如下:
并发累计器
又叫做并发加法器, 它允许我们在许多场景中实现一个无锁算法,java concurrent包下提供了LongAccumulator (也有 DoubleAccumulator)使用提供的函数更新值,
在多线程场景下,如果要更新一个int,long 类型的值,通常我们会用AtomicInteger,AtomicLong.给定初始值为10,然后50个线程并发调用,每次累计加1,这里我们直接用 Accumulator 结构实现:
LongAccumulator accumulator = new LongAccumulator(Long::sum, 10); Runnable w = () -> accumulator.accumulate(1); ExecutorService executor = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) { executor.submit(w); } executor.shutdown();
System.out.println("Balance: " + accumulator.get());
结果返回如下:
十六进制格式
在jdk8版本中,如果需要在十六进制、字符串、字节 类型相互转换时,转换的操作稍微复杂.在jdk 17版本中提供了HexFormat类来完成这之间的相互转换.代码如下:
HexFormat format = HexFormat.of();
byte[] input = new byte[] {100, 20, -10, 30}; String hex = format.formatHex(input); System.out.println(hex); byte[] output = format.parseHex(hex); assert Arrays.compare(input, output) == 0;
数组的二分查找
在jdk1.8以前,在给定有序数组集合中,查找一个元素的位置,有则返回元素位置,没有返回-1,这需要手动实现二分查找算法,该算法每次把元素近似二分,然后查找.在 jdk1.8 以后提供了底层算法支持,
例如在给定有序集合中,查找元素x,如果x 存在则,返回元素的位置,元素不存在则返回插入该元素的位置索引,代码如下:
int[] t = new int[]{10, 15, 20, 21}; int idx = Arrays.binarySearch(t, 15); System.out.println("元素位置:" + idx); idx = Arrays.binarySearch(t, 100); System.out.println("插入元素的位置:" +~idx);
这里补充一下:
取反操作符(~)结论总结:
当n为正数时,~(n) = -(n+1)
当n为负数时,~(-n) = n - 1,忽略负号
结果如下:
Bit Set
如果我们需要对位数组进行操作,一般操作就是我们申明一个boolean 类型的数组,每一位用boolean表示true和fasle(0和1).有了bit set 类,我们就可以申明为 BItSet .该类允许我们存储和操作位的数组。与布尔值[]相比,它消耗的内存要少8倍,我们可以对数组执行逻辑操作,例如,和,或,xor (可以理解就是集合中的 并集 交集 差集),代码如下:
BitSet bs1 = new BitSet(); bs1.set(0); bs1.set(2); bs1.set(4); System.out.println("bs1 : " + bs1); BitSet bs2 = new BitSet(); bs2.set(1); bs2.set(2); bs2.set(3); System.out.println("bs2 : " + bs2); bs2.xor(bs1); System.out.println("xor: " + bs2); bs2.and(bs1); System.out.println("and: " + bs2);
结果如下:
Phaser
phaser 是解决了分阶段的并发问题,与的 countdowlatch 非常相似。然而,它提供了一些额外的功能。它允许我们设置在继续执行之前需要等待的线程的动态数量。使用 Phaser 时,定义的线程数需要在屏障上等待,然后才能进入下一步的执行。正因为如此,我们可以协调多个执行阶段.定义3个线程,这3个线程共同有3个阶段,第一个阶段执行完之后,再执行下一个阶段,代码如下:
Phaser phaser = new Phaser(3); Runnable r = () -> { System.out.println("phase-0"); phaser.arriveAndAwaitAdvance(); System.out.println("phase-1"); phaser.arriveAndAwaitAdvance(); System.out.println("phase-2"); phaser.arriveAndDeregister(); }; ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 3; i++) { executor.submit(r); }
结果如下:
Stamped Lock
javaconcurrent 是最有趣的java包之一,该包提供了很多多线程下并发使用的工具和集合.该锁的核心思想在于,在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。这种模式也就是典型的无锁编程思想,和CAS自旋的思想一样。这种操作方式决定了StampedLock在读线程非常多而写线程非常少的场景下非常适用,同时还避免了写饥饿情况的发生.是ReadWriteLock 的替代品,它允许对读操作进行乐观锁定。而且,它的性能比 ReentrantReadWriteLock 更好。 下面的例子是一个多个线程并发执行,其中一个线程是对一个数的自增操作,另外一个线程则是读出最新写入后的结果,其实根据所的特性,读采取乐观锁,理论上不会阻塞写操作.
public class Number {
private int acc;
public Number(int amount) {
this.acc = amount;
}
}
StampedLock lock = new StampedLock(); Number b = new Number(10); Runnable w = () -> { long stamp = lock.writeLock(); b.setAcc(b.getAcc() + 1); System.out.println("acc: " + b.getAcc()); lock.unlockWrite(stamp); }; Runnable r = () -> { long stamp = lock.tryOptimisticRead(); if (!lock.validate(stamp)) { stamp = lock.readLock(); try { System.out.println("read acc: " + b.getAcc()); } finally { lock.unlockRead(stamp); } } else { System.out.println("Optimistic read fails"); } }; ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executor.submit(w); executor.submit(r); }
下面的直接结果在没台机器上执行的过程可能不太一样,这取决于cpu的速速,但是最终结果一定是20.
执行结果如下: