HTTP协议与无状态

大家常提起 HTTP 协议是无状态的,其指代的“无状态”是什么?常见的观点有:

  • 无状态代表相同的请求参数总是能返回相同的结果
  • HTTP 本身的设计是无状态的,增加了有状态协议头(Cookie/Session)后变成了有状态协议

对于第一个观点显而易见是错误的,它的表示更倾向于“幂等性”,这往往无法由协议本身保证,还需要服务器进行“有状态”的响应(若服务器不进行状态的持久化,当然无法做到幂等);而第二个观点听起来就靠谱很多,通过 Cookie/Session 进行状态维护从而保证了有状态。但我们可以考虑下 Cookie 或 Session 保证的是谁的状态?一般情况下,它们保证了后端服务器的状态,而非 HTTP 协议的状态。因此对于最初的问题 HTTP 协议的”无状态“,我们是不是应该从协议的本身出发呢?

揭开try-catch-finally的神秘面纱

根据 JDK Tutorial 的描述,除非在执行 try 或 catch 代码时线程被中断或 JVM 退出,finally 中的逻辑始终会执行。因此 finally 关键字常被用于释放资源,防止程序出现异常时出现资源泄露。本文主要探讨其在 JVM 层面的实现原理,以及 synchronized 关键字在类似场景的处理手段。首先来看一段简单的 try-finally 代码

1
2
3
4
5
6
7
public void testWithTryFinally() {
try {
System.out.println("try");
} finally {
System.out.println("finally");
}
}

如何优雅的避免空指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NPESolution {

public void withIf(Person person){
if(person != null){
// ...
}
// ...
}

public void withSpringAssert(Person person){
Assert.isTrue(person != null, "person must be not null.");
// ...
}

public void withOptional(Person person){
Optional<Person> personOptional = Optional.ofNullable(person);
// ...
}

public void withJsr305Annotation(@Nonnull Person person){
Optional<Person> personOptional = Optional.of(person);
// ...
}
}

上述的代码是我在日常用于避免空指针(NPE)的常用方式,很长时间内我都热衷于断言(Assert)这类防御性编程方式,防御性编程可以有效的保证方法的输入条件,并在毫无意义的边界情况能够给出有效的提示,何乐而不为呢?事实上防御性编程也确实是一种非常推荐的方式,并且其在 Spring 源码中随处可见。而 JDK8 的 Optional 是否会是一种更优雅的方式呢?亦或许,另有它人?

HSDB从入门到实战

HSDB(Hotspot Debugger),是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除

启动HSDB

检测不同 JDK 版本需要使用不同的 HSDB 版本,否则容易出现无法扫描到对象等莫名其妙的问题

  • Mac:JDK7 和 JDK8 均可以采用以下的方式

    1
    $ sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

    事实上经过测试,即使通过 JDK8 自带的 sa-jdi.jar 去扫描对象(scanoops)的时候也会发生扫不到的情况,但可以通过其他手段代替

    而 JDK11 的启动方式有些区别

    1
    $ /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home/bin/jhsdb hsdb

    事实上经过测试,该版本启动的 HSDB 会少支持一些指令(比如 mem, whatis),因此目前不推荐使用该版本

  • Windows:

    1
    $ java -classpath "%JAVA_HOME%/lib/sa-jdi.jar" sun.jvm.hotspot.HSDB

其中启动版本可以使用 /usr/libexec/java_home -V 获取

若遇到 Unable to locate an executable at “/Users/xx/.jenv/versions/1.7/bin/jhsdb” (-1) 可通过 Jenv 切换到当前 Jdk 版本即可解决

Static Nested Or Inner Classes

在 Java 中,在一个类中声明另一个类则称为嵌套类,被声明为 static 的嵌套类称为静态嵌套类(static nested classes ),与之相对的非静态嵌套类被称为内部类((inner classes

  • 非静态嵌套类每个实例都包含一个额外指向外围对象的引用,换句话说,要实例化一个非静态嵌套类必须首先实例化外部类

  • 静态嵌套类独立于外部类实例,可以看作嵌套在一个顶级类中的顶级类。因此,如果嵌套类不要求访问外部类的实例变量或方法,就要始终把 static 修饰符放在它的声明中,使它成为静态嵌套类。(如果该嵌套类不作为基类,那么更适合同时加上 final 修饰符)。JDK1.8 源码可见各种这样的设计,如 ReentrantLock 中

    1
    2
    3
    static final class NonfairSync extends Sync {
    ...
    }

我们从四个方面来更详细的讨论它们的区别:

  • 嵌套类访问外部类的范围

  • 嵌套类本身定义变量的范围

  • 实例化

  • 同名覆盖

泛型进阶

无限制通配符

无限通配符即: <?>,主要在不确定或不关心实际参数类型时使用,如:

1
2
3
public boolean removeAll(Collection<?> c){
...
}

由于它不确定具体类型,所以不能将任何元素(Null 除外)放入,即它是只读的,但在很多情况下需要放入对象,因此一种比较常见的方法是使用 类型参数 作为辅助函数

1
2
3
4
5
6
7
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}

public static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.get(j));
}

那么 List<?>List<Object> 有什么区别呢?

SELECT FOR UPDATE语句深度解析

  MysqlSELECT ... FOR UPDATE 语句是日常使用较多的用于锁定资源,确保在多个事务读取数据时始终能够读取到最新版本的数据的有效语句。那么它是怎么实现呢?在经过官网文档以及大量实践的验证之后发现网上存在大量不严谨甚至错误的信息,因此通过本文对 SELECT FOR UPDATE 语句作出以下总结。在具体介绍之前,先对目前网上教程或博客中会提到的几个常见误区进行纠正:

  • SELECT FOR UPDATE 在xx情况下会添加表级锁。

    请注意,在任何情况下 SELECT FOR UPDATE 都不会添加表级锁。事实上,在大部分情况下(DQL 语句,DML 语句,DDL 语句)都不会添加表锁,取而代之的是各种类型的行锁。

      那么我们如何获取表锁呢?语句如下:

    1
    2
    LOCK TABLES xx READ; # 为 xx 表添加表级 S 锁
    LOCK TABLES xx WRITE; # 为 xx 表添加表级 X 锁

    然后我们可以通过以下语句来检测当前 Mysql 有哪些表获取了表级锁

    1
    SHOW OPEN TABLES WHERE In_use > 0

    更多的表级锁相关知识请参考官网介绍

  • SELECT FOR UPDATE 在未使用索引时会”锁表”。

    SELECT FOR UPDATE 确实可以通过 Next-key lock 锁住所有记录和间隙来实现和表锁类似的效果。但未使用索引并非充分条件,我们判断 SELECT FOR UPDATE 是否锁住了所有数据和间隙还需要看它的隔离级别。

基础同步工具类

Semaphore,CountDownLatch,CyclicBarrier 均是 JDK1.5 提供的基础并发工具:

  • Semaphore 是一个计数信号量,用于限制同时访问某个特定资源的数量
  • CountDownLatch 是一个闭锁,允许一个或多个线程等待一组其他线程执行完成后执行,但只能使用一次
  • CyclicBarrier 是一个循环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行,并且支持重复使用

Semaphore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class X{

Random random = new Random();

Semaphore semaphore = new Semaphore(10, true);

// 方法本身控制同步
public synchronized Integer getNextAvailableItem() throws InterruptedException {

// 信号量控制访问次数
semaphore.acquire();

return random.nextInt();
}

public synchronized boolean markAsUnused(Integer item){

// do something

semaphore.release();

return true;
}
}

这是 Semaphore 的一个标准的使用方式,用于控制流量。上述程序创建了一个允许 10 个线程同时访问的信号量,并且使用公平锁(一般来说用于控制流量的使用需要使用公平模式,用于防止线程饥饿),然后在提供获取资源的接口 getNextAvailableItem 方法前先获取凭证,在释放资源后释放凭证。但是注意 Semaphore 不保证并发正确性,这需要接口自己保证,因此这里使用 synchronized 来提醒这一点。

ReentrantLock

  ReentrantLock 是基于 AQS 同步器实现的互斥锁,它支持设置公平锁/非公平锁模式,同时具有可重入性。在这里讨论 ReentrantLock 对这些特性的支持及应用。

标准模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class X {

private final ReentrantLock lock = new ReentrantLock();

public void m() {

lock.lock();

try {

// ... method body

} finally {

lock.unlock();
}
}

}

AQS同步器

  在 java.util.concurrent (JUC) 并发包中,如 ReentrantLock,Semaphore,CountDownLatch 等并发类的同步控制都是基于 AbstractQueuedSynchronizer (简称 AQS) 这个同步器抽象类来实现的。在这里较为深入的讨论同步器抽象类的实现原理与应用。

AQS简介

AbstractQueuedSynchronizer 内部维护着一个 FIFO 的 CLH 队列,队列中的每个 Node 代表着一个需要获取锁的线程

  自旋锁:自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是立刻进入线程挂起或睡眠状态。

  • CLH 锁(Craig, Landin, and Hagersten locks):基于链表的可扩展、高性能、公平的自旋锁,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋
  • MCS 锁:在当前结点自旋,但由前驱结点通知其结束自旋

AQS 采用的是一种变种的 CLH 队列锁:原始 CLH 是在前驱结点自旋,通过判断 pred.locked 来自旋,而 AQS 的 CLH 则是根据前驱结点的状态来控制阻塞,不会一直自旋。同时当前驱结点释放锁时会去唤醒该结点使其参与竞争锁。 AQS 的结点的定义如下:

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×