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之后复习,也对知识的理解有很大帮助。我学习的时候,一般情况下光看描述如果没有代码的话不知道这一概念是什么意思。因此这篇博客尽量多写了点代码(比起以前写的来说)。