美文网首页JavaSE
高并发基础知识详细讲解

高并发基础知识详细讲解

作者: AIGame孑小白 | 来源:发表于2021-05-20 02:28 被阅读0次

Synchronized

synchronized锁哪些东西?

  • this(指的是当前对象)
  • 临界资源(锁的是临界资源)
  • 需要同步的方法(也是锁定当前对象)

启动运行java的时候会启动线程,每一条线程中都存在线程栈帧,在栈帧里面会存引用或者cont等,而对象创建会在Jvm的堆空间中,当这条线程需要访问该对象资源的时候会把这个引用的指针指向堆内存中的对象,如果需要上锁,就相当于给堆中的对象加锁。

所以锁到底是什么?

运行代码的时候实际上在线程栈中运行,对象存在于堆内存,而类对象存在于代码区,实际上我们的锁锁定的对象就是这两个,那么其他的线程就无法访问锁定的对象。

我们不妨这样理解:就是你的女朋友就相当于被你加锁了,别的男人不能再和她产生关系,那么并不代表她就不能和别人说话了,这个加锁其实锁定的是一种状态,不是完全限制。

static synchronized(静态锁方法)

public static synchronized void testSync(){
    System.out.println(staticCount++);
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

上面的静态锁方法实际上是锁定该类.class对象(类对象),等同于下面的代码:

public static void testSync(){
    synchronized(Test.class){
        System.out.println(staticCount++);
    }
}

锁定某一个代码块(锁定临界资源,最优写法):

private int count = 0;
private Object o = new Object();
public void testSync1(){
    synchronized(o){
        System.out.println(count++);
    }
}

锁定对象(外部访问时保证一致性):

public void testSync2(){
    synchronized(this){
        System.out.println(count++);
    }
}

重量级访问操作(不建议使用):

public synchronized void testSync3(){
    System.out.println(count++);
}

保证原子性

public class Test_03 implements Runnable {
    private int count = 0;
    @Override
    public /*synchronized*/ void run() {
        System.out.println(
                Thread.currentThread().getName() 
                + " count = " + count++);
    }
    public static void main(String[] args) {
        Test_03 t = new Test_03();
        for(int i = 0; i < 5; i++){
            new Thread(t, "Thread - " + i).start();
        }
    }
}

同步方法在调用的时候,可以调用别的方法吗?

同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。实验代码:

public class Test_04 {
    Object o = new Object();
    public synchronized void m1(){ // 重量级的访问操作。
        System.out.println("public synchronized void m1() start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("public synchronized void m1() end");
    }
    
    public void m3(){
        synchronized(o){
            System.out.println("public void m3() start");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("public void m3() end");
        }
    }
    public void m2(){
        System.out.println("public void m2() start");
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("public void m2() end");
    }
    public static class MyThread01 implements Runnable{
        public MyThread01(int i, Test_04 t){
            this.i = i;
            this.t = t;
        }
        int i ;
        Test_04 t;
        public void run(){
            if(i == 0){
                t.m1();
            }else if (i > 0){
                t.m2();
            }else {
                t.m3();
            }
        }
    }
    public static void main(String[] args) {
        Test_04 t = new Test_04();
        new Thread(new Test_04.MyThread01(0, t)).start();
        new Thread(new Test_04.MyThread01(1, t)).start();
        new Thread(new Test_04.MyThread01(-1, t)).start();
    }
}

上面的sm1()被加锁,但是m2()没有被加锁,如果我们同时多线程调用m1()的时候,那么m1()会被同步执行,但是m2()没有加锁不受影响。

synchronized可重入:

什么叫做可重入?举个简单的例子:例如下面的m1()和m2()都被加锁,那么我如果在m1()加锁的情况下在方法中调用被加锁的m2()方法,会不会出现死锁?

synchronized void m1(){ // 锁this
    System.out.println("m1 start");
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    m2();
    System.out.println("m1 end");
}
synchronized void m2(){ // 锁this
    System.out.println("m2 start");
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("m2 end");
}

public static void main(String[] args) {
    new Test_06().m1();
}

答案是不会出现死锁的情况,因为锁是可重入的。但是注意:锁可重入是需要前提条件的:同一个线程,多次调用同步代码,锁定同一个锁对象,此时才可重入。这里需要提到一个概念:那就是锁其实就相当于一个标记,并不是一个黑屋子。

锁的重入还有一个场景就是在子类继承父类的时候,依然可重入:

public class Test_07 {
    synchronized void m(){
        System.out.println("Super Class m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Super Class m end");
    }
    public static void main(String[] args) {
        new Sub_Test_07().m();
    }
}
class Sub_Test_07 extends Test_07{
    synchronized void m(){
        System.out.println("Sub Class m start");
        super.m();
        System.out.println("Sub Class m end");
    }
}

当同步方法出现异常的时候,会自动释放锁资源。

在业务逻辑中的编写(同步粒度问题)

synchronized void m1(){
    // 前置逻辑
    System.out.println("同步逻辑");
    // 后置逻辑
}

这种写法是不被接纳的,当高并发时,一定会影响效率,毕竟这是重量级锁。

void m2(){
    // 前置逻辑
    synchronized (this) {
        System.out.println("同步逻辑");
    }
    // 后置逻辑
}

所以我们一般都是在需要加锁的业务逻辑上加锁,同步代码块,这样会提高一点效率。(细粒度解决同步问题)

锁对象变更问题

synchronized锁的是对象而不是对象的引用,对象是存在对内存的,而引用是存在线程栈帧中的,看如下示例源码:

在m()中使用sync对object加锁以后,在主线程开启,并且过一段时间重新对object赋值,使得对象改变:

Object o = new Object();
void m(){
    System.out.println(Thread.currentThread().getName() + " start");
    synchronized (o) {
        while(true){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " - " + o);
        }
    }
}

public static void main(String[] args) {
    final Test_13 t = new Test_13();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m();
        }
    }, "thread1").start();
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            t.m();
        }
    }, "thread2");
    t.o = new Object();
    thread2.start();
}

在堆内存当中,存在该类Test,和对象Object,而在该类Text中存在的是对对象Object的引用o,线程启动后读取Text中的对象引用o,然后在线程栈帧中保存一个副本引用_o,该副本引用指向的也是堆内存中的对象,所以我们加锁实际上sync是对副本引用_o加锁,而不是对Test中的对象o加锁。


当第二条线程启动,把堆内存的对象o赋值为新的o后,原本Test中o指向的是Object,现在修改为o指向Object1了,但是对于第一条线程而言,同步代码块还是指向的之前的Object没有变,对于第二条线程sync锁的o指向的是新的Object1,又因为代码中输出的是Test的o,所以两个线程输出的结果是一致的。

对于上面的线程而言,对虽然对象换了,但是栈帧中的引用没有改变。综上所述:同步代码一旦加锁后,那么会有一个临时的锁引用执行锁对象,和真实的引用无直接关联。在锁未释放之前,修改锁对象引用,不会影响同步代码的执行。

同一对象

当我们下边的i1和i2是同一对象的时候,只有线程1执行,当i1和i2不是同一个对象的时候,两条线程都执行。

void m1(){
    synchronized (i1) {
        System.out.println("m1()");
        while(true){}
    }
}

void m2(){
    synchronized (i2) {
        System.out.println("m2()");
        while(true){}
    }
}
public static void main(String[] args) {
    final Test t = new Test();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m1();
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m2();
        }
    }).start();
}

注意下面的代码:假如上面的两个对象分别是这样的,那么这两个值是存在方法区中的常量池中,依然能够使得线程2无法运行。

Integer i1 = 1;
Integer i2 = 1;

假如修改成这样:

Integer i1 = new Integer("1");//只要new必在堆内存
Integer i2 = new Integer("1");

那么这两个就是不同的对象了,两个线程都会运行。

Volatile

基本概念

变量的线程可见性。在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其他线程修改,可能造成数据不一致的情况,从而导致结果错误。而 volatile修饰的变量是线程可见的,当 JVM 解释 volatile 修饰的变量时,会通知 CPU,在计算过程中,每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓存中的数据,可以保证计算结果的正确。

volatile 只是通知底层计算时,CPU 检查内存数据,而不是让一个变量在多个线程中同步。

volatile解决的是不可见的问题,例如下面的代码中,使用一个全局变量来控制while循环,当这个死循环方法被一条线程执行后,假如对全局变量修改为false,理论上来讲,死循环应该结束运行才对:

volatile boolean b = true;//控制死循环
void m(){
    System.out.println("start");
    while(b){}//死循环
    System.out.println("end");
}
public static void main(String[] args) {
    final Test_09 t = new Test_09();
    new Thread(new Runnable() {
        public void run() {
            t.m();
        }
    }).start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t.b = false;
}

但是实际上我们发现,第一条线程执行m()了以后一致处于运行状态,说明第二条线程即使修改了b=false但是对于第一条线程而言是不可见(为止)的。

volatile原理

计算机中的储存有这样几种:磁盘,内存和CPU中的高速缓存区,当一段代码执行时,首先是把磁盘的字节码文件(.class)读取加载到内存当中,在内存中形成类对象和对象,当CPU执行某条线程需要访问到对象的时候,会把与该对象相关的数据放到高速缓存区,在CPU执行中断的过程中,可能会刷新(清空)高速缓存区,进而使得CPU去读取内存中对象的资源,当然这只是一种可能的情况,假如在执行过程中CPU不中断,那么它每次访问都是从高速缓存中的数据,这样就导致一条现场在修改数据以后,CPU不能及时的更新该数据,产生不可见性。

volatile的作用就相当于一个通知,(通知OS)告诉CPU在访问某个变量的时候,每次都去内存区寻找新的数据,保证可见性。

不能保证原子性实验

我们在全局变量申请一个volatile的变量,同时编写一个方法去执行一万次该变量加1,那么我们同时开启10条线程(jion)同步去调用该方法,理论上最后的结果应该是10万,才对:

volatile int count = 0;
void m(){
    for(int i = 0; i < 10000; i++){
        count++;
    }
}
public static void main(String[] args) {
    final Test_10 t = new Test_10();
    List<Thread> threads = new ArrayList<>();
    for(int i = 0; i < 10; i++){
        threads.add(new Thread(new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        }));
    }
    for(Thread thread : threads){
        thread.start();
    }
    for(Thread thread : threads){
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println(t.count);
}

但是通过我们的实验发现最后的结果在8万左右,数据严重缺失,这是因为volatile关键字只是通知CPU在访问该数值的时候每次都去内存对象中读取,保证了可见性,但是没有保证原子性,这就导致其他线程可能同时修改成了相同的值,最终导致数据缺失。

Atomic

原子操作类型,其中的每个方法都是原子操作,可以保证线程安全。

AtomicInteger count = new AtomicInteger(0);
void m(){
    for(int i = 0; i < 10000; i++){
        //相当于++后get这个值(前++)
        count.incrementAndGet();
    }
}
public static void main(String[] args) {
    final Test_11 t = new Test_11();
    List<Thread> threads = new ArrayList<>();
    for(int i = 0; i < 10; i++){
        threads.add(new Thread(new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        }));
    }
    for(Thread thread : threads){
        thread.start();
    }
    for(Thread thread : threads){
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println(t.count.intValue());
}

CountDownLatch

门闩,代表在门上挂5把锁:

CountDownLatch latch = new CountDownLatch(5);

门闩的效率要比sync和volatile的效率要高很多,下面的m1()会等待门闩开放,当5把锁被线程2解开后,线程1就会执行m1():

void m1(){
    try {
        latch.await();// 等待门闩开放。
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("m1() method");
}
void m2(){
    for(int i = 0; i < 10; i++){
        if(latch.getCount() != 0){
            System.out.println("latch count : " + latch.getCount());
            latch.countDown(); //减门闩上的锁,每次减一个
        }
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2() method : " + i);
    }
}
public static void main(String[] args) {
    final Test_15 t = new Test_15();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m1();
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m2();
        }
    }).start();
}

很多框架中的应用广泛,比如Spring中有个ClassPathApplicationContext中init的时候,首先要创建所有的properties,然后创建Service,最后就是Bean对象,那么为了提高效率,给门闩上2把锁,当properties创建完后减1把锁,当Service创建完后再减去1把锁,最后门闩打开直接创建所有的Bean对象。

方法区

方法区中主要包含字节码和机器码,机器码包含逻辑和数据,数据有真实数据和引用数据,真实数据其实就是基础的八大类型,而引用数据是来自栈帧中的引用指向堆内存空间,所以,当线程中的引用被打断或者清空的时候,并不是影响堆内存中的数据(不会被回收)。


ReentrantLock

重入锁(jdk1.5版本后加入util.concurrent包)尽量避免sync的应用而出现的锁机制。重入锁的效率比sync的效率高,是一个轻量级的锁,但是synchronized在jdk1.5版本以后开始被优化,在jdk1.7版本之后,优化的效率已经非常好了,和ReentrantLock相比差不了多少。

使用重入锁必须要手动释放锁资源,一般都是在finally当中释放。

Lock lock = new ReentrantLock();

void m1(){
    try{
        lock.lock(); // 加锁
        for(int i = 0; i < 10; i++){
            TimeUnit.SECONDS.sleep(1);
            System.out.println("m1() method " + i);
        }
    }catch(InterruptedException e){
        e.printStackTrace();
    }finally{
        lock.unlock(); // 解锁
    }
}

void m2(){
    lock.lock();
    System.out.println("m2() method");
    lock.unlock();
}

public static void main(String[] args) {
    final Test_01 t = new Test_01();
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m1();
        }
    }).start();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(new Runnable() {
        @Override
        public void run() {
            t.m2();
        }
    }).start();
}

以上是对ReentrantLock基础使用,加锁和释放锁,两条线程虽然同时开启,但是线程2是等待线程1执行完后才开始执行的。

尝试锁

void m2(){
    boolean isLocked = false;
    try{
        // 非阻塞尝试锁, 如果有锁,无法获取锁标记,返回false。
        // 如果获取锁标记,返回true
        // isLocked = lock.tryLock();
        
        // 阻塞尝试锁,阻塞参数代表的时长,尝试获取锁标记。
        // 如果超时,不等待。直接返回。
        isLocked = lock.tryLock(5, TimeUnit.SECONDS); 
        if(isLocked){
            System.out.println("m2() method synchronized");
        }else{
            System.out.println("m2() method unsynchronized");
        }
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        if(isLocked){
            // 尝试锁在解除锁标记的时候,一定要判断是否获取到锁标记。
            // 如果当前线程没有获取到锁标记,会抛出异常。
            lock.unlock();
        }
    }
}
  • "isLocked = lock.tryLock();"非阻塞式尝试获取锁,如果有锁,无法获取锁标记,返回false。
  • "isLocked = lock.tryLock(5, TimeUnit.SECONDS); "阻塞尝试锁,阻塞参数代表的时长,尝试获取锁标记,如果超时,不等待。直接返回。

可打断

  • 阻塞状态: 包括普通阻塞,等待队列,锁池队列。
  • 普通阻塞: sleep(10000), 可以被打断。调用thread.interrupt()方法,可以打断阻塞状态,抛出异常。
  • 等待队列: wait()方法被调用,也是一种阻塞状态,只能由notify唤醒。无法打断
  • 锁池队列: 无法获取锁标记。不是所有的锁池队列都可被打断。
  • 使用ReentrantLock的lock方法,获取锁标记的时候,如果需要阻塞等待锁标记,无法被打断。
  • 使用ReentrantLock的lockInterruptibly方法,获取锁标记的时候,如果需要阻塞等待,可以被打断。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test_03 {
    Lock lock = new ReentrantLock();
    void m1(){
        try{
            lock.lock();
            for(int i = 0; i < 5; i++){
                TimeUnit.SECONDS.sleep(1);
                System.out.println("m1() method " + i);
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    void m2(){
        try{
            lock.lockInterruptibly(); // 可尝试打断,阻塞等待锁。可以被其他的线程打断阻塞状态
            System.out.println("m2() method");
        }catch(InterruptedException e){
            System.out.println("m2() method interrupted");
        }finally{
            try{
                lock.unlock();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        final Test_03 t = new Test_03();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                t.m2();
            }
        });
        t2.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        t2.interrupt();// 打断线程休眠。非正常结束阻塞状态的线程,都会抛出异常。
    }
}

公平锁

公平锁:会记录每一条线程的阻塞时间,当锁被释放之后,会优把锁分配给阻塞时间最长的线程。

首先定义一个公平锁:参数为true即可

class TestReentrantlock extends Thread{
    // 定义一个公平锁
    private static ReentrantLock lock = new ReentrantLock(true);
    public void run(){
        for(int i = 0; i < 5; i++){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName() + " get lock");
            }finally{
                lock.unlock();
            }
        }
    }
    
}

接下来启动两条现场:

public static void main(String[] args) {
    TestReentrantlock t = new TestReentrantlock();
    Thread t1 = new Thread(t);
    Thread t2 = new Thread(t);
    t1.start();
    t2.start();
}

最后的运行效果是:两条线程交替执行。一般来说,不建议使用公平锁。

ReentrantLock增加解锁条件

Condition:为Lock增加条件,当满足条件的时候可以做一些事情,例如加锁或者解锁、睡眠或唤醒。

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestContainer02<E> {
    private final LinkedList<E> list = new LinkedList<>();
    private final int MAX = 10;
    private int count = 0;
    private Lock lock = new ReentrantLock();
    //生产者条件
    private Condition producer = lock.newCondition();
    //消费者条件
    private Condition consumer = lock.newCondition();
    
    public int getCount(){return count;}
    
    public void put(E e){
        lock.lock();
        try {
            while(list.size() == MAX){
                System.out.println(Thread.currentThread().getName() + " 等待。。。");
                //释放锁标记,借助条件,进入的等待队列。
                producer.await();
            }
            System.out.println(Thread.currentThread().getName() + " put 。。。");
            list.add(e);
            count++;
            // 借助条件,唤醒所有的消费者。
            consumer.signalAll();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    //和生产者刚好相反
    public E get(){
        E e = null;
        lock.lock();
        try {
            while(list.size() == 0){
                System.out.println(Thread.currentThread().getName() + " 等待。。。");
                // 借助条件,消费者进入等待队列
                consumer.await();
            }
            System.out.println(Thread.currentThread().getName() + " get 。。。");
            e = list.removeFirst();
            count--;
            // 借助条件,唤醒所有的生产者
            producer.signalAll();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        } finally {
            lock.unlock();
        }
        return e;
    }
    public static void main(String[] args) {
        final TestContainer02<String> c = new TestContainer02<>();
        for(int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 5; j++){
                        System.out.println(c.get());
                    }
                }
            }, "consumer"+i).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        for(int i = 0; i < 2; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 25; j++){
                        c.put("container value " + j); 
                    }
                }
            }, "producer"+i).start();
        }
    }
}

可以发现Lock更加灵活,所以他是在jdk1.5版本后增加的,并非原生。

锁的底层实现

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步方法并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

对象头:存储对象的 `hashCode`、锁信息或分代年龄或 GC 标志,类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。
实例变量:存放类的属性数据信息,包括父类的属性信息。
填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
  • 栈帧在Jvm中叫做java栈(栈空间)一个java栈中有若干个栈帧,一个栈帧对应一条线程,当这条线程需要去执行同步方法的时候,那么就一定会对应一个monitor对象,当给对象加锁以后,会在对象的头信息里面记录锁的信息,该锁信息指向对应的monitor对象
  • 当其他的线程访问被锁对象的时候,发现对象头中已经记录了一把锁,那么这条线程就会放到等待队列中(处于阻塞状态或者锁池状态)
  • 当第一条线程出现异常、结束运行或者释放锁以后,那么对象头中记录的锁信息会被置空,处于阻塞状态的线程此时会被唤醒,重新争夺锁资源。
当在对象上加锁时,数据是记录在对象头中。当执行 `synchronized` 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 `monitor` 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 `monitor` 与之关联,对象与其 `monitor` 之间的关系有存在多种实现方式,如 `monitor` 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 `monitor` 被某个线程持有后,它便处于锁定状态。

在 Java 虚拟机(`HotSpot`)中,`monitor`(存在栈中) 是由 `ObjectMonitor` 实现的。

`ObjectMonitor` 中有两个队列, `_WaitSet` 和  `_EntryList`, 以及  `_Owner`  标记。其中 `_WaitSet`是用于管理等待队列(wait)线程的, `_EntryList`  是用 于管理锁 池阻塞线 程的, `_Owner` 标记用于记录当前执行线程。线程状态图如下:

当多线程并发访问同一个同步代码时,首先会进入 _EntryList ,当线程获取锁标记后,monitor 中的 _Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在 _EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行赋值为 0 计算,并将 _Owner 标记赋值为 null,代表放弃锁,执行线程进如 _WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法, _WaitSet 中的线程被唤醒,进入 _EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的 _Owner标记赋值为 null,且计数器赋值为 0 计算。

  • monitor记录的是增强锁的线程在哪。
  • monitor是有ObjectMonitor的底层对象。

锁的类型

偏向锁

是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用锁标记的形式记录锁状态。在 Monitor 中有变量 ACC_SYNCHRONIZED。当变量值使用的时候,代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护,提高效率。

轻量级锁

过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记 ACC_SYNCHRONIZED 标记记录的ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁,两个线程也可能出现重量级锁。

自旋锁

是一个过渡锁,是偏向锁和轻量级锁的过渡。当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况,称为自旋锁,自旋锁提高效率就是避免线程状态的变更。

相关文章

网友评论

    本文标题:高并发基础知识详细讲解

    本文链接:https://www.haomeiwen.com/subject/ysxvjltx.html