Java中的锁

170 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的7天,点击查看活动详情

Synchronize

概念

即俗称的对象锁,它采用互斥的方式让同一 时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

临界区与竞态条件

临界区: 一段代码块内如果存在对共享资源的多线程读写操作

竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测, 称之为发生了竞态条件

同步互斥

互斥:保证临界区竞态条件发生,同一时刻只能有一个线程执行临界区代码

同步:由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

加在方法上的synchronize相当于锁住该对象,而加在静态方法上的synchronize相当于锁住这个类(类的class对象)

synchronize锁存在于Java的对象头

对象头中的Mark Word 标记锁·的状态

synchronize一共有4种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态

无锁

没有发生竞态条件时的状态

偏向锁

锁不仅不存在多线程竞争,而且总是由同一线程多次获得。这时,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里面存储锁偏向的线程ID,以后不需要CAS来加锁和解锁,只需要检查对象头中的线程ID是否是自己即可

偏向锁撤销

偏向锁使用了一种等到竞争才释放锁的机制

  1. 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被 撤销 ,禁用偏向锁
  2. 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。

加锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录

然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则表示获得锁,如果失败就表示有其他线程竞争锁,便使用自旋来获取锁

失败后有两种情况

  1. 其他线程已经获得了该对象的轻量级锁,表明有竞争,进入锁膨胀
  2. 自己执行synchronize,锁重入,那么再添加一条LockRecord作为重入计数

解锁

轻量级解锁时,会使用原子的CAS操作将锁记录替换到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前存在锁竞争,轻量级锁已经升级为重量级锁,进入重量级锁的解锁流程

重量级锁

Monitor

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

锁膨胀

Thread-1加轻量级锁失败

  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList BLOCKED

锁膨胀解锁过程

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。
  • 这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

CAS

无锁并发,即Compare and Swap CAS操作需要两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才换成新值,发生了变化则不交换

class AccountSafe implements Account {
	private AtomicInteger balance;
	public AccountSafe(Integer balance) {
		this.balance = new AtomicInteger(balance);
	}
	@Override
	public Integer getBalance() {
		return balance.get();
	}
	@Override
	public void withdraw(Integer amount) {
		while (true) {
			int prev = balance.get();
			int next = prev - amount;
			if (balance.compareAndSet(prev, next)) {
				break;
			}
		}
		// 可以简化为下面的方法
		// balance.addAndGet(-1 * amount);
	}
}

特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。

CAS 体现的是无锁并发无阻塞并发,请仔细体会这两句话的意思

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

JUC中含有常用的原子类:原子数组原子引用原子整数

ABA问题

static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
	log.debug("main start...");
	// 获取值 A
	// 这个共享变量被它线程修改过?
	String prev = ref.get();
	other();
	sleep(1);
	// 尝试改为 C
	log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
private static void other() {
	new Thread(() -> {
		log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
	}, "t1").start();
	sleep(0.5);
	new Thread(() -> {
		log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
	}, "t2").start();
}

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程 希望: 只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号

使用AtomicStampedReference添加版本号

static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
	log.debug("main start...");
	// 获取值 A
	String prev = ref.getReference();
	// 获取版本号
	int stamp = ref.getStamp();
	log.debug("版本 {}", stamp);
	// 如果中间有其它线程干扰,发生了 ABA 现象
	other();
	sleep(1);
	// 尝试改为 C
	log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
	new Thread(() -> {
		log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", 
													  ref.getStamp(), ref.getStamp() + 1));
		log.debug("更新版本为 {}", ref.getStamp());
	}, "t1").start();
	sleep(0.5);
	new Thread(() -> {
		log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", 
													  ref.getStamp(), ref.getStamp() + 1));
		log.debug("更新版本为 {}", ref.getStamp());
	}, "t2").start();
}

AQS

队列同步器 AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int 成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中

具体api和源码看书本P121

class MyLock implements Lock {

    class MySync extends AbstractQueuedSynchronizer {

        @Override // 尝试获得锁
        protected boolean tryAcquire(int arg) {
            // 比较和设置
            if (compareAndSetState(0, 1)) {
                // 将当前的线程标记为占有该锁的线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override // 尝试释放锁
        protected boolean tryRelease(int arg) {
            // 表示没有锁占领该线程
            setExclusiveOwnerThread(null);
            // 设置状态为0
            setState(0);
            return true;
        }

        @Override // 是否持有独占锁
        protected boolean isHeldExclusively() {
            return super.isHeldExclusively();
        }

        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final MySync mySync = new MySync();

    @Override  // 加锁 不成功会进入等待队列
    public void lock() {
        mySync.acquire(1);
    }

    @Override  // 加锁 可打断
    public void lockInterruptibly() throws InterruptedException {
        mySync.acquireInterruptibly(1);
    }

    @Override // 尝试获取锁
    public boolean tryLock() {
        mySync.tryAcquire(1);
        return false;
    }

    @Override // 尝试获取锁,有超时时间
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        mySync.tryAcquireNanos(1, unit.toNanos(time));
        return false;
    }

    @Override
    public void unlock() {
        mySync.release(1);
    }

    @Override
    public Condition newCondition() {
        return mySync.newCondition();
    }
}

Lock

JavaSE 5之后,并发包中新增了Lock接口用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显示地获取和释放锁

具体API 书本P120

ReentrantLock

支持可重入的锁,它表示该锁能够支持一个线程对资源的重复加锁,除此之外,该锁的还支持获取锁的公平非公平性选择

  1. 实现可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被该锁所阻塞

static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
	method1();
}
public static void method1() {
	lock.lock();
	try {
		log.debug("execute method1");
		method2();
	} finally {
		lock.unlock();
	}
}
public static void method2() {
	lock.lock();
	try {
		log.debug("execute method2");
		method3();
	} finally {
		lock.unlock();
	}
}
public static void method3() {
	lock.lock();
	try {
		log.debug("execute method3");
	} finally {
		lock.unlock();
	}
}
  1. 锁超时
  • 立即失败
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
	log.debug("启动...");
	if (!lock.tryLock()) {
		log.debug("获取立刻失败,返回");
		return;
	}
	try {
		log.debug("获得了锁");
	} finally {
		lock.unlock();
	}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
	sleep(2);
} finally {
	lock.unlock();
}
  • 超时失败
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
	log.debug("启动...");
	try {
		if (!lock.tryLock(1, TimeUnit.SECONDS)) {
			log.debug("获取等待 1s 后失败,返回");
			return;
		}
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	try {
		log.debug("获得了锁");
	} finally {
		lock.unlock();
	}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
	sleep(2);
} finally {
	lock.unlock();
}
  1. 锁打断
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
	log.debug("启动...");
	try {
		lock.lockInterruptibly();
	} catch (InterruptedException e) {
		e.printStackTrace();
		log.debug("等锁的过程中被打断");
		return;
	}
	try {
		log.debug("获得了锁");
	} finally {
		lock.unlock();
	}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
	sleep(1);
	t1.interrupt();
	log.debug("执行打断");
} finally {
	lock.unlock();
}
  1. 公平锁

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO

ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0; i < 500; i++) {
	new Thread(() -> {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + " running...");
		} finally {
			lock.unlock();
		}
	}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
	System.out.println(Thread.currentThread().getName() + " start...");
	lock.lock();
	try {
		System.out.println(Thread.currentThread().getName() + " running...");
	} finally {
		lock.unlock();
	}
}, "强行插入").start();
lock.unlock();
  1. 支持多条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
	new Thread(() -> {
		try {
			lock.lock();
			while (!hasCigrette) {
				try {
					waitCigaretteQueue.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			log.debug("等到了它的烟");
		} finally {
			lock.unlock();
		}
	}).start();
	new Thread(() -> {
		try {
			lock.lock();
			while (!hasBreakfast) {
				try {
					waitbreakfastQueue.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			log.debug("等到了它的早餐");
		} finally {
			lock.unlock();
		}
	}).start();
	sleep(1);
	sendBreakfast();
	sleep(1);
	sendCigarette();
}
private static void sendCigarette() {
	lock.lock();
	try {
		log.debug("送烟来了");
		hasCigrette = true;
		waitCigaretteQueue.signal();
	} finally {
		lock.unlock();
	}
}
private static void sendBreakfast() {
	lock.lock();
	try {
		log.debug("送早餐来了");
		hasBreakfast = true;
		waitbreakfastQueue.signal();
	} finally {
		lock.unlock();
	}
}

ReentrantReadWriteLock

ReentrantReadWriteLock,读写锁,允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞,即读读并发读写互斥写写互斥

在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量

  1. 公平性选择
  2. 可重入

读线程可以重入读锁,而写线程可以重入读锁和写锁

  1. 锁降级

指当前线程拥有写锁,再获取到读锁,然后将写锁释放的过程

class Container {

    // 数据
    private int data;

    // 读写锁
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 读锁
    private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

    // 写锁
    private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    public Container(int data) {
        this.data = data;
    }

    public int read() {
        readLock.lock();
        System.out.println(Thread.currentThread());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            return data;
        } finally {
            readLock.unlock();
        }
    }

    public void write(int data) {
        writeLock.lock();
        try {
            this.data = data;
        } finally {
            writeLock.unlock();
        }
    }
}

LockSupport

LockSupport工具类

void park() :阻塞当前线程

void unpark(Thread thread):唤醒处于阻塞状态的线程