分类 JavaWeb 下的文章

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

Javassist

Javaassist 是 Java 提供用于操作字节码的库。
即使 JDK11 后,反射的效率依旧很糟糕,Javaassist 可以用硬编码的方式,解决反射生成实例时,频繁反射带来的效率问题。

例子:

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

import java.util.concurrent.locks.ReentrantLock;

public class JavassistDemo {

    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();

        // Import package
        classPool.importPackage("java.util.concurrent.*");

        // Make class, set parent class
        CtClass parentCtClass = classPool.getCtClass(ReentrantLock.class.getName());
        CtClass warsLockClass = classPool.makeClass("WarsLock", parentCtClass);

        // Make constructor, set params and target class
        CtConstructor constructor = new CtConstructor(new CtClass[0], warsLockClass);
        constructor.setBody("{}");
        warsLockClass.addConstructor(constructor);
        // Make method
        warsLockClass.addMethod(CtMethod.make("void lock() { System.out.println(\"123~\"); }", warsLockClass));
        // Make instance
        Class<?> lockClass = warsLockClass.toClass();
        ReentrantLock lock = (ReentrantLock) lockClass.getDeclaredConstructor().newInstance();

        lock.lock();
    }
}

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

1) 线程通信

题目:实现一个容器,提供两个方法,add / size,写两个线程,线程 1 添加 10 个元素到容器中,线程 2 实现监控元素的个数,当个数到 5 个时,线程 2 给出提示并结束。

1.1) 思路:

首先会面临两个问题,add / size 函数的一致性问题,和容器的线程见可见性问题

  • 容器选择:两种选择方案,使用普通 list,给 add / size 上锁;或者用 Collections 来生成同步容器
  • 可见性问题:遇到可见性问题第一个会想到 volatile,但是 volatile 不能保证元素内部发生改变时的线程间可见性问题。处理方式多种:notify、CountDownLatch、LockSupport 等等,Semaphore 都行。

LockSupport 版,代码少哈哈哈哈哈哈哈哈

public class Thread_Notify {

    List<Object> list = Collections.synchronizedList(new ArrayList<>());
    static Thread a;
    static Thread b;

    public void add(Object o) {
        list.add(o);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Thread_Notify instance = new Thread_Notify();

        a = new Thread(() -> {
        LockSupport.park();
        System.out.println("=====5");
        LockSupport.unpark(a);
        });

        b = new Thread(() -> {
        for (int i = 1; i <= 10; i++) {
            instance.add(i);
            System.out.println(i);
            if (5 == i) {
            LockSupport.unpark(b);
            LockSupport.park();
            }
        }
        });

        a.start();
        b.start();
    }
}

2) 线程阻塞

题目:写一个固定容量同步容器,拥有 put 和 get 方法,以及 getCount 方法,能够支持 2 个生产者线程和 10 个消费者线程的阻塞调用。

2.1) 思路

这题本质上是考知识面,是否知道 Condition,可以给容器加个泛型装逼 -。-

public class ThreadCondition {

    private final int MAX_SIZE = 10;
    private final LinkedList<Object> list = new LinkedList<>();
    private int count = 0;

    private Lock lock = new ReentrantLock();
    Condition producer = lock.newCondition();
    Condition consumer = lock.newCondition();

    public void put(Object o) {
        try {
        lock.lock();
        while (MAX_SIZE == list.size()) producer.await();

        list.add(o);
        ++count;
        consumer.signalAll();
        } catch (InterruptedException e) {
        e.printStackTrace();
        } finally {
        lock.unlock();
        }
    }

    public Object get() {
        Object result = null;
        try {
        lock.lock();
        while (0 == count) consumer.await();

        result = list.removeFirst();
        --count;
        producer.signalAll();
        } catch (InterruptedException e) {
        e.printStackTrace();
        } finally {
        lock.unlock();
        }
        return result;
    }

    public static void main(String[] args) {
        ThreadCondition list = new ThreadCondition();

        for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 5; j++) {
            System.out.println(list.get());
            }
        }).start();
        }

        for (int i = 0; i < 2; i++) {
        new Thread(() -> {
            for (int j = 0; j < 25; j++) {
            list.put(Thread.currentThread().getName() + " - " + j);
            }
        }, "Producer " + i).start();
        }
    }
}

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

理论上来说,Synchronized 是自动锁,Java 中还有一些手动锁,即本文主题,介绍一些常用锁。
手动锁要注意写在 finally 中,避免死锁发生。

锁种类大致有乐观锁、悲观锁、自旋锁、读写锁、(排它锁、共享锁、)分段锁等等

1) ReentrantLock (重入锁)

众所周知 Synchronized 是可重入的,有一个可以替代 Synchronized 的锁即 ReentrantLock,且提供更多 Synchronized 无法实现的功能。

重入即:有 AB 两方法,需使用同一把锁,线程嵌套调用两方法是否会死锁。

1.1) 常用 API

  1. ReentrantLock(boolean fair):是否 fair,队列确保公平,默认非公平
  2. lock/unlock: 获取锁 / 解锁
  3. boolean trylock:尝试获取锁(可指定 try 的时间),返回是否成功
  4. lockInterruptibly:获取锁,可对 interrupt 响应
     Thread interrupt = new Thread(() -> {
         try {
             lock.lockInterruptibly(); // 获取锁
         } catch (InterruptedException e) {
             // ...... 收到 interrupt 信号处理
         } finally {
             // 要注意没拿到锁 unlock 会抛异常
             if (lock.isHeldByCurrentThread()) lock.unlock();
         }
     });
     interrupt.start();
     interrupt.interrupt();

1.2) 对比 Synchronized 优点

  • 基于 CAS
  • trylock 尝试获取锁
  • lockInterruptibly 可打断
  • fair 公平锁

2) CountDownLatch (倒计数门闩)

等待计数器归零,再继续运行程序

  1. CountDownLatch(count)
  2. await:线程进入阻塞,等待倒计数结束,可选指定超时时间
  3. countDown:count 计数减一
  4. getCount

3) CyclicBarrier (循环屏障)

循环设置一个线程数屏障,当线程数达到屏障数时,放行这些线程

  1. CyclicBarrier:设置屏障,指定屏障计数,可选指定每次打开屏障执行一个线程。
  2. await: 线程进入阻塞,等待屏障打开,可指定超时时间
  3. getNumberWaiting:正在等待线程数
  4. getParties:获取构造方法计数数量
  5. isBroken
  6. reset

4) Phase

大致是 CountDownLatch 和 CyclicBarrier 的整合, 可用来实现遗传算法,重写 Phase 自定义每个阶段操作

5) ReadWriteLock、StampedLock

内部有两把锁,读锁共享锁,写锁互斥锁。
读写互斥(排他),读锁被使用时其他读线程可共享这把锁
支持 Condition

  • ReentrantReadWriteLock(fair):是否公平,默认不公平
  • boolean trylock:尝试获取锁(可指定 try 的时间),返回是否成功
  • lockInterruptibly:获取锁,可对 interrupt 响应
  • newCondition

StampedLock 是 ReadWriteLock 的升级态,这里就不贴 API 了可以对照着使用。

6) Semaphore (信号量)

限制同时运行的线程数,可用于限流

  • Semaphore: 可指定是否公平
  • acquire / acquireUnInterraptibly:获取许可,为获取到许可则等待,默认为可 interrapter
  • release: 释放许可
  • tryAcquire:尝试获取许可,可指定超时

7) Exchanger

用于两个线程间通信交换变量,例:游戏交换装备

  • Exchanger
  • exchange(obj):线程进入等待,当有两个线程同时调用了 exchange,交换这两个线程传入对象的引用

8) LockSupport

线程阻塞工具,可以在线程内任意位置阻塞,常用 API 即 park / unpark 阻塞线程和唤醒线程

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

java.util.concurent 包下有一个 atomic,作用如其名,atomic 包下的类可以解决原子性问题。

1) Atomic

juc 提供了一些原子性的工具,可以实现对象的原子性,如 AtomicInteger、AtomicLong 可以实现原子的 Integer 和 Long。

1.1) 原理

原理是基于 Java 的下层 API:Unsafe 的 CAS(Compare and swap)保证原子性,和 volatile 保证线程可见性。
CAS(无锁优化、自旋、乐观锁) 就是一个自旋,不断的比较目标实例是否符合预期原值,符合才将目标值 swap 到目标实例。

CAS 存在 ABA 问题:我们想将 A swap 成 C,但是在 CAS 过程中,A 的值发生过改变但最后又变回了 A,compare 成功,CAS
,这就是 ABA 问题,一般情况下不影响业务逻辑,可以通过 versionNum 来解决 ABA 问题,JUC 也提供了 AtomicStampedReference 解决 ABA 问题。

2) LongAdder/DoubleAdder

在 JDK8 后,atomic 包中新增了两个类,LongAdder、DoubleAdder
在线程高竞争情况下 Adder 的效率要更高,但是会带来更大的空间消耗,且没有提供 xxAndGet 这样的 API,所以算是不安全的 Read。

2.1) 原理

Adder 相关类继承了 Striped64,Striped64 内部维护了一个 Cell 数组,并发时会把每个线程所做的修改分散到一个个 Cell 中
,Cell 中通过 CAS 实现原子性。当需要取 value 时,会把 Cell 中数据整合在一起,从而在线程竞争激烈时提高效率。

3) Accumulator

XXAdder 只能实现递增,如果有其他的需求,可以使用 XXAccumulator 自定义函数来实现非递增操作

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

1) 进程与线程

进程:Program app,如:qq.sh
线程:即一个进程里,最小的执行单元;进程中不同的执行路径

1.1) 启动线程方式

严谨来说只有两种方式,第三种线程池实际上也是用的 Thread 和 Runnable

  1. Thread
  2. Runnable
  3. new ThreadPoolExecutor

1.2) 线程状态

可以通过 target.getState 获取线程状态

  • New
  • Runnable
    • Ready
    • Running
    • TimedWaiting
    • Waiting
    • Blocked
    • Teminatted
  • Terminated

1.3) 线程基础 API

  1. sleep
  2. yield(): 将当前线程丢回等待队列
  3. target.join: join 到 target 线程中,target 执行后继续执行当前线程
  4. stop: 建议让线程正常结束,不建议使用 stop,容易产生线程的不一致。
  5. target.Interrupt: 向 target 线程发送 interrupt 信号
    • target 处于非阻塞状态时:target 中可以根据 Thread.currentThread().isInterrupted() 来捕获 interrupt 信号
    • target 处于阻塞状态时:target 可以通过捕获 InterruptedException 异常来处理 Interrupt 信号

1.4) Synchronized

给目标加锁,需要一个锁对象,各种使用方式:

  • 代码块:手动指定锁对象
  • 标注普通方法:使用 this 为锁对象
  • 标注 static 方法:使用当前类对象 xxx.class

1.4.1) 坑

锁对象避免使用基础的数据类型:String、Integer、Long,自己体会为何 -。-

1.4.2) 重入

Synchronized 是可重入锁

1.4.3) 异常

当 Synchronized 域中抛出异常时,HotSpot 中锁会被释放,要注意一致性的处理

1.4.4) 实现

JVM 没有规定如何实现 Synchronized,以下都是 HotSpot 的实现:

如何确定一个 Object 是否上锁,是通过 Object head 中的 MarkWord 实现 ,在 head 中占了 64bit,其中有 2bit 来标识此对象是否被锁定、和使用的锁类型。

在 JDK1.5 之前,Synchronized 都是使用 OS 锁(重量),从 1.5 开始,引入了锁升级的概念:

  1. 偏向锁
    如果锁对象只被某个线程访问,默认不加锁,只在 MarkWord 中记录该线程号,即偏向锁
  2. 自旋锁
    若有其他线程访问这把锁,升级为自旋锁
  3. 重量级锁
    • 1.5 时,自旋 10次 后未拿到锁,锁升级
    • 1.6 引入适应性自旋锁,大体上根据,当同时有超过 CPU线程数 / 2 的线程处于等待锁状态时,锁升级

当加锁代码,执行时间短、线程数少时使用自旋。
当加锁代码,执行时间长、线程数多时使用 OS 锁。

1.4.5) 优化

  • 细化 synchronized 域;
  • 一个方法中若使用多次 synchronized 域,可以考虑直接给方法加锁;

1.5) Volatile

volatile 关键字是在 1.5 后出现的

  • 保证线程可见性
    • MESI
  • 禁止指令重排序

Synchronized 只能保证原子性,所以 DCL单例模式(Double check lock) 的写法,若不给实例加 volatile,在初始化过程中还是会引发线程安全问题。

一个 if 没加锁,且条件变量无 volatile 修饰,会由于指令重排序印发线程安全问题。

Title - Artist
0:00