【Java多线程】synchronized、ReentrantLock基础原理

什么是多线程?

在执行代码的过程中,我们很多时候需要同时执行一些操作,这些同时进行操作可以尽可能的提升代码执行效率,充分发挥CPU运算能力。

public class Test implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "_" + i);
        }
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        Thread ta = new Thread(t1, "tbA");
        Thread tb = new Thread(t1, "tbB");
        ta.start();
        tb.start();
    }
}

输出:

tbA_0
tbB_0
tbA_1
tbB_1
tbB_2
tbA_2

为什么使用多线程?

  • 分离单一逻辑
  • 提高代码效率
  • 充分发挥硬件能力

多线程的代价?

在多个线程同时需要访问同一对象时,会出现意料之外的结果。

public class Test implements Runnable {
    private static Integer i = 0;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "的赛道:" + (++i));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("----4位选手进入赛道----");
        Thread thread1 = new Thread(t1, "tbA");
        Thread thread2 = new Thread(t1, "tbB");
        Thread thread3 = new Thread(t1, "tbC");
        Thread thread4 = new Thread(t1, "tbD");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

输出:

----4位选手进入赛道----
tbA的赛道:1
tbD的赛道:3
tbB的赛道:1
tbC的赛道:2

选手A和B进入了同一赛道,这不是我们期望的。

怎么保证线程安全?

为对象加锁,可以最大可能保证线程安全。
线程锁有哪些?
最常用的是synchronized,除此之外还有ReentrantLock

synchronized锁原理

两个概念

CAS

是一个CPU指令,三个参数:地址,原始值,新值,返回一个bool型。

function cas(p , old , new ) returns bool {
    if *p ≠ old { // *p 表示指针p所指向的内存地址
        return false
    }
    *p ← new
    return true
}

Mark Word

Java对象头的Mark Word中存储了HashCode、分代年龄、锁状态等信息。

三种锁

三种锁依次完成了三种设想下的线程安全保障。

起初我们悲观的认为,几乎所有的多线程访问对象都可能存在并发竞争,需要阻塞竞争的线程以已达到在同一时间只有一个线程访问对象。这就是synchronized最初设计的重量级锁。

重量级锁

使用操作系统的互斥量实现。在用户态与内核态切换,需要消耗较大性能。

内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

轻量级锁

轻量级锁的诞生源于我们的比较乐观的设想——部分多线程在访问对象时可能是串行的竞争关系。
串行竞争是指,虽然多线程依然存在竞争,但不是同时访问,而是依次的。我们可以让线程按照先来后到的顺序有序访问。

流程:
1.对象(OBJ)被创建,它目前没有被任何线程占用
2.线程1尝试访问OBJ,通过OBJ的头标记(Mark Word)内记录的信息,发现他没有被任何线程占用
3.线程1将OBJ的Mark Word拷贝至自己栈帧的锁空间(Lock Record)内,我们称它为置换标记(Displaced Mark Word),并通过CAS将原OBJ的Mark Word内所指针指向线程1的Lock Record
4.线程1开始占用OBJ,并执行自己内部操作
5.线程2尝试访问OBJ,通过OBJ的Mark Word发现已被线程1占用
6.线程2开始执行自旋锁,进入循环等待,每隔一段时间尝试通过CAS判断OBJ是否被占用,如果是则继续循环等待,如果否则占用OBJ,自旋尝试有次数限制(默认10,可以通过JVM调优修改)
7.线程1执行完毕,通过CAS将自己Displaced Mark Word拷贝至 OBJ的Mark Wrod,进行复原,释放OBJ
8.线程2在自旋锁执行过程中发现OBJ已经被线程1释放,执行第3步操作占用OBJ
9.线程3尝试访问OBJ...

偏向锁

轻量级锁已经极大优化了重量级锁阻塞带来的负担。但很快,我们又想到另一种更乐观的多线程情况——只有一个线程多次访问某个对象。
在这种情况下,甚至没有第二个线程,只有唯一的一个线程不断的访问对象。不存在其他线程竞争,不存在等待。

流程:
1.线程1尝试访问OBJ,通过其头标记(Mark Word)发现其没有线程占用且可以设置偏向锁(Mark Wor中有一个标识代表是否可以设置偏向锁)
2.线程1通过CAS占用OBJ,并执行自己的操作
3.线程1再次尝试访问OBJ,发现Mark Word中记录的是自己的信息,则直接访问OBJ执行操作

锁之间的关系

轻量级锁及偏向锁是较乐观的情况,但如果出现了不那么乐观的特殊情况怎么办?

一般synchronized锁会默认执行偏向锁,但在执行过程中发现有其他线程竞争,自动膨胀至轻量级锁。但当执行多次自旋锁都没法争取对象时,将自动膨胀至重量级锁。

ReentrantLock锁原理

ReentrantLock的原理是通过CAS及AQS队列搭配实现。

AQS
AQS使用一个FIFO的队列(也叫CLH队列,是CLH锁的一种变形),表示排队等待锁的线程。队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

流程:
1.线程1尝试访问OBJ,通过AQS队列的state属性发现其没有被占用(state=0)
2.线程1占用OBJ,设置AQS的state=1,并设置其AQS的thread为当前线程(线程1)
3.线程2尝试访问OBJ。发现其已被占用(状态为1),加入AQS的等待队列
4.线程1执行完毕,释放OBJ
5.位于AQS等待队列最前的线程2开始尝试访问OBJ...

代码

了解了锁的原理,让我们用实际代码解决刚开始遇到的多线程问题。

synchronized方式:

public class Test implements Runnable {
    private static Integer i = 0;
    @Override
    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "的赛道:" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("----4位选手进入赛道----");
        Thread thread1 = new Thread(t1, "tbA");
        Thread thread2 = new Thread(t1, "tbB");
        Thread thread3 = new Thread(t1, "tbC");
        Thread thread4 = new Thread(t1, "tbD");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

输出:

----4位选手进入赛道----
tbA的赛道:1
tbD的赛道:2
tbB的赛道:3
tbC的赛道:4

ReentrantLock方式:

public class Test implements Runnable {
    private static Integer i = 0;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "的赛道:" + (++i));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("----4位选手进入赛道----");
        Thread thread1 = new Thread(t1, "tbA");
        Thread thread2 = new Thread(t1, "tbB");
        Thread thread3 = new Thread(t1, "tbC");
        Thread thread4 = new Thread(t1, "tbD");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

输出与上一个一致。

参考文献: https://www.cnblogs.com/maxigang/p/9041080.html
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://zhuanlan.zhihu.com/p/249147493

posted @ 2022-05-27 10:49  Hi-Jimmy  阅读(50)  评论(0编辑  收藏  举报