【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
作者:Mr.Jimmy
出处:https://www.cnblogs.com/JHelius
联系:yanyangzhihuo@foxmail.com
如有疑问欢迎讨论,转载请注明出处