Chapter 3. Data synchronization
在
第二章中介绍了如何创建线程对象、启动和终止线程。但多线程编程的关键在于多个线程之间数据的共享和同步,从这一章开始,将详细介绍线程之间数据的共享和同步的各种方法。
3.1 The Synchronized Keywor 1. synchronized是Java中最基本也最常用的用来编写多线程安全代码的关键字,用以保护对多线程共享的数据的操作总是完整的;
2.
Atomic: 当一个操作被定义成原子操作时,意味着该操作在执行过程中不会被打断;原子操作可以由硬件保证或者通过软件来模拟;
3.
Mutex Lock:
在很多多线程系统,通过互斥锁来保护的共享数据。Java中的任何一个对象都有一个与之相关的锁,当一个方法被声明成synchronized时,就表示
当线程进入该方法前,必须获得相应对象的锁,在执行完毕再释放这个锁。从而保证同一时刻只有一个线程调用该对象上的被声明为synchronized的方
法。注意:Java的互斥锁只能加在对象级上,获得某个对象的锁,并不能保证该对象的属性和其它非synchronized的方法是线程安全的;也不能保
证受保护的方法里调用的其它对象是多线程安全的,除非任何调用这些没有被保护的方法或者对象只通过受保护的方法进行调用。所以,编写线程安全的代码关键就
在于规划方法和对象之间的调用关系,并尽量采用相同对象的锁来进行同步控制。
3.2 The Volatile Keyword 1. Scope of a Lock: 锁的作用范围即获得和释放锁之间的那段时间。
2. Java标准虽然声明存取一个非long和double变量的操作是原子操作,但由于不同虚拟机实现的差异,在多线程环境下每个线程可能会保留自己的工作拷贝,而导致变量的值产生冲突。为了避免这种情况的发生,可以有两种方法:
1) 为变量创建声明为synchronized的setter和getter方法,然后任何调用(包括在类类部)该变量的地方都通过setter和getter方法;
2) 采用volatile声明,确保每次存取这些属性时都从主内存中读入或者写入主内存中;
3) volatile仅仅用于解决Java内存模式导致的问题,只能运用在对该变量只做一个单一装载或写入操作且该方法的其它操作并不依赖该变量的变化。如:在一个循环体中作为递增或递减变量时就不能使用volatile来解决线程同步的问题。
3. volatile的使用是有限的,一般而言,仅仅将其作为强制虚拟机总是从主内存读写变量的一个手段,或者某些需要其参数声明为volatile的函数。
3.3 More on Race Conditions 本节以打字游戏中显示成绩的例子来解释了可能存在的race condition。关键要注意以下几点:
1. 操作系统会随机的切换多个线程的运行,因此当多个线程调用同一个方法时,可能在
任何地方被暂停而将控制权交给另外的线程;
2. 为了减少被误用的可能,总是假设方法有可能被多个线程调用;
3. 锁仅仅与某个特定实例相关,而与任何方法和类都无关,这一点当需要存取类属性或方法时要特别注意;
4. 任何时候只有一个线程能够运行某个类中一个被声明为synchronized的静态方法;一个线程只能运行某个特定实例中一个被声明为synchronized的非静态方法。
3.4 Explicit Locking 1.
学过Win32下编写多线程的朋友刚开始可能会被Java的Synchronized关键词搞糊涂。因为Java中的任何一个对象都有一个与之相关的锁,
而不象在Win32下要先定义一个互斥量,然后再调用一个函数进入或者离开互斥区域。在JDK 1.5以后也开始提供这种显示声明的锁。JDK
1.5中定义了一个Lock接口和一些类,允许程序员显示的使用锁对象。
2. 在Lock接口里有两个方法:lock()和unlock()用来获取和释放锁对象,从而保证受保护的代码区域是线程安全的。
3. 使用锁对象的一个好处在于可以被保存、传递和抛弃,以在比较复杂的多线程应用中使用统一的锁。
在使用锁对象时,总是将lock()和unlock()调用包含在try/finally块中,以防止运行时异常的抛出而导致死锁的情况。
3.5 Lock Scope 利用lock()和unlock()方法,我们可以在任何地方使用它们,从一行代码到跨越多个方法和对象,这样就能根据程序设计需要来定义锁的作用(scope of lock)范围,而不是象以前局限在对象的层次上。
3.5.1 Synchronized Blocks 1. 利用synchronized关键字也能建立同步块,而不一定非得同步整个方法。
2. 同步一段代码时,需要明确的指定获取哪个对象的锁(这就类似于锁对象了),这样,就可以在多个方法或对象中共享这个锁对象。
3.6 Choosing a Locking Mechanism 使
用锁对象还是同步关键字(synchronized),这个取决于开发员自己。使用synchronized比较简单,但相对而言,比较隐晦,在比较复杂
的情况下(如要同时同步静态和非静态方法时),没有锁对象来得直观和统一。一般而言,synchronized简单,易于使用;但同时相对而言效率较低,
且不能跨越多个方法。
3.6.1 The Lock Interface
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
void unlock();
Condition newCondition();
}
其中的tryLock()和tryLock(long, TimeUnit)将尝试获取锁对象,如果不能获取或在指定时间内不能获取,将立即返回。调用者可以根据返回值来判断是否获得了锁对象以做进一步的操作。
3.7 Nested Locks 1.
在一个同步方法内调用同一个对象上的另外的同步方法的情况,称之为嵌套锁(nested
locking)。系统将自动执行嵌套的同步方法,而无须等待锁的释放。因此,有的时候,即使一些私有方法仅仅被已同步的方法调用,我们也给其加上
synchronized关键字,以减少后续维护时可能产生的误导。
2. ReenterantLock类也支持嵌套锁。在ReenterantLock类维持一个计数器,只有当这个计数器为0时,才会释放锁。注意:
这个特性是ReenterantLock类的特性,而不是所有实现Lock接口的类的特性。
3.
需要支持嵌套锁的一个原因是方法之间交叉调用(cross-calling)。设想对象a的方法M1调用对象b的方法N1,然后N1再调用对象a的方法
M2,而M1和M2都是同步方法,如果不支持嵌套锁,则N1将在调用M2时等待M1释放锁,而M1则由于N1没有返回永远也不会释放锁,这样就产生了死
锁。
4. synchronized和Lock接口并没有提供锁对象被嵌套获取的次数,但ReentrantLock则提供了这样一种机制:
public class ReentrantLock implements Lock {
public int getHoldCount();
public boolean isLocked();
public boolean isHeldByCurrentThread();
public int getQueueLength();
...
}
其中:
1) getHoldCount()返回当前线程的获取次数,返回0并不表示该锁是可获取的,有可能没有被当前线程获得;
2) isLocked()判断该锁对象是否被任何线程获得;
3) isHeldByCurrentThread()判断是否由当前线程获得;
4) getQueueLength()用来估计当前有多少线程在等待获取这个锁对象。
3.8 Deadlock 介绍了死锁的概念,并修改例子代码来演示deadlock的产生。死锁一般产生在多个线程在多个锁上同步时产生的;当然,多个线程在判断多个条件的时候,也有可能产生死锁。
3.9 Lock Fairness 1. Java里锁的获取机制依赖于底层多线程系统的实现,并不保证一个特定的顺序;
2. ReentrantLock类提供一个先进先出(first-in-first-out)的获取锁的选项(在创建锁对象时传入true值)。
文章来源:
http://blog.csdn.net/evanwhj/archive/2006/03/05/616068.aspx