《Java 线程编程》学习笔记7
第七章 并发访问对象和变量
- 当多个线程与对象交互时,则需要适当的控制,以确保线程间不会产生不利的影响。
7.1 易变成员变量修饰符
volatile
关键字是用于成员变量的一个修饰符,每次访问线程时,强迫它从共享内存中重读变量的值。而且,当变量发生变化时,强迫线程将变化值写到共享内存中。如此一来,不管在任意时刻,两个不同的线程总是看到某个成员变量的同一个值。- Java 语言规范表明,为了获得最佳速度,允许线程保存共享成员变量的工作拷贝,而且只是偶尔用共享的原始值来校准。为了更准确地描述,“偶尔”可以翻译为:“当线程进入或离开同步校验代码块时”。
volatile
关键字用于告诉 VM:它不应该保存变量的私有拷贝,而应该直接与共享拷贝交互。- 代码示例:
public class Volatile extends Object implements Runnable {
// 没有标记为 volatile,但是应该标记的
private int value;
private volatile boolean missedIt;
// 不需要申明为 volatile 的变量
private long creationTime;
public Volatile() {
value = 10;
missedIt = false;
creationTime = System.currentTimeMillis();
}
public void run() {
print("entering run()");
// 每次检查 value 值是否相同
while(value < 20) {
// 如果找不到对值修改,则跳出循环
if(missedIt) {
int currValue = value;
// 在一个对象上执行同步语句,观察其效果
Object lock = new Object();
synchronized(lock) {
// 不做任何事情
}
int valueAfterSync = value;
print("in run() - see value = " + currValue + ", but rumor has it that it changed!");
print("in run() - valueAfterSync = " + valueAfterSync);
break;
}
}
print("leaving run()");
}
private void workMethod() throws InterruptedException {
print("entering workMethod()");
print("in workMethod() - about to sleep for 2 second");
Thread.sleep(2000);
value = 50;
print("in workMethod() - just set value = " + value);
print("in workMethod() - about to sleep for 5 second");
Thread.sleep(5000);
missedIt = true;
print("in workMethod() - just set missdIt = " + missedIt);
print("in workMethod() - about to sleep for 3 second");
Thread.sleep(3000);
print("leaving workMethod()");
}
private void print(String msg) {
// 使用 java.text 包的功能
// 可以简化这个方法
// 但这里没有利用这一点
// 因为 JDK1.0 没有这个包
...
}
public static void main(String[] args) {
try {
Volatile vol = new Volatile();
Thread.sleep(100);
Thread t = new Thread(vol);
t.start();
Thread.sleep(100);
vol.workMethod();
} catch(InterruptedException x) {
System.out.println("one of the sleeps was interrupted.");
}
}
}
/*
执行的可能结果:
Thead-0: entering run()
main: entering workMethod()
main: in workMethod() - about to sleep for 2 second
main: in workMethod() - just set value = 50
main: in workMethod() - about to sleep for 5 second
main: in workMethod() - just set missedIt = true
main: in workMethod() - about to sleep for 3 second
Thread-0: in run() - see value = 10, but rumor has it that it changed!
Thread-0: in run() - valueAfterSync = 50
Thread-0: leaving run()
main: leaving workMethod()
加上 volatile 后可能的执行结果:
Thead-0: entering run()
main: entering workMethod()
main: in workMethod() - about to sleep for 2 second
main: in workMethod() - just set value = 50
main: in workMethod() - about to sleep for 5 second
Thread-0: leaving run()
main: in workMethod() - just set missedIt = true
main: in workMethod() - about to sleep for 3 second
main: leaving workMethod()
*/
- Sun 在 VM 中包含 JIT 之前,使用
volatile
无差异。(JIT = Just-in-time 及时编译技术)
什么是JIT?
参考: http://blog.youkuaiyun.com/ns_code/article/details/18009455
不论是物理机还是虚拟机,大部分的程序代码从开始编译到最终转化成物理机的目标代码或虚拟机能执行的指令集之前,都会按照如下图所示的各个步骤进行:
其中绿色的模块可以选择性实现。很容易看出,上图中间的那条分支是解释执行的过程(即一条字节码一条字节码地解释执行,如JavaScript),而下面的那条分支就是传统编译原理中从源代码到目标机器代码的生成过程。
如今,基于物理机、虚拟机等的语言,大多都遵循这种基于现代经典编译原理的思路,在执行前先对程序源码进行词法解析和语法解析处理,把源码转化为抽象语法树。对于一门具体语言的实现来说,词法和语法分析乃至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是C/C++语言。也可以把抽象语法树或指令流之前的步骤实现一个半独立的编译器,这类代表是Java语言。又或者可以把这些步骤和执行引擎全部集中在一起实现,如大多数的JavaScript执行器。
7.2 同步方法修饰符
- 在方法中添加修饰符
synchronized
,确保在同一时刻,方法内只允许有一个线程,当对象的状态临时处于不一致时,这对于阻止其他线程进入方法有用。
7.2.1 两个线程同时位于一个对象的同一个方法中(没加 synchronized)
7.1.1 同一时刻一个线程(加 synchronized)
- 当线程碰到
synchronized
实例方法时,就会一直阻塞到可以排它性访问对象级别的互斥锁(mutex lock)为止。互斥(mutex)是互相排斥(mutual exclusion)的缩写。互斥锁在一个时刻只能由一个线程持有。当释放该锁时,所有等待的线程均竞争排它性访问权限。只有一个线程可以竞争成功,其它线程恢复阻塞状态,并再次等待锁的释放。 - 如果对象上的一个
synchronized
方法调用同一个对象上的另一个synchronized
方法,它不会阻塞来竞争对象级别的锁(还有其他级别的锁?),因为它已经获得了排它性访问锁的权限。
疑问:除了对象级别锁,还有其他级别的锁?
参考:http://zhh9106.iteye.com/blog/2151791
在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,下面看看这个关键字的用法。因为synchronized关键字涉及到锁的概念,所以先来了解一些相关的锁知识。
java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
**java的对象锁和类锁:**java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的
public class TestSynchronized
{
public synchronized void test1()
{
int i = 5;
while( i-- > 0)
{
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie)
{}
}
}
public static synchronized void test2()
{
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie)
{}
}
}
public static void main(String[] args)
{
final TestSynchronized myt2 = new TestSynchronized();
Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" );
Thread test2 = new Thread( new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2" );
test1.start();
test2.start();
// TestRunnable tr=new TestRunnable();
// Thread test3=new Thread(tr);
// test3.start();
}
}
/*
上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
*/
7.2.3 两个线程,两个对象
- 类的每个对象都有自己的对象级别锁!
7.2.4 避免对象的意外崩溃
- 对于原子操作,一般是不存在线程问题的。
- 但是,如果两个线程同时对变量进行赋值,这种非原子操作,就可能出现线程问题。
public class CorruptWrite extends Object {
private String fname;
private String lname;
public void setNames(String firstName, String lastName) {
print("entering setName()");
fname = firstName;
// 线程可能从此处交换出去,
// 可能在外部逗留不同的时间,
// 用不同的休眠时间放大了该值
if(fname.length() < 5) {
try {Thread.sleep(1000);}
catch(InterruptedException x) {}
}
else {
try {Thread.sleep(2000);}
catch(InterruptedException x) {}
}
lname = lastName;
print("leaving setName() - " + lname + ", " + fname);
}
public static void print(String name) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": " + msg);
}
public static void main(String[] args) {
final CorruptWrite cw = new CorruptWrite();
Runnable runA = new Runnable() {
public void run() {
cw.setName("George", "Washington");
}
};
Thread threadA = new Thread(runA, "threadA");
threadA.start();
try {Thread.sleep(200)};
catch(InterruptedException x) {}
Runnable runB = new Runnalbe() {
public void run() {
cw.setNames("Abe", "Lincoln");
}
}
Thread threadB = new Thread(runB, "threadB");
threadB.start();
}
}
- 在多线程环境中,以上修改后的
setName()
方法快速版本仍然存在不易察觉的危险。可能刚好在 fname 赋值之后,lname 赋值之前,线程规划器让 threadA 从处理器中交换出去。虽然 threadA 只完成了一半的工作,但 threadB 可能交换进来,并将它的参数赋值给 fname 和 lname。这就使得对象处于不一致状态。这段代码大部分时间可以良好运行,但是偶尔会出现崩溃现象。 - 给代码添加
synchronized
属性后可以避免这个问题。让setNames()
成为一个synchronized方法,则试图进入该方法的所有线程都将阻塞,直到获得对象级别锁的排斥访问权限为止。
7.2.5 对象处于不一致状态时,推迟对它的访问
- 7.2.4 中添加
synchronized
关键字使得对变量的修改保持了唯一性,现在我们考虑变量的读取的唯一性。 - 代码如下:
public class DirtyRead extends Object {
private String fname;
private String lname;
public String getNames() {
return lname + ", " + fname;
}
public synchronized void setNames(String firstName, String lastName) {
...
}
public static void main(String[] args {
final DirtyRead dr = new DirtyRead();
dr.setNames("George", "Washington");
Runnable runA = new Runnable {
public void run() {
dr.setNames("Abe", "Lincoln");
}
}
try {Thread.sleep(200);}
catch(InterruptedException x) {}
Runnable runB = new Runnable {
public void run() {
print("getName() = " + dr.getnames();
}
}
Thread threadB = new Thread(runB, "threadB");
threadB.start();
}
}
/*
输出结果:
main: entering setNames()
main: leaving setNames() - Washington, George
threadA: entering setNames()
threadB: getNames() = Washington, Abe
threadA: leaving setNames() - Lincoln, Abe
*/
- 上述例子说明了一个不可避免的事实:对象必定在短时间内处于不一致的状态,即使只是保留赋值,删除其他所有语句,情况也是如此。
- 不论处理器速度多快,线程规划器也可能交换出进行更改的线程,交换它的时间在修改 fname 之后,但在修改 lname 之前。持有一个对象级别锁不会阻止线程被交换出来。如果被交换出来,它将继续持有对象级别锁。因此,必须小心数据处于不一致状态时,确保阻塞所有的读。
- 下面的代码通过在
getNames()
方法上添加一个synchronized
关键字来控制并发读和写。
/*
由于 getNames() 是 synchronized 方法,所以,处于 setNames() 状态下时,getNames() 的 threadB 阻塞,尽量获得对对象级别锁的排斥访问权限。当 threadA 从 setNames() 方法退出后,自动释放对象级别锁。于是,threadB 得以获得对象级别锁,并进入 getNames() 方法。
*/
public synchronized String getNames() {
return lname + ", " + fname;
}
技巧:
如果两个或更多线程同时与某个对象的成员变量交互,而且至少其中一个线程会更改它的值,则一般来说,理想的做法是使用synchronized
来控制并发访问。如果只有一个线程访问对象,那么,没有必要使用 synchronized,这样反而会减缓它的执行速度。(从这里也可以看出,对于一个对象,只有一个对象锁。)
7.3 同步语句块
- 当整个方法不需要同步,或者希望线程获得不同对象上的对象级别锁时,可以使用同步块(synchronized block)。同步(synchronized)语句块如下所示。
synchronized (obj) {
// 代码块
}
7.3.1 减少持有锁的时间
synchronized
块可以用于减少持有对象级别锁的时间。如果方法进行大量其他不需要访问成员变量的工作,就可以缩短持有锁的时间,只限制到关键的部分:
public void setValues(int x, double ratio) {
// 它们不需要使用成员变量
// ...
double processedValA = ... // 长时间计算
double processedValB = ... // 长时间计算
// ...
synchronized (this) {
a = processedValA;
b = processedValB;
}
}
7.3.2 锁定任意对象,而非仅仅锁定当前对象
- 同步语句:
/*
mutex 是 VM 中任意对象
*/
syncrhonized(mutex) {
}
7.3.3 把向量内容安全地复制到数组
- 以 Vectory 类为例进行复制,这个可以推广到所有的复制/添加内容方法。
Vector vect = new Vector();
synchronized(vect) {
int size = vect.size();
word = new String[size];
for(int i = 0; i < word.length; i++) {
word[i] = (String) vect.elementAt(i);
}
}
警告:
从 JDK1.2 开始,Vector 和 Hashtable 已经添加到 Collections API 中。所欲的方法仍然存在,同时还添加了一些新的非同步方法。示例只对 JDK1.2 以前安全。
7.4 静态同步方法
- 对于类的每个实例,除了存在对象级别锁外,还存在类级别锁,它被特定类的所有实例共享。VM 装载的每个类只有一个类级别锁。如果方法既是静态,又是同步的,则线程进入方法前,必须获取排斥性访问类级别锁的权限。
7.5 在同步语句中使用类级别锁
- 示例代码:
synchronized (ClassName.class) {
// 方法体
}
7.6 同步化和集合API
- JDK1.2 以后,Collection API 是新添加的,其中包含大量接口和类。
7.6.1 封装集合,使之同步化
- 最初设计的 Vector 和 HashTable 是多线程安全的。例如,对于 Vectory 而言,删除或添加元素的方法是同步的。等等。
- 集合 API 的设计者希望避免在不必要的时候滥用同步化,以免带来过多的死锁。因此,对于更改集合内容的方法,没有一个是同步化的。如果多线程访问集合(Collection)或映射(Map),则应当用一个同步化所有方法的类封装它。
警告:
集合本质上非多线程安全。当多个线程与集合交互时,为了使它多线程安全,必须采取额外的措施。
- 在 Collection 类中有多个静态方法,他们用于运用同步方法封装非同步集合,例如:
List list = Collections.synchronizedList(new ArrayList());
技巧:
当同步化集合时,不要使用原始未同步集合的直接引用。这将确保其他线程不会意外作出不一致的更改。
7.6.2 安全地把列表中的内容复制到数组
- 下面展示了3种安全的途径:
...
// 为了安全起见,仅使用同步列表的一个引用
// 可以确保控制了所有的访问
List wordList = Collections.synchronizedList(new ArrayList());
// 第一种技术(推荐)
String[] wordA = (String[]) wordList.toArray(new String[0])
// 第二种技术
String[] wordB;
synchronized(wordList) {
int size = wordList.size();
wordB = new String[size];
wordList.toArray(wordB);
}
// 第三种技术(必须使用 synchronized)
String[] wordC;
synchronized (wordList) {
wordC = (String[]) wordList.toArray(new String[wordList.size()]);
}
...
7.6.3 安全遍历集合元素
7.7 死锁
- 死锁出现情况的抽象图:
7.7.1 规避死锁
- 对于容易发生死锁的代码,应该尽量遵循以下原则:
- 只在必要的最短时间内持有锁。考虑使用同步语句块代替整个同步方法。
- 尽量编写不在同一时刻需要持有多个锁的代码。如果不可避免,则确保线程持有第二个锁的时间尽量短。
- 创建和使用一个大锁来代替多个小锁。
7.8 加速并发访问
- 同步对于编写多线程代码十分关键。但是同步需要付出代价。获取和释放锁的简单任务给处理器添加了更多的工作量,因而减慢了执行速度。这个额外的开销就是为什么默认时,集合 API 中的方法不是同步方法阿德原因。只有一个线程处理集合时,同步化是一种处理器资源的浪费。