多线程|锁|JVM基础(难,需多学习,补充)

115 阅读3分钟

线程模型

Snipaste_2022-11-07_09-31-16.png 1:1线程模型用的比较多,用户线程调用频繁会影响内核线程,引用CAS避免部分状态切换(Java AQS函数级别锁),限制线程数量。(UT-用户线程;P-内核线程) 1对1.png n:1线程模型存在的问题是一个用户线程占用了轻量级进程,其他用户线程不能使用。 n对1.png m:n线程模型是结合上述两者优点。GO语言的GMP线程模型就是多对多。(Java的loom项目) m对n.png

锁机制

堆和方法区中需要人工加锁保证线程安全(蓝色部分)。 JVM运行内存结构.png 每个对象有一把锁,存放在对象头中,存放当前对象被哪个锁占用。
对象在内存中的布局: 对象头(markword/class pointer)+实例数据(instance data)+对齐(padding)
对象头.png

markword存储了很多和当前对象运行时状态有关的数据。(hashcode/锁状态标志/GC)
4Byte markword.png

synchronized通过javac转化成字节码指令(monitorenter/monitorexit)使线程同步。
monitor依赖操作系统的mutex lock来实现的。

public class TestSync {
    private int num = 0;
    public void test() {
        for (int i = 0; i < 1000; i++) {
            synchronized (this) {
                System.out.println("thread:"+Thread.currentThread().getId()+", num:"+num++);
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        TestSync sync = new TestSync();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                sync.test();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                sync.test();
            }
        });

        t1.start();
        t2.start();
    }
}

thread:21, num:599
thread:21, num:600
thread:22, num:601
thread:22, num:602 

多线程的:wait、notify 线程状态变换.png

无锁:非锁方式同步线程CAS。
偏向锁:不用CAS和mutex lock。对象头的markword,锁标志位(2bit)为01,是否偏向锁(1bit)为1,最前面的23bit位(线程ID)只有一个。假如多个线程ID,升级为轻量级锁。
轻量级锁:指向栈中锁记录的指针(30bit)+锁标志位(2bit)为00。

  • 当一个线程获取对象的锁时,看到markword的锁标志位为00,那么就是轻量级锁。
  • 这时,线程会在虚拟机栈开辟Lock Record空间(存放markword副本+owner指针)。
  • 线程通过CAS尝试获取锁,获得锁后:将markword复制给Lock Record,将owner指针指向该对象;将对象的markword的前30bit指针指向虚拟机栈的Lock Record。
  • 完成线程和对象锁的绑定。

有别的线程想获得对象锁,就CAS(自旋,CPU空转)->适应性自旋(自旋时间不固定,上次自旋时间+锁状态)决定。
当自旋等待的线程自旋个数超过CPU核数的一半 或者自旋次数超过10次,就会升级为重量级锁,使用monitor堆线程控制,完全锁定资源。
自旋->线程等待空转,不调用重量锁挂起线程、锁->挂起线程,状态切换

“悲观锁”(Pessimistic Concurrency Control):对资源进行锁定,一个一个来。
“乐观锁”(Optimistic Concurrency Control):无锁的同步机制
同步代码块 vs 线程切换

for (int i = 0; i < 3; i++) {
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            /*while(num < 1000) {  // 各线程num值未同步
                System.out.println("Thread name:" + Thread.currentThread().getName() + ":" + num++);
            }*/
            synchronized (Main.class) { // 互斥锁-悲观锁
                while(num < 1000) {
                    System.out.println("Thread name:" + Thread.currentThread().getName() + ":" + num++);
                }
            }
        }
    });
    t.start();
}

CAS

CAS(compare and swap),乐观锁,原子性,底层调用cmpxchg指令
old value:代表之前读到的资源对象的状态值;
new value:代表想要将资源对象的状态值更新后的值。
自旋:使其不断的重试CAS操作,通常会配置自旋次数来防止死循环。

import java.util.concurrent.atomic.AtomicInteger;
public class Main {
    static AtomicInteger num = new AtomicInteger(0);
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    while(num.get() < 1000) {
                        System.out.println("Thread name:" + Thread.currentThread().getName() + ":" + num.incrementAndGet());
                    }
                }
            });
            t.start();
        }
    }
}

Thread name:Thread-2:999
Thread name:Thread-2:1000
Thread name:Thread-0:906
Thread name:Thread-1:925

AQS(D)

AbstractQueuedSynchronizer
java.util.concurrent.locks

资源是否被占用的标志位(线程有独占模式、共享模式):
private volatile int state;

FIFO(队列-先进先出的双向链表)
private transient volatile Node head;
private transient volatile Node tail;

static final class Node {
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    volatile int waitStatus;  //等待状态
    volatile Node prev;       //前指针
    volatile Node next;       //后指针
    volatile Thread thread;   //线程对象
    Node nextWaiter;
    ...
    }

尝试获取锁(修改标记位),立即返回 ---> tryAcquire

protected boolean tryAcquire(int arg) {  //用户对该方法重写
    throw new UnsupportedOperationException();
}

获取锁(修改标记位),愿意进入队列等待,直到获取 ---> acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

addWaiter():将当前线程封装后加入等待队列

tryRelease
release

ReentrantLock(D)

基于AQS,在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步,
同时,和synchronized—样,ReentrantLock支持可重入,
除此之外,ReentrantLock在调度上更灵活,支持更多丰富的功能。
public class ReentrantLock implements Lock, java.io.Serializable {...} ReentrantLock.png

JUC(D)

JUC.png

ConcurrentHashMap(JDK1.7)

HashMap分布策略(JDK1.8):

  • 数组的长度始终保持为2的次幂
  • 将哈希值的高位参与运算
  • 通过位与操作来等价取模操作

HashMap动态扩容(JDK1.8):

  • 数组长度2n{2^n},因此数组长度length的二进制表示会在高位多出1bit
  • length会参与与运算确定下标位置,
  • 因此,扩容后的下标要么不变,要么移动2n{2^n}的位置?

问题:

  • 扩容出现环形链表异常
  • 多线程put会造成脏数据读写
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> 
    implements ConcurrentMap<K,V>, Serializable {...}

CurrentHashMap.png ConcurrentHashMap核心分段锁:Segment数组(继承ReentrantLock) + HashEntry数组

线程池

ThreadPoolExecutor:int变量保存->线程池状态(3bit)+工作线程数(29bit)

JVM基础

参数调优 JVM调优参数.png

垃圾回收算法 JVM垃圾回收算法.png

堆内存的新老永 JVM堆的新老永.png

运行数据区 JVM运行数据区.png

类加载器 JVM类加载器.png

参考资料

B站-寒食君-锁和多线程讲解