JUC并发编程

Java多线程

回顾:操作系统的进程概念。进程的问题:上下文切换开销。为了解决这个问题,出现了线程。

Thread thread = (() -> {
// ...
System.out.pirntln("Sub Thread");
});
thread.start();
System.out.println("Main Thread");
Main Thread # 主线程先输出结果,说明两个线程同时运行!
Sub Thread

线程优先级

Java使用抢占式调度,有以下三种优先级

MIN_PRIORITY
MAX_PRIORITY
NOM_PRIORITY

线程同步

共享内存会出现缓存一致性问题,因此需要线程锁机制保证数据安全性(原子性)。

synchronized // 悲观锁

synchronized (Class.class / this) { ... } // 类锁
synchronized (new Class() / instanceOfClass) { ... } // 实例锁
public synchronized void operation() { ... }

synchronized使用的锁存储在Java对象头中。

重量级锁

JDK6以前,synchronized被称为重量级锁。因为Java的线程是映射在OS原生线程上,上下文切换成本高;直到JDK6以后才优化了锁的实现。
简单来说,每个等待锁都会被封装成ObjectWaiter对象,分为三个区域

direction: right
Entry Set -> The Owner
The Owner <-> Wait Set

Entry Set会排队,直到它成为The Owner,享有资源。当The Owner调用wait()方法,就会挂起进入Wait Set,直到wait()所等待的操作完成。
但是每个线程占用同步代码块的时间并不长,完全不需要挂起又唤醒。
因此,可以使用自旋锁

自旋锁

调用wait(),自旋锁并不是被挂起,而是无限循环是否能够获取锁;当等待时间太长,会恢复重量锁机制。
JDK6以后,自旋时间是动态变化的。如果某个线程经常自旋失败,它会直接使用重量级锁;反之,则会延长自旋时间。

轻量级锁

JDK6后,为了减少获得和释放锁的消耗,引入了轻量级锁。
轻量级锁的设计目标是,在无竞争状态下减少重量级锁带来的性能消耗(切换内核态、线程阻塞引发线程切换)。

如果只有一个线程占用资源,那就不要加锁、解锁。
轻量级锁需要向系统申请互斥量。

CAS算法

Compare and Swap
CAS算法不是加锁,而是通过比较来判断对象是否已被修改,如果没有直接替换;如果被修改,那么修改失败。

轻量级锁就是使用CAS算法,如果CAS失败,那么进入重量级锁状态。

偏向锁

Biased Locking
-XX:UserBiasLock
当只有一个线程反复访问同步代码块,JVM直接让该线程获取锁,避免不必要的不同步操作。
根据对象头底层数据结构,如果对象调用过hashCode()通过哈希值来检查一致性,那么对象头就没有空间存放ThreadId了(JVM通过这个id判断是否频繁访问),此时该线程只能使用轻量级锁。

锁消除和锁粗化

如果在运行过程中,根本没有出现资源竞争,那就会直接把锁消除掉。
如果某个资源频繁地开锁解锁(比如在循环内部synchronized),JVM会把锁的范围放大,避免加锁解锁的开销。

Java Memory Model

Java Thread 1 <-> Working Memory 1 <-> Save/Load Operation 
Java Thread 2 <-> Working Memory 2 <-> Save/Load Operation
Java Thread 3 <-> Working Memory 3 <-> Save/Load Operation
Save/Load Operation <-> Main Memory

JMM内存模型中有以下规定:

  1. 所有变量存储在主内存
  2. 每条线程有自己的工作内存,不能直接操作主内存
  3. 不同线程间互相隔离,要传递内容,必须通过主内存

volatile

volatile的最大作用是保证变量可见性,即发生修改后强制刷新到主内存中,使其他线程的缓存失效;相当于通知了其他线程要更新变量为最新版本。
注意,volatile不能保证原子性。

Lock&Condition

Lock用法:

Lock lock = new ReentrantLock(); // 可重入锁
Runnable action = () -> {
for (int i = 0; i < 100000; i += 1) {
lock.lock();
i += 1; // 保证同一时刻只有一个线程操作i
lock.unlock();
}
}

new Thread(action).start();
new Thread(action).start();

Condition用法:

Condition cond = lock.newCondition();

Thread thread1 = () -> {
...
cond.await(); // 等待
...
}
Thread thread2 = () -> {
...
cond.signal(); // 唤醒await线程
...
}
new Thread(thread1).start();
new Thread(thread2).start();

LeetCode 1114 顺序打印123

class Foo {
    private Lock lock = new ReentrantLock();
    private Condition cond1 = lock.newCondition();
    private Condition cond2 = lock.newCondition();
    private volatile int state = 0;

    public Foo() { }

    public void first(Runnable printFirst) throws InterruptedException {
        lock.lock();
        try {
            // printFirst.run() outputs "first". Do not change or remove this line.
            printFirst.run();
            state = 1;
            cond1.signal();
        } finally {
            lock.unlock();
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        lock.lock();
        try {
            while (state != 1) {
                cond1.await();
            }
            // printSecond.run() outputs "second". Do not change or remove this line.
            printSecond.run();
            state = 2;
            cond2.signal();
        } finally {
            lock.unlock();
        }
    }

    public void third(Runnable printThird) throws InterruptedException {
        lock.lock();
        try {
            while (state != 2) {
                cond2.await();
            }
            // printThird.run() outputs "third". Do not change or remove this line.
            printThird.run();
        } finally {
            lock.unlock();
        }
    }
}

可重入锁

Re-entrant-Lock
这种锁可以多次加锁,同时也要多次解锁才算真的解锁了。

可重入锁是一种排他锁,其他线程必须等锁释放了才可以获取到锁。

公平锁与非公平锁

  • 公平锁:按照申请锁的时间去获得锁,会进入队列排队
  • 非公平:抢占式获取锁

读写锁

读写锁在同一时刻,可以让多个线程获取到锁。

  • 读锁:没有线程占用写锁的情况下,同一时间可以有多个线程加读锁。
  • 写锁:没有线程占用读锁的情况下,只有一个线程可以加写锁。
Lock reEntLock = new ReentrantLockReadWriteLock();
reEntLock.readLock().lock();
reEntLock.writeLock().lock();

锁降级、锁升级

reEntLock.writeLock().lock();
// 先加写锁,后加读锁,降级
reEntLock.readLock().lock();

...

// 然后释放写锁,只留下读锁,锁降级
reEntLock.writeLock().unlock();

AQS实现

Abstract Queued Synchronizer
在AQS中,一个线程获取锁后,其他线程进入等待队列。
等待队列由双向链表实现。