软件构造第十章

java线程锁

线程安全

  • 线程安全:多个线程使用时行为正确,无论这些线程如何执行,无需额外协调
  • 原则:并发程序的正确性不应取决于随机
  • 四种并发安全策略:
    – 各个线程之间不共享对象
    – 各个线程之间共享不可变类型
    – 使用线程安全数据类型
    – 使用Synchronization:防止多个线程同时访问共享数据

  • 锁是一种抽象,对于某个类型或对象,某时刻最多只允许一个线程拥有锁,其他线程不能使用上了锁的类型
  • 锁的两个操作
    – 获取锁的拥有权,如果锁被其他线程拥有,将进入阻塞状态,等待锁释放后, 再同其他线程竞争获取锁的拥有权
    – 释放放弃锁的所有权,允许另一个线程获得它的所有权
  • 锁的作用
    – 使用锁还会告诉编译器和处理器您正在使用共享内存,以便将寄存器和高速缓存刷新到共享存储。锁机制可以确保锁的拥有者始终查看最新的数据,避免了 reordering问题(出于优化目的,编译器和处理器会复制变量的临时副本在高速 存储中,在存储回正式内存位置之前,基于临时副本工作。存储回内存的顺序,可能与代码中操作的变量顺序不同,所以可能会出现和预期不符的现象,称作reordering问题)
    – 阻塞一般意味着线程等待(不再 继续工作),直到一个事件发生(其他线程释放锁)
  • 可以使用synchronized 实现锁的功能

Synchronization

Java将锁定机制作为内置语言特性提供,每个类及其所有对象实例都有一个锁,Object类也有锁,经常用于显示地定义锁,常常使用synchronized语句获取语句块持续时间的锁。

synchronized statements /block 同步语句/同步代码块

  • Synchronized 语句必须指定提供内部锁的对象。
  • Synchronized区域提供了互斥功能: 一次只能有一个线程处于由给定对 象的锁保护的同步区域中
  • 锁用于保护共享数据变量,确保操作原子性
  • 锁定时,遵循顺序执行模式
  • 在线程t中,会阻止其他线程进入保护块中,直到语句块中代码执行完
  • 它阻止其他线程进入它们自己的synchronized(expression)块,(其中expression表示expression引用的对象)直到线程t完成其synchronized块。注意:锁只能确保与其他请求获取相同对象锁的线程互斥访问
  • 当不确定对哪个对象加锁但需要阻碍线程执行的时候,可以考虑零长度的byte数组。零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
    在这里插入图片描述
    如图,当两个线程A,B几乎同时执行方法fun的时候,假设A先执行,由于两个线程都对aInteger对象使用了synchronized(可以判断,由于aInteger是静态的,所以两个线程的aInteger是同一个对象),则只有当A执行完int i = 0; i += 1; System.out.print(i);之后,B线程才会开始执行。
public class hello implements Runnable{
  
  static Integer  aInteger= new Integer(1001);
  
  private void fun() {
    synchronized (aInteger) {
      int i = 0;
      i += 1;	
      System.out.print(i);
    }
  }
  @Override
  public void run() {
    fun();
  }
}

synchronized 修饰方法

  • 使用synchronized修饰方法的效果等同在方法体里面使用synchronized(this)
  • 当线程调用同步方法时,它会自 动获取该方法所在对象的内部锁,并在方法返回时释放它。即使返回是由未 捕获的异常引起的,也会释放锁。
  • 同一对象上的同步方法的两次调用不会有交叉现 象
  • 注意:当一个同步方法退出时,会保证对象状态的更改 对所有线程都是可见的。
    – synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。
    – 定义接口方法时不能使用synchronized关键字
    – 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步

synchronized修饰静态方法

同一类的不同对象的被synchronized修饰的静态方法都可以阻塞

synchronized按类阻塞

同synchronized代码块类似。只不过是当两个线程执行碰到类的时候就可以阻塞。如下图

  private void fun() {
    synchronized (Integer.class) {
      int i = 0;
      i += 1;
      System.out.print(i);
    }
  }

  @Override
  public void run() {
    fun();
  }

锁规则是确保同步代码是线程安全的策略。需要注意,每个共享的可变变量必须由某个锁保护,如果一个不变量涉及多个共享的可变变量(甚至可能在不同的对象中),那么涉及的所有变量都必须由相同的锁保护。

原子操作

  • 让数据类型的锁对客户端可用,以实现更高级的原子操作,下面是个例子

首先我们有在这里插入图片描述
其中EditBuffer通过monitor pattern(使用synchronized(this){} 包围)已经确保了每个方法(toString、delete和 insert)的原子性,但整体来说,这个findReplace方法不具有原子性。
改进方法为在方法体里面上一把锁(此处可以给buf上锁),代码如下
在这里插入图片描述

  • 另外,使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写入都会在之后与同一变量的读取之间建立关系。
    – volatile变量有一下性质:
    1、volatile变量的更改,对其他线程总是立即可见的
    2、禁止进行指令重排序,避免了reordering带 来的问题。。基本原理:每次使用此类变量时都到主存中进行读取,而且当成员变量 发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两 个不同的线程总是看到某个成员变量的同一个值。
    – volatile的优势:降低内存一致性错误的风险;速度更快
    – 缺点:volatile 不能提供必须的原子 特性,只能在有限的一些情形 下使用 volatile变量替代锁: 对变量的写操作不依赖于当前 值,变量的有效值独立于任何 程序的状态,包括变量的当前 状态。

synchronized 的使用

  • synchronized 修饰方法的缺点
    – 同步开销大
    – 需要时间申请锁,刷新缓存,同其他线程通讯
    – 将同步添加到每个方法意味着锁是对象本身,并且每个 引用对象的客户端都会自动拥有锁的引用,导致他们可以随意获取和释 放锁 。线程安全机制因此是公开的,可能会受到客户的干扰 。
    – 同步方法意味着正在获取一个锁,而不考虑它是哪个锁,或者 它是否是保护你将要执行的共享数据访问的正确锁。
    – 对静态方法加锁,将使用其所在类的锁,将对该类所有对象生效。这样导致一次只有一个线程可以调用该方法。
  • 相比而言,使用某内部对象的锁,并使用synchronized语句块进行适当 的获取和使用,更加合适

死锁deadlock, 饥饿starvation 和 活锁livelock

deadlock

  • 由于使用锁需要线程等待(当另一个线程 持有锁时),因此可能会陷入两个线程都在等待对方的情况 – 此时它们都无法继续。死锁描述了两个或更多线程 永远被阻塞的情况,都在等待对方。
  • 死锁可能涉及两个以上的模块: 线程间的依赖关系环是出现死锁的信号。
  • 下面的代码会在某些情况下出现死锁(这个方法是hello类的一个方法)。在线程一中,当对象A执行friend方法去friend(对象B),此时A已经在线程一加锁;如果执行“that.friend(this);”之前有个线程二中的B执行friend(A),此时B已经在线程二加锁。A要执行“that.friend(this);”就要等待B执行完friend(A)方法;同时如果B想执行“that.friend(this);”也得等A执行完friend(B)方法。A,B等待对方,造成死锁。
ArrayList<hello> friend = new ArrayList<hello>();
public synchronized void friend(hello that) {
    if (friend.add(that)) {
      that.friend(this);
    }
  }
  • 改造这种状况的方法:
    – 防止死锁的一种方法是对需要同时获取的锁进行排序,并确保所有代码按照该顺序获取锁定 。这需要知道子系统/系统中所有的锁,无法模块化(如果有多个对象,这个方法的代码会很长…);
ArrayList<hello> friend = new ArrayList<hello>();
  public void friend(hello that) {
    hello first, second;
    if (that.hashCode()>this.hashCode()) {
      first = that;
      second = this;
    } else {
      first = this;
      second = that;
    }
    synchronized (first) {
      synchronized (second) {
        if (friend.add(that)) {
          that.friend(this);
        }
      }
    }
  }

– 另一种方法比较是利用粗粒度的锁,即用单个锁来监控多个对象实例 。但这样性能损失大。如果用一个锁 保护大量的可变数据,那么就放弃了同时访问这些数据的能力。在最糟糕的情况下,程序可能基本上是顺序执行的,丧失了并发性。

  static public String aString = "";
  ArrayList<hello> friend = new ArrayList<hello>();
  public void friend(hello that) {
    synchronized (aString) {
      if (friend.add(that)) {
        that.friend(this);
      }
    }
  }

阻塞方法

阻塞方法不同于一般的要运行较长时间的方法。一般方法的完成只取决于它所要 做的事情,以及是否有足够多可用的计算资源 。而阻塞方法的完成还取决于一 些外部的事件,例如计时器到期,I/O 完成,或者另一个线程的动作( 释放一个锁,设置一个标志,或者将一个任务放在一个工作队列中)。一般方法在它们的工作完成 后即可结束,而阻塞方法较难于预测,因为它们取决于外部事件。 阻塞方法可能因为等不到所等的事件而无法终止, 因此令阻塞方法可取消就非常有用(如果长时间运行的非阻塞方法是可取消的,通常也非常有用)。可取消操作 是指能从外部使之在正常完成之前终止的操作

sleep()

Thread类的静态方法,参数为整形,表示让当前线程暂停指定时间的执行(单位是毫秒),期间不参与CPU的调度,不释放所拥有的监视器资源(锁) 。可能会抛出InterruptedException异常。下面的代码表示线程暂停1秒

	try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

join()

Join()方法用于保持当前正在运行的线程的执行,直到该线程死亡(执行完毕),才能继续执行后续线程。

   Thread th1 = new Thread(new hello(), "th1"); 
   Thread th2 = new Thread(new hello(), "th2");
   Thread th3 = new Thread(new hello(), "th3");
   th1.start();
   try { th1.join(); } catch (InterruptedException ie) {}
   th2.start();
   try { th2.join(); } catch (InterruptedException ie) {}
   th3.start();
   try { th3.join(); } catch (InterruptedException ie) {} 

上面的代码中,线程th1,th2,th3先后按顺序执行。

wait(), notify(), and notifyAll()

  • 执行wait()后,当前线程会等待,直到其他线程调用此 对象的notify( ) 方法或 notifyAll( ) 方法
  • 调用wait()时,当前线程需要拥有对象的锁。执行 wait()后,线程释放锁并等待。当被唤醒后,重新参与锁所有权的竞 争,成功后从之前开始wait的点继续执行
  • 只有获得了对象锁所有权的线程可以调用对象 的wait()方法,否则会抛出InterruptedException 异常(即使有该对象的锁也可能异常,后面会提到)。
synchronized (nameString) {
      try {
        nameString.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  • 虚假唤醒:一个线程可以从挂起状态变为运行状态(被唤醒),即使该 线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被 中断,或者等待超时。为了避免虚假唤醒,则采用循环的方法测试被唤醒的条件是否满足, 不满足则继续等待。经典的调用共享变量wait()方法的实例如下
    在这里插入图片描述
  • notifyAll()与notify() :wait让其他线程获得锁,并可能调用notify或notifyAll方法。notifyAll唤醒所有 wait 线程,notify只随机唤醒一个 wait 线程。被唤醒的资源会重新参与到锁的竞争中,获取后, 从wait的地方继续执行。

interrupt()

每个线程 都有一个与interrupt相关联的 Boolean 属性,用于表示线程的中断状态,中断状 态初始时为 false 。当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况之一:

  • 如果被中断线程在执行一个低级可中断阻塞方法,例如 Thread.sleep()、 Thread.join() 或Object.wait(),那么它将取消阻塞并抛出 InterruptedException
  • 否则, interrupt() 只是设置线程的中断状态,通知该线程有其他线程想终止它,让它 自己决定是否终止
  • 被中断线 程中运行的代码,可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过 Thread.isInterrupted() 来读取,还可以通过一个名为 Thread.interrupted() 的操作读取和清除。
  • 中断 的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的 灵活性。 通常,我们很少希望一个活动立即停止。如果活动在 正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。

设计多线程安全ADT

  • 设计步骤

1.定义操作
2.测试
3.确定rep
4.checkrep(),检查不变性
5.阐述如何使rep线程安全,写入表示不变性的说明中,以便代码维护者知道你是如何为类设 计线程安全性的:

  • 阐述使用了哪种技术 ,各个线程之间不共享对象,各个线程之间共享不可变类型,使用线程安全数据类型,还是使用Synchronization:
  • 即使在使用线程安全数据类型时也可能线程不安全,因此需要对安全性进行这种仔细的论证
  • 一种实现方式:在类中所有的方法前面加上synchronized或者在代码块上使用synchronized(this){} 包围起来(称作monitor pattern)。

如图在这里插入图片描述

说点什么

虽说是为了软件构造这门课程写的博客。但是收获很大。这篇博客是迄今为止写的最认真的一篇。我学习这章的时候有一些概念,比如死锁、interrupt等含义并不太理解,但今天写博客并复习的过程中总算是明白了其具体含义。另外,在完成lab6之后复习,也对知识的理解有很大帮助。我学习的时候,一般情况下光看描述如果没有代码的话不知道这一概念是什么意思。因此这篇博客尽量多写了点代码(比起以前写的来说)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值