java 锁
话术
话术 |
解释 |
race condition |
竞争条件,指的是多线程访问共享数据时产生的竞争 |
unconsistency |
数据的不一致问题,在并发环境下产生了非期望的结果 |
monitor |
锁 |
critical section |
临界区, 就是被锁的代码区域 |
- 如果临界区执行时间比较长,叫做锁的粒度比较粗,反之 就是锁的粒度比较细
- 如果保证线程一致性? 线程同步
- 锁其实就是保证代码的原子性
保证原子性的方案
悲观锁
- 认为代码一定会被别人打断
- 会无脑上锁(如synchronized就是悲观锁)
乐观锁(cas,无锁,自旋锁)
- 默认不会有人沟通,如下图

ABA问题
- 原值为1 修改为8 ,又有其他线程修改回了1,大多数情况这个问题不用解决,因为别人改成的1和原值的1没有什么本质区别。
- 解决方法是使用 version ,两种做法1:用boolean 2:用时间戳 3:自增序列
原子性问题
if(version == 1){
xxxxxxxxx
}
- 存在一种情况if 的时候version 没发生改变,执行操作的时候version 的值被改了。
- 解决方案: 使用Atomic类,它可以保证原子性,具体原理因为是native 的就不深入研究了
AtomicInteger atomic = new AtomicInteger(0);
乐观锁与悲观锁效率哪个更高?
- 并非乐观锁 效率一定比悲观锁高
- 悲观锁等待锁的任务都是不消耗cpu资源的,而乐观锁都是在循环判断是否轮到他了(乐观锁会消耗cpu)
- 等待的线程越多,临界粒度越大的应该使用悲观锁,反之使用乐观锁。
- 实战一般使用synchronized 因为jvm 对他做过了非常多的优化。
synchronized
- 自动保障可见性,synchronized底层实现,它在解锁后,会把本地缓存做同步。
修饰在对象上
Object o = new Object();
synchronized(o){
xxxxxxx
}
- 代表每次运行到synchronized 都去check这个o,如果o 上有锁就等待。
修饰在方法上
- 在方法上的锁,以当前类作为对象锁定,区别是如果这个类两个方法都加了锁,那么这两个方法则持有的是同一把锁。
问题
- 加了synchronized还有必要加volatile吗?
- 没必要因为synchronized 既保证了可见性,又保证了原子性。
可重入
class T{
public void synchronized m1(){
m2();
}
public void synchronized m2(){
xxxxxx
}
- 当两个方法加了同一把锁,他的调用是不会受到阻碍的,这个叫做可重入。
异常和锁
- 执行程序时,如果发生了异常 锁会被释放,导致的问题是 其他等待锁的任务会乱入,会导致数据不一致问题。
synchronized底层实现
- 最早期jvm 的synchronized 调用操作系统的锁,效率非常差!
####后期升级
- 第一个线程访问,其实不会加锁只是记录了线程的id(此时是偏向锁,再有方法来访问如果还是这个线程直接放行)
- 如果有另外的线程争用升级为自旋锁(占用cpu)。
- 如果旋10次以后锁还没有被释放则升级为重量级锁(此时是操作系统的锁不会占用cpu)。
优化
也不是所有情况都尽量小
- 比如一个方法调用了20个方法,有20把小锁,这种场景还不如一把大锁性能更高。
其他常用的锁
AtomicInteger /AtomicLong 等
- 底层是cas 锁的实现
- 他的效率比 synchronized(Object lock){count++}性能更高。
LongAddr
- 作用和AtomicLong ,synchronized(Object lock){count++} 作用是类似的。
- 经过测试在并发数特别高的时候性能优于 AtomicLong。
- 原因是LongAddr 使用了分段锁,他会把LongAddr 分成不同的段,之后再累加。
ReentrantLock 可重入锁
- 指的是同一个线程下,同一把锁 是可以互相通过的,比如m1 有把🔒,调用m2 调同一把🔒时 他是没有问题的。
- synchronized 也是可重入锁。
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.lock(); /
try {
System.out.println("我是一段代码");
}catch (Exception exception){
exception.printStackTrace();
}finally{
lock.unlock();
}
}
- 必须写try catch 因为他不会自动解锁,并且 lock必须紧跟try,否则可能中途有异常,这个锁就解不掉了。
- 比synchronized 强大的地方
- tryLock 尝试加锁(也可以加时间),如果加锁失败也可以继续执行,具体看你的逻辑,返回boolean.
lock.tryLock(5, TimeUnit.SECONDS);
- 获取队列长度等操作,比synchronized更灵活。
- lockInterruptibly 表示这个锁可以被其他线程打断。其他线程只要调用interrupt打断这把锁。
- 可以设置为公平锁,谁在前面谁持有这把锁,默认为非公平的锁。
ReentrantLock lock = new ReentrantLock(true); //设置为true 这把锁是公平锁。
公平锁
- 谁先进去,这把锁就谁持有。
- 因为jvm 的设定,直接抢锁是有可能抢到的,比如a 正好解锁了,b线程去lock 是有可能抢到锁的,不论 等待队列中是否还有锁。
- 如果是公平锁,则会在lock 之前check 队列中是否还有元素处于锁的等待。
CountDownLatch 锁
public static void main(String[] args) throws InterruptedException {
var count = 100;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
new Thread(() -> {
try{
System.out.println("doSomething");
}catch(Exception e){
e.printStackTrace();
}finally{
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println("end");
}
- countDown 操作也必须写在finally 里否则会在awiat的地方一直block住。
CyclicBarrier 栅栏类
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(20,() -> System.out.println("人数满了"));
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
System.out.println("start");
}

- 上述代码的意思是当cyclicBarrier 被调用20次 每当被调用20次await 以后,则调用其初始化时,传入的runable回调。
- 参考核酸检测,10人一组的采样,和这个对象的玩法就很像。
ReadWriteLock 读写锁
- 共享锁(Read)
- 排他锁(Lock)
- 当读线程持有锁的时候允许其他读线程进来读,因为都是读取无所谓,没必要锁定,但是不允许写线程进来写数据
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public void read(Lock lock){
try {
Thread.sleep(1000);
System.out.println("read");
}catch(Exception e){
}finally{
lock.unlock();
}
}
public void write(Lock lock){
lock.lock();
try {
Thread.sleep(1000);
System.out.println("write");
}catch(Exception e){
}finally{
lock.unlock();
}
}
Semaphore 信号灯锁
public static void main(String[] args) {
var semaphore = new Semaphore( 2); //数字代表可以同时执行的数量
new Thread(() -> {
try {
semaphore.acquire(); //阻塞方法
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release(); //释放信号灯,其他线程就可以往下走了
}
});
}
- 等于 Semaphore 上有n 把锁
- acquire尝试获取锁,如果锁被拿完了,就阻塞住。
- 应用场景,限流,最多允许多少个线程并发执行。
Exchanger 交换器
public static void main(String[] args) {
var exchanger = new Exchanger<Long>();
new Thread(() -> {
try {
var res = exchanger.exchange(1L);
System.out.println("first:"+ res);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
var res = exchanger.exchange(100L);
System.out.println("second:"+ res);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}

- 执行结果如上,他是一个交换机 他会交换两个线程的传参。
- 当第一个线程执行了exchange时,exchange方法会阻塞住。
LockSupport 当前线程阻塞
public static void main(String[] args) throws InterruptedException {
var t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5) {
LockSupport.park();
}
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
TimeUnit.SECONDS.sleep(8L);
System.out.println("unpark");
LockSupport.unpark(t);
}

- 上述代码的意思是当i==5时阻塞线程。
- 当调用 unpark 那个线程时,才能恢复这个线程。
public static void main(String[] args) throws InterruptedException {
var t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5) {
LockSupport.park();
}
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
LockSupport.unpark(t);
TimeUnit.SECONDS.sleep(8L);
System.out.println("unpark");
}

- 需要注意一点unpark 可以先于park 调用,提前解锁。
- 测试发现必须要在线程处于运行中调用unpark才有效,否则线程走到i==5会死锁。
- 用法上比await 更灵活,因为他可以提前unpark.