并发:互斥锁

为了保证某段程序的原子性,需要使得同一时刻只有一个线程执行,这就是互斥

一段需要互斥执行的代码称之为临界区

线程进入临界区之前,首先试图执行加锁操作lock(),如果成功加锁,则进行临界区执行,此时该线程持有锁;否则就等待其他进程释放锁;持有锁的线程离开临界区后需要释放锁unlock()。

我们都知道锁是用来保护临界资源的,也就是共享变量、共享文件等资源,那么锁的是什么呢?

1、synchronized关键字

Java中的synchronized关键字是java在语言层面提供的互斥原语,是锁的一种实现。synchronized既可以用来修饰方法,也可以只修饰某一段代码块。

比如:

class Clazz{
  //synchronized修饰非静态方法
  synchronized void fun1(){
    //临界区
  }
  
  //synchronized修饰静态方法
  synchronized static void fun2(){
    //临界区
  }

  Object obj = new Object();
  void fun3(){
    //synchronized修饰代码块
    synchronized(obj){
      //临界区
    }
  }
}

java编译器在synchronized修饰的方法或代码块前后自动加上了加锁lock()和解锁unlock(),这样就可以保证加锁和解锁是成对出现的,防止用户在使用时漏写其中的一个。

那synchronized这把锁到底锁了什么呢?

  • 当synchronized修饰static方法(类方法或者静态方法)时,锁的是当前类的Class对象,也就是本例中的Clazz类 ;
  • 当synchronized修饰实例方法(非静态方法)时,锁的是当前这个实例对象this;
  • 当synchronized修饰代码块时,锁的就是synchronized后面括号中放入的对象,也就是本例中的obj。

2、锁有何限制

对于一段临界资源,只可以有一把锁。锁和受保护资源的关系应该是1 : N。这是合理的,因为如果使用不同的锁去保护某一临界区,那么将会使得多个线程同时持有锁,然后同时进入临界区。

比如:

class Test{
  
  public void fun(){
    synchronized(new Object()){
      //临界区
    }
  }
  
}

上述代码中每次有线程进来,都会使用new创建一个Object对象作为临界区的锁,等于没有上锁。

在一些特殊情况下,多个临界区也可以使用同一把锁。

1)不建议使用同一把锁保护多个资源的情况

比如,对两个相互独立的方法采用同一把锁会导致程序不够精细化,性能太差。

public class Accout{
  private Integer balance;
  private String password;
  private Object obj = new Object();
  
  public void withdraw(Integer amt){
    synchronized(obj){
      if(balance >= amt){
        balance -= amt;
      }
    }
  }
  
  public void updatePassword(String pwd){
    synchronized(obj){
      password = pwd;
    }
  }
}

上述代码中,取款withdraw方法和更新密码updatePassword没有关系,但被强制使用了同一把锁,也就是说同一个实例不能同时取款和更新密码,显然效率比较低,这时建议使用不同的锁。

2)必须使用同一把锁保护多个资源的情况

比如转账操作的一致性问题,账户A给账户B转账100元,A减少100元,B增加100元,这两个操作涉及到两个临界资源,分别是A的余额和B的余额。

那假如使用以下方式加锁:

public class Account{
  private Integer balance;
  
  synchronized void transfer(Integer amt, Account target){
    if(this.balance >= amt){
      this.balance -= amt;
      target.balance += amt;
    }
  }
}

账户A和账户B是两个不同的实例,因此这两个实例的transfer方法的锁就是这两个实例本身。于是就出现了两把锁,也就是说A在给B转账的同时,B也可以给C转账。

在这里插入图片描述

初始时刻,账户A、B、C余额都是200。线程1和线程2同时读了B的初始余额200,如果线程2先将B的余额减100得到100,再写回内存,然后线程1将B的余额加100得到300,再写回内存,导致最后B的余额是300,而实际上应该是200。

因此,我们需要使用同一把锁来保护多个临界资源。比如使用Account类的Class对象作为transfer方法的锁,然而这样做的性能会很差,因为这样做会导致所有的转账操作都是串行的,而账户A给账户B转账,账户C给账户B转账,实际上是可以同时进行的。所以,将Class对象作为锁的粒度太大了,还需要探讨更细粒度的锁。

比如可以使用两把锁:

public class Account{
  private Integer balance;
  
  void transfer(Integer amt, Account target){
    synchronized(this){
      synchronized(target){
        if(this.balance >= amt){
      		this.balance -= amt;
      		target.balance += amt;
    		}
      }
    }
  }
}

这样就可以保证账户A在给账户B转账的时候,账户B不可以转账。但是不会限制账户C给账户D转账。

但这样做也不是万无一失的,因为可能会出现死锁,也就是线程1和线程2同时各自获取了this和target作为其第一把锁,那获取第一把锁时就会陷入永久等待。对于死锁的处理,不是本文的重点,因此不深入介绍。

3)不能把可变对象当做锁

比如在取款方法withdraw中,不可以使用this.balance作为锁。因为当线程A执行该方法,balance -= amt,balance的值发生变化,balance指向了别的对象。那么接下来线程B来执行withdraw方法时就会拿到另一把锁,此时两个线程同时在临界区中执行。

参考资料:
《操作系统精髓与设计原理》
《java并发编程实战》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值