MyBatis idea 常用快捷键 reactjs null jqgrid vue绑定class 河南网络推广 广告投放系统源码 java清空数组 matlab求向量的模 python随机数 python生成随机数 python编译环境 python开发工具 python程序 python安装模块 java中string java中substring java获取当前月份 java8的新特性 java继承关键字 java自学教程 java初学者 java架构 java连接sql linux命令行大全 linux简介 linuxshell编程 html实例教程 mounted 网络是怎样连接的 din字体下载 防沉迷助手 完美手游模拟器 橄榄山快模 陌陌电脑直播设置教程 惠普战99 polyworks 加速软件 ps高手教程
当前位置: 首页 > 学习教程  > 编程语言

并发:互斥锁

2020/11/4 14:07:48 文章标签:

为了保证某段程序的原子性,需要使得同一时刻只有一个线程执行,这就是互斥。 一段需要互斥执行的代码称之为临界区。 线程进入临界区之前,首先试图执行加锁操作lock(),如果成功加锁,则进行临界区执行,此时…

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

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

线程进入临界区之前,首先试图执行加锁操作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并发编程实战》


本文链接: http://www.dtmao.cc/news_show_350082.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?