Version: Next

线程同步

多个线程操作同一个资源,并发问题

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候就需要线程同步
  • 线程同步是一种等待机制,多个需要同时访问此对象的线程进入线程等待池形成队列,等待前面线程使用完毕,下一个线程再使用
  • 形成条件:队列 +

线程同步:

  • 由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他资源必须等待,使用后释放锁即可,存在以下问题:
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

线程不安全案例

1. 买票

public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
//三个线程操作同一个对象
new Thread(station,"person1").start();
new Thread(station,"person2").start();
new Thread(station,"person3").start();
}
}
class BuyTicket implements Runnable {
private int ticketNumber = 10;
private Boolean flag = true;
@Override
public void run() {
//买票
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
//判断是否有票
if (ticketNumber <= 0) {
System.out.println("卖光了");
flag = false;
return;
}
//买票
//模拟办手续的延时
Thread.sleep(200);
System.out.println(
Thread.currentThread().getName() +
"买到了第" + ticketNumber-- + "张票");
}
}
person3买到了第10张票
person2买到了第8张票
person1买到了第9张票
person3买到了第6张票
person1买到了第7张票
person2买到了第5张票
person2买到了第3张票
person1买到了第3张票
person3买到了第4张票
person2买到了第1张票
person1买到了第2张票
卖光了
person3买到了第2张票

可以观察到问题,第三张和第二张都被两个人买走了

2. 不安全取钱

// 两个人去银行取钱
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"结婚基金");
GettingMoney me = new GettingMoney(account,50,"我");
GettingMoney girlFrient = new GettingMoney(account,100,"我女朋友");
me.start();
girlFrient.start();
}
}
class Account {
int money; //余额
String name; //卡明
public Account() {
}
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//模拟取款
class GettingMoney extends Thread {
Account account; //账户
//取了多少钱
int getMoney;
//现在手里的存款
int currentMoney;
public GettingMoney() {
}
public GettingMoney(Account account, int getMoney, String threadName) {
super(threadName);
this.account = account;
this.getMoney = getMoney;
}
@Override
public void run() {
//判断有没有钱
if (account.money - getMoney < 0) {
System.out.println(Thread.currentThread().getName() + "去取钱,余额不足,钱不够");
return;
}
//模拟办理延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 旧卡内余额-要取得钱
account.money -= getMoney;
//当前余额 = 旧当前余额+取到的钱
currentMoney += getMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手中的钱为:" + currentMoney);
}
}
结婚基金余额为:0
结婚基金余额为:0
我手中的钱为:50
我女朋友手中的钱为:100

设定银行里一共只有100块钱,但是我和我女朋友一人取了50,一人取了100,给取出来了,显然不对

3. 不安全集合

public class UnsafeCollection {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(10000);
System.out.println(list.size());
}
}
9888

一些线程可能被覆盖

同步方法与同步块

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:
    • synchronized方法
    • synchronized块

同步方法:

public synchronized void method(int args){}
  • synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞
  • 方法一旦执行,旧独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺陷:若将一个大的方法声明为synchronized会影响效率

同步方法的弊端

  • 方法里需要修改的内容才需要锁,锁太多,浪费资源

同步块

  • 同步块synchronized(Object){}
  • Object称为同步监视器
    • Object可以使任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是对象本身,或者是class
  • 同步监视器的执行过程
    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

将买票案例修改为线程安全的

buy()方法上加synchronized

public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
//三个线程操作同一个对象
new Thread(station,"person1").start();
new Thread(station,"person2").start();
new Thread(station,"person3").start();
}
}
class BuyTicket implements Runnable {
private int ticketNumber = 10;
private Boolean flag = true;
@Override
public void run() {
//买票
while (flag) {
try {
buy();
//模拟办手续的延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() throws InterruptedException {
//判断是否有票
if (ticketNumber <= 0) {
System.out.println("卖光了");
flag = false;
return;
}
//买票
System.out.println(
Thread.currentThread().getName() +
"买到了第" + ticketNumber-- + "张票");
}
}
person1买到了第10张票
person3买到了第9张票
person2买到了第8张票
person1买到了第7张票
person3买到了第6张票
person2买到了第5张票
person1买到了第4张票
person2买到了第3张票
person3买到了第2张票
person1买到了第1张票
卖光了
卖光了

将取钱案例修改为线程安全的

用同步块,锁account。 synchronized写在方法run()方法上的话,锁的是this,就是GettingMoney的对象。所以没有用

public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(1000, "结婚基金");
GettingMoney me = new GettingMoney(account, 50, "我");
GettingMoney girlFrient = new GettingMoney(account, 100, "我女朋友");
me.start();
girlFrient.start();
}
}
class Account {
int money; //余额
String name; //卡明
public Account() {
}
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//模拟取款
class GettingMoney extends Thread {
Account account; //账户
//取了多少钱
int getMoney;
//现在手里的存款
int currentMoney;
public GettingMoney() {
}
public GettingMoney(Account account, int getMoney, String threadName) {
super(threadName);
this.account = account;
this.getMoney = getMoney;
}
@Override
public void run() {
//模拟办理延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (account) {
//判断有没有钱
if (account.money - getMoney < 0) {
System.out.println(Thread.currentThread().getName() + "去取钱,余额不足,钱不够");
return;
}
//卡内余额 = 旧卡内余额-要取得钱
account.money -= getMoney;
//当前余额 = 旧当前余额+取到的钱
currentMoney += getMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手中的钱为:" + currentMoney);
}
}
}
结婚基金余额为:950
我手中的钱为:50
结婚基金余额为:850
我女朋友手中的钱为:100

将线程不安全集合改为线程安全集合

public class UnsafeCollection {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(10000);
System.out.println(list.size());
}
}
10000

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或多个线程都在等待对方释放资源,全部停止执行的情形。某一个同步块同时拥有「两个以上对象的锁」时,就可能发生死锁问题

// 死锁
public class Demo01DeathLock {
public static void main(String[] args) {
Makeup girl1 = new Makeup(0,"小红");
Makeup girl2 = new Makeup(1,"小美");
new Thread(girl1,"小红").start();
new Thread(girl2,"小美").start();
}
}
//口红
class Lipstick {}
//镜子
class Mirror {}
class Makeup extends Thread {
// static表示全局唯一的对象
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
//定义选择
int choice;
//定义使用化妆品的人
String girlName;
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
// 化妆
try {
makeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//互相持有对方的锁,就是需要拿到对方的资源
private void makeUp() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { //获得口红的锁
System.out.println(this.girlName + "获得了口红的锁");
Thread.sleep(1000);
synchronized (mirror) { // 一秒种后,想获得镜子的锁
System.out.println(this.girlName + "获得了镜子的锁");
}
}
} else {
synchronized (mirror) { //获得镜子的锁
System.out.println(this.girlName + "获得了镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) { // 两秒种后,想获得口红的锁
System.out.println(this.girlName + "获得了口红的锁");
}
}
}
}
}
小红获得了口红的锁
小美获得了镜子的锁

互相持有对方锁,程序卡住。

死锁的避免方法:

  • 产生死锁的四个必要条件:
    1. 互斥条件: 一个资源每次只能被一个线程使用
    2. 请求保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
    3. 不剥夺条件:线程已获得的资源,在未用完之前,不能强行剥夺
    4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

上面列出了死锁的四个必要条件,只需要想办法破除其中任意一个或多个条件,就能避免死锁的发生

Lock锁

  • 从JDK 5开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象
  • ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发行和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
  • ReentrantLock——可重入锁
public class Demo02Lock {
public static void main(String[] args) {
TestLock lock = new TestLock();
new Thread(lock, "t1").start();
new Thread(lock, "t2").start();
new Thread(lock, "t3").start();
}
}
class TestLock implements Runnable {
private int ticketNum = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
if (ticketNum > 0) {
try {
lock.lock(); // 加锁
System.out.println(Thread.currentThread().getName() + " -> " + ticketNum);
if (ticketNum > 0){
ticketNum--;
}else{
break;
}
} finally {
lock.unlock(); // 释放锁
}
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
t1 -> 10
t3 -> 9
t2 -> 8
t1 -> 7
t3 -> 6
t2 -> 5
t1 -> 4
t3 -> 3
t2 -> 2
t1 -> 1
t3 -> 0

在 Lock 接口的注释中写到:Lock 的意义在于提供了区别于 synchronized 的另一种具有更多广泛操作的同步方式,它能支持更多灵活的结构,并且可以关联多个 Condition 对象

Lock 接口中的六个方法

  • void lock() —— 获取锁,获取不到就等待
  • void lockInterruptibly() —— 获取锁,加入当前线程在等待锁的过程中被中断,则退出等待并抛出中断异常
  • boolean tryLock() —— 尝试获取锁,立即返回结果
  • boolean tryLock(long time, @NotNull TimeUnit unit) —— 在指定时间内尝试获取锁,假设中间被中断,则退出并抛出中断异常
  • void unlock()
  • Condition newCondition() —— 新建一个绑定在当前 Lock 上的 Condition 对象

Condition 表示一个等待状态,可以做精准唤醒

ReentrantLock

image-20210519182029506

ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步,同时,和 synchronzied 一样,ReentrantLock 支持可重入,除此之外 ReentrantLock 在调度上更灵活,支持更丰富的功能

public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
...
// 三个内部类
abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
}
  • ReentrantLock 只有一个属性 sync 并且是 final;三个内部类,其中第一个就是成员属性 sync 对应的类 Sync,它继承了 AQS
    • Sync 可以使用 AQS 的机制
    • Sync 是抽象类,而 NonfairSyncFairSync 是它唯二的实现子类
Sync 类 nonfairTryAcquire 方法
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 0表示 AQS 状态为空闲,可以通过CAS原子操作修改state
if (compareAndSetState(0, acquires)) { // 如果修改成功则获取了锁,将点前线程设置为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 当 state 不为0,首先判断当前线程是否已经是独占线程
// 这里就是对可重入特性的实现
int nextc = c + acquires; // 累加记录重入次数,之后释放时也需要释放相应的次数
if (nextc < 0) // overflow 将重入次数限制为 int 类型能表示的最大正数
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 来到这里表示获取锁失败
return false;
}
Sync 类 tryRelease 方法
// 返回是否完全被释放,而不是是否成功释放
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

公平锁与非公平锁

  • 公平锁:按照请求锁的顺序分配拥有稳定获取锁的机会,但是性能可能比非公平锁低
  • 非公平锁:不按照请求锁的顺序分配,不一定拥有获得锁的机会,但是性能可能比公平锁高
非公平锁实现
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 直接CAS尝试拿锁,不管前面的人,体现非公平
if (compareAndSetState(0, 1)) // 只尝试一次
setExclusiveOwnerThread(Thread.currentThread());
else // 一次没拿到就直接进入 AQS FIFO 排队
acquire(1); // 内部会调用 tryAcquire
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁实现
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1); // AQS FIFO 队列,会调用tryAcquire()
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果锁空闲
if (!hasQueuedPredecessors() && // 没有排在当前线程之前的线程
compareAndSetState(0, acquires)) { // 就 CAS,如果成功
setExclusiveOwnerThread(current); // 将当前线程设置为独占线程
return true; // 返回获取锁成功
}
}
else if (current == getExclusiveOwnerThread()) { // AQS 状态不是0,判断当前线程是不是独占线程
int nextc = c + acquires; // 记录重入次数
if (nextc < 0) // 限制重入次数
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 不是独占线程,获取锁失败
return false;
}
}

总结:ReentrantLock 如何通过 AQS 实现可重入锁

  • 公平锁:只有当 AQS FIFO 队列上,当前线程节点 与 headNode 之间没有其他线程节点时,才能尝试直接通过 CAS 获取锁;否则都要正常通过 AQS 排队(体现公平)
  • 非公平锁:上来就是一个 CAS 直接抢,但是只会尝试一次,失败就进 AQS 队列

ReetrantLock 实现 Lock 接口的方法,调用 sync 的子类实现公平锁与非公平锁,通过构造方法区分公平与非公平

通过抽象类 Sync 的引用 sync 来切换公平与非公平实现;构造方法区分公平与非公平声明
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

synchronized与Lock对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能个更好,并且具有更好的扩展性(提供更多的子类)
  • synchronized是java内置关键字,Lock是一个Java类
  • synchronized是可重入锁,不可以中断,非公平 Lock,可重入锁,非公平(可以自己设置)
  • synchronized适合少量的代码同步问题,Lock适合大量的同步代码
  • 优先使用顺序
    • Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

synchronized 锁升级过程

https://blog.csdn.net/zwx900102/article/details/106305107

对象头——持有一把锁

  • markword 32/64 bit
    • 锁标志位
  • ClassPoint : 指向方法区中的数据

synchronized 关键字在 javac 后会生成 两条字节码指令monitorentermonitorexit

Monitor —— 管程/监视器

  • 可以想象成一个只能容纳一个人的房间
  • 将想要进入 Monitor 这个房间的线程想象成人
  • 底层依赖操作系统的 mutex 指令
  • Java 线程模型采用 一对一模型 ,一个 Java 线程对应一个操作系统内核线程,频繁的线程阻塞挂起会带来频繁的内核态用户态切换,耗费资源
  • 进入队列:一堆想要访问临界区资源的线程,等待系统调度,尝试用 acquire() 方法进去
  • 房间:临界区:里面有共享资源,同一时刻只允许一个线程持有,wait() 方法会让临界区的资源释放锁,进入 等待队列notify 会唤醒一个等待的线程重新进去 进入队列 ,参与时间片竞争,notifyAll 则是唤醒一堆
  • release() 等方法就是直接退出整个管程

无锁、偏向锁、轻量级锁、重量级锁以及升级过程

  • 无锁:
    • 资源无竞争
    • 资源存在竞争,但是希望通过无锁的方式,实现同步
      • CAS——比较与交换,自旋锁,原语
  • 偏向锁:资源会被加锁,但在实际工程中只有一个线程会使用这个资源,那么希望:
    • 不使用 mutex lock 获取锁
    • 不使用 CAS 获取锁
    • 而是,对象认识这个唯一的线程
      • mark word 的倒数 2bit 为所标志位, 01 表示无锁或偏向锁
      • mark word 的倒数第3个 bit 表示是否为偏向锁,1 表示偏向锁,0 表示无锁
      • 如果是偏向锁,1 ~ 23bit 为 偏向的线程ID
      • 如果 偏向线程ID 与当前线程ID 不一致,说明有多个线程在竞争资源,则 升级为轻量级锁
  • 轻量级锁:将 mark word 的前 30bit 拿来指向栈中锁记录 Lock Record 的指针
    • 检查倒数 2bit 为 00 说明是轻量级锁
    • 在 JVM 栈中开辟一块称为 Lock Record 的空间,存放 mark word 的副本,以及 owner 指针
    • 线程通过 CAS 尝试获取对象的锁,一旦成功获得锁,就将当前 mark word 复制到 Lock Record,并让 Lock Record 中的 owner 指针指向当前对象,这样就实现了 对象 ↔ Lock Record 双射
    • 此时获得锁的线程执行自己的代码,其他想要获取资源的线程 自旋等待,自旋消耗cpu算力,默认自旋10次,可以通过指令开启自适应自旋
    • 一旦自旋等待的线程数目超过 1 个,触发 轻量级锁升级到重量级锁
  • 重量级锁:完全锁定资源,通过阻塞机制,操作系统调度实现