java 锁

话术

话术 解释
race condition 竞争条件,指的是多线程访问共享数据时产生的竞争
unconsistency 数据的不一致问题,在并发环境下产生了非期望的结果
monitor
critical section 临界区, 就是被锁的代码区域
  • 如果临界区执行时间比较长,叫做锁的粒度比较粗,反之 就是锁的粒度比较细
  • 如果保证线程一致性? 线程同步
  • 锁其实就是保证代码的原子性

保证原子性的方案

悲观锁

  • 认为代码一定会被别人打断
  • 会无脑上锁(如synchronized就是悲观锁)

乐观锁(cas,无锁,自旋锁)

  • 默认不会有人沟通,如下图
    image-1650966095854

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 上有锁就等待。

修饰在方法上

  • 在方法上的锁,以当前类作为对象锁定,区别是如果这个类两个方法都加了锁,那么这两个方法则持有的是同一把锁。

问题

  1. 加了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 强大的地方
  1. tryLock 尝试加锁(也可以加时间),如果加锁失败也可以继续执行,具体看你的逻辑,返回boolean.
lock.tryLock(5, TimeUnit.SECONDS);
  1. 获取队列长度等操作,比synchronized更灵活。
  2. lockInterruptibly 表示这个锁可以被其他线程打断。其他线程只要调用interrupt打断这把锁。
  3. 可以设置为公平锁,谁在前面谁持有这把锁,默认为非公平的锁。
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");
    }

image

  • 上述代码的意思是当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();
    }

image-1651664828486

  • 执行结果如上,他是一个交换机 他会交换两个线程的传参。
  • 当第一个线程执行了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);
    }

image-1651666119506

  • 上述代码的意思是当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");
    }

image-1651666301796

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