并发基础-锁

129 阅读2分钟

  1. 什么是锁?锁是用来解决原子性的问题,通过在临界区代码前面分别对需要操作的资源加锁和解锁,解决并发问题。临界区代码是同步执行的
  2. 锁的实现:在锁对象的对象头里面写入当前线程id
  3. 锁与资源关系:1:N.即一把锁能保护多个资源(粗粒度锁),但不能多把锁保护一个资源
  4. 一把锁 去锁 一个资源是没有问题的
    案例1:转账 image.png

死锁

  1. 什么是死锁?:一组互相竞争资源的线程互相等待,导致“永久”阻塞的现象
  2. 锁的粒度:锁的范围越大(锁住的对象越多),粒度越粗,性能越不好。
  3. 死锁案例:账户A向账户B转账,我们有两种锁的方案:一、锁住Class对象(粒度粗,关于这个类的所有对象转账操作都变成串行);二、先后锁住账户A和账户B,但可能导致死锁
  4. 死锁产生的必要条件:
    1. 互斥:共享资源X和Y只能被一个线程占用
    2. 占有且等待:线程T1已经取得了共享资源X,在等待共享资源Y时,不释放共享资源X
    3. 不可抢占:其他线程不能抢占线程T1已经占有的资源
    4. 循环等待:线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源
  5. 避免死锁:通过破坏四个必要条件之一
    1. 破坏互斥:无法破坏,因为我们使用的是synchronized互斥锁
    2. 破坏占有且等待:一次性申请所有资源
      实现案例:通过在内部创建一个单例的Allocator对象去申请资源,只有申请成功,才进行下一步
    // Account.java
    public class Account {
        private Allocator allocator = Allocator.getInstance(); //单例
        private int balance; // 余额
        Account(int m){
            balance = m;
        }
        /**
         * 通过 一次性申请所有资源来解决死锁
         */
        public void transferByAllocator(Account target, int money) throws InterruptedException {
            // 申请不到会阻塞进程
            allocator.apply(this, target);
            // 如果只有转账,这里也可以不需要加synchronized。因为由于apply(),只有一个线程执行
            synchronized(this){
                synchronized(target){
                    if(this.balance >= money){
                        target.balance += money;
                        this.balance -= money;
                        allocator.release(this, target);
                    }
                }
            }
        }
    }
    
    //Allocator.java
    public class Allocator {
        List<Object> als = new ArrayList<>();
        private static volatile dy.practice.Allocator allocator;
        private Allocator(){
        }
    
        public static Allocator getInstance(){
            if(allocator == null){
                synchronized (Allocator.class){
                    allocator = new Allocator();
                }
            }
            return allocator;
        }
    
        //一次性抢占资源
        public synchronized void apply(Object source, Object target){
            // 经典写法
            while(als.contains(source) || als.contains(target)){
                //所有线程抢占this锁,当第一个线程申请this锁,然后执行完apply()(执行完成,自动释放this锁)。
                //其他线程才会抢占this锁,执行进入aplly(),如果第一个线程还没有执行release(), 那么这些线程会满足条件从而执行wait()(释放this锁)。
                //直到第一个线程执行notifyAll(),从其他线程唤醒某个线程,执行apply()方法剩下的
              try{
                wait();
              }catch(Exception e){
              }
            } 
            als.add(source);
            als.add(target);
        }
    
        // 归还所有资源
        public synchronized void release(Object source, Object target){
            als.remove(source);
            als.remove(target);
            this.notifyAll();
        }
    }
    
    1. 破坏不可抢占:占有资源的线程进一步申请资源时,如果申请不到,可以主动释放已经持有的资源。synchronized锁无法主动释放(申请不到,直接阻塞),Lock锁才能主动释放。
    2. 破坏循环等待:将资源进行线性排序,线程按照顺序申请。 案例
    public class Account {
      private int id;
      private int balance;
      // 转账 
      void transfer(Account target, int amt){
        Account first = this;
        Account second = target
        if (this.id > target.id) {
          first = target;
          second = this;
        }
        // 锁定序号小的账户
        synchronized(first){
          // 锁定序号大的账户
          synchronized(second){ 
            if (this.balance > amt){
              this.balance -= amt;
              target.balance += amt;
            }
          }
        }
      } 
    }
    

活锁

饥饿