有时,你只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为临界区(critical section),它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:
synchronized(syncObject){
// This code can be accessed by only one task at a time
}
这也被称为同步控制块;在进入此段代码前,必须得到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高,下面的例子比较了这两种同步控制方法。此外,它也演示了如何把一个非保护类型的类,在其他类的保护和控制之下,应用于多线程的环境:
package lime.tij._021._003._005;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author : Lime
* @Descri :
* @Notice :
* 00. 共享受限资源
* 01. 不正确地访问资源
* 因为canceled标志时boolean类型的,所以它是原子性的,即诸如赋值和返回值这样的简单操作在发生时没有中断的可能。(准确的说基础类型除去long和double读写都是原子性的)
* 为了保证可视性,canceled标志还是volatile的。
* 在Java中,递增不是原子性的操作
* 02. 解决共享资源竞争 1.synchronized、2. 使用显式的Lock对象
* 对于并发工作,你需要某工方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况。
* 防止共享资源访问冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项共享资源的任务必须锁定这项资源,其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它,以此类推。
* 基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。
* 这意味着在给定时刻只允许一个任务访问共享资源。
* 通常这是通过在代码前面加上一条锁语句来实现,这就使得在一段时间内只有一个任务可以运行这段代码。
* 因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)
* 1. synchronized
* Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
* 共享资源一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。
* 要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized。
* 如果某个任务处于一个标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。
* 所有对象都自动含有单一的锁(也称为监视器),对于某个特定对象来说,其所有synchronized方法共享同一个锁。
* 一个任务可以多次获得对象的锁。JVM负责跟踪对象被加锁的次数。进入synchronized方法,计数加1,离开synchronized方法,计数减1.当计数为零的时候,锁被完全释放。
* 针对每一个类,也有一个锁(作为类的Class对象的一部分),所以synchronize static方法可以在类的范围内防止对static数据的并发访问。
* 每个访问临界共享资源的方法都必须被同步,否则它们就不会正确地工作。(如果在你的类中有超过一个方法在处理临界数据,那么你必须同步所有相关方法。如果只同步一个方法,那么其他方法将会随意地忽略这个对象锁,并可以在无任何惩罚的情况下被调用)
* 2. 使用显示的Lock对象
* 定义在java.util.concurrent.locks中的显示的互斥机制。
* Lock对象必须被显式地创建(private Lock lock = new ReentrantLock();)、锁定(lock.lock();)、释放(lock.unlock();)
* 如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常。
* 有了显示的Lock对象,你就可以使用finally子句将系统维护在正确的状态了。
* 显式的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了你更细粒度的控制力。
* 这对于实现专有同步结构时很有用的,例如用于遍历链接列表中的节点的节节传递的加锁机制(也称为锁耦合),这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁。????
* 03. 原子性与易变性
* 原子性操作时不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。
* 原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。
* 对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存。
* 但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32为操作来执行,
* 这就产生了在一个读取和写入操作中间发生上下文切换,
* 从而导致不同的任务可以看到不正确结果的可能性(这有时被称为字撕裂,因为你可能会看到部分被修改过的数值)。
* 但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性。
* volatile关键字还确保了应用中的可视性。
* 如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。
* 即便使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读操作就发生在主存中。
* 在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。
* 如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。
* 同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
* 一个任务所作的任何写入操作对这个任务来说都是可视的(单线程),因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile的。
* ???当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。
* ???如果某个域的值受到其他域的值的限制,那么volatile也无法工作,例如Range类的lower和upper边界就必须遵循lower<=upper的限制。
* 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
* 04. 原子类
* 诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供下面形式的原子性条件操作:
* boolean compareAndSet(expectedValue,updateValue);
* 这些类被调整为可以使用在某些现代处理器上的可获得的,并且是机器级别上的原子性。
* 05. 临界区
* 同步控制块
* 把一个非保护类型的类,在其他类的保护和控制之下,应用于多线程的环境
* 模版方法模式
* 使用Lock显式创建临界区
*/
class Pair { //Not Thread-safe
private int x;
private int y;
public Pair() {
this(0, 0);
}
public Pair(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void incrementX() {
x++;
}
public void incrementY() {
y++;
}
@Override
public String toString() {
return "x : " + x + " , y : " + y;
}
public class PairValuesNotEqualException extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal : " + Pair.this);
}
}
public void checkState() {
if (x != y) {
throw new PairValuesNotEqualException();
}
}
}
abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>());
public synchronized Pair getPair() {
return new Pair(p.getX(), p.getY());
}
protected void store(Pair p) {
storage.add(p);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
}
}
public abstract void increment();
}
//Synchronize the entire method:
class PairManager1 extends PairManager {
@Override
public synchronized void increment() {// 对象不加锁的时间更长。使得其他线程能更多的访问。
p.incrementX();
p.incrementY();
store(getPair());
}
}
//use a critical section:
class PairManager2 extends PairManager {
@Override
public void increment() {// 对象不加锁的时间更长。使得其他线程能更多的访问。
Pair temp;
synchronized (this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}
class PairManipulator implements Runnable {
private PairManager pm;
public PairManipulator(PairManager pm) {
this.pm = pm;
}
@Override
public void run() {
while (true) {
pm.increment(); // 对象不加锁的时间更长。使得其他线程能更多的访问。
}
}
@Override
public String toString() {
return "Pair : " + pm.getPair() + " checkCounter = " + pm.checkCounter.get();
}
}
class PairChecker implements Runnable {
private PairManager pm;
public PairChecker(PairManager pm) {
this.pm = pm;
}
public void run() {
while (true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState(); // 对象不加锁的时间更长。使得其他线程能更多的访问。
}
}
}
public class CriticalSection {
//Test the two different approaches:
static void testApproaches(PairManager pman1, PairManager pman2) {
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator
pm1 = new PairManipulator(pman1),
pm2 = new PairManipulator(pman2);
PairChecker
pcheck1 = new PairChecker(pman1),
pcheck2 = new PairChecker(pman2);
exec.execute(pm1);
exec.execute(pm2);
exec.execute(pcheck1);
exec.execute(pcheck2);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("Sleeep interrupted");
}
System.out.println("pm1 : " + pm1 + "\npm2 : " + pm2);
System.exit(0);
}
public static void main(String[] args) {
PairManager
pman1 = new PairManager1(),
pman2 = new PairManager2();
testApproaches(pman1, pman2);
}
}
正如注释中注明的,Pair不是线程安全的,因为它的约束条件(虽然是任意的)需要两个变量要维护成相同的值。此外,如本章前面所述,自增加操作不是线程安全的,并且因为没有任何方法被标记为synchronized,所以不能保证一个Pair对象在多线程程序中不会被破坏。
你可以想象一下这种情况:某人交给你一个非线程安全的Pair类,而你需要在一个线程环境中使用它。通过创建PairManager类就可以实现这一点,PairManager类持有一个Pair对象并控制对它的一切访问。注意唯一的public方法是getPair(),它是synchronized的。对于抽象方法increment(),对increment()的同步控制将在实现的时候进行处理。
至于PairManager类的结构,他的一些功能在基类中实现,并且其一个或多个抽象方法在派生类中定义,这种结构在设计模式中称为模版方法。设计模式使你得以把变化封装在代码里;在此,发生变化的部分使模版方法increment()。在PairManager1中,整个increment()方法使被同步控制的;但是PairManager2中,increment()方法使用同步控制块进行同步。注意,synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。
store()方法将一个Pair对象添加到synchronized ArrayList中,所以这个操作是线程安全的。因此,该方法不必进行防护,可以放置在PairManager2的synchronized语句块的外部。
Pairmanipulator被创建用来测试两种不同类型的PairManager,其方法是在某个任务中调用increment(),而PairChecker则在另一个任务中执行。为了跟踪可以运行测试的频度,Pairchecker在每次成功时都递增checkCounter。在main()中创建了两个PairManipulator对象,并允许它们运行一段时间,之后每个PairManipulator的结果会得到展示。
尽管每次运行的结果可能会非常不同,但一般来说,对于PairChecker的检查频率,PairManager1.increment()不允许有PairManager2.increment()那样多。后者采用同步控制块进行同步,所以对象不加锁的时间更长。这也是宁愿使用同步控制块而不是对整个方法进行同步控制的典型原因:使得其他线程能更多地访问(在安全的情况下尽可能多)。
你还可以使用显式的Lock对象来创建临界区: