- synchronized–共享资源竞争的简单解决方案
在我们写多线程的程序时,经常会碰到多个线程需要访问同一资源,并且这个资源在当前状态,只能被一个线程访问,否则程序就会出错,这时候,我们一般的解决方式就是采用“序列化访问共享资源”的方案。意思说,在给定时刻只允许一个任务访问共享资源。
通常时在代码前面加上一条锁语句来实现序列化访问共享资源,也就是说在一段时间内,只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)。
Java提供了synchronized的形式,来实现代码锁,为防止资源冲突提供了内置支持。当一个任务执行到synchronized关键字保护的代码片段时,先会去检查锁是否可用,如果锁可用,会获取锁,执行代码,然后释放锁。
我们一般会将所有访问到共享资源的方法标记为synchronized,在一个任务处于synchronized方法的调用中,其余所有线程调用类中任意一个被标记为synchronized的方法的线程都将被阻塞。也就是说,在同一个对象内的synchronized的方法共享同一个锁,只要一个上了锁,那么所有的synchronized方法都将不可用。
在当前线程已经拿到了synchronized的锁之后,将可以继续获取相应对象的其他的方法的synchronized的锁,JVM会去为当前这个线程计数,统计对象被加锁的次数,每当这个线程退出一个synchronzied方法时,当前的计数就会减一,直到为0时,锁才会被完全释放。此时,别的任务才可以使用该对象。
synchronized的机制,意味着类中所有对于临界资源访问的操作都需要加上synchronized关键字,否则,只要有一个没有加上,都有可能导致错误。因为其他线程会访问到这个临界资源。忽略整个对象的锁。
代码如下:
```
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public synchronized int next() {
++currentEvenValue; // Danger pointer here
++currentEvenValue;
return currentEvenValue;
}
public static void main(String [] args) {
EvenChecker.test(new EvenGenerator());
}
}
```
-
Lock对象
除了给对象的方法加上synchronized关键字之外,我们还可以使用Lock对象,使用Lock对象,较之synchronized的方式,代码缺乏优雅性,但是对于解决问题时,这种方式相较更加灵活。可以决定需要上Lock对象的代码段,不用整个流程都变成临界资源。
代码如下:private int currentEvenValue = 0; private Lock lock = new ReentrantLock(); public int next() { lock.lock(); try { ++currentEvenValue; ++currentEvenValue; return currentEvenValue; } finally { lock.unlock(); } }
使用ReentrantLock的实例化对象Lock,然后在即将进入临界资源的地方使用lock()方法,这样这段代码就上了锁,然后这里必须使用try-finally子句,并且return语句必须放在try语句中,unlock方法暴露在finally子句中,这样就可以保证在return之前一定可以释放锁,并且在释放所之前,不会有其他线程进入临界资源区。
使用Lock这样的方式虽然比synchronized方式要麻烦得多,但是,synchronized中报错时,会直接抛出一个异常,没有机会去 做清理工作,而这里在finally子句中,可以先做好清理工作,这样就可保证系统一直维护在正确的状态了。还有一点就是,用synchronized不能控制线程尝试获取锁,或者尝试获取锁一段时间,如果失败了,那么处理其他事物。如果想要实现上述过程,就需要用到了concurrent类库。
代码如下:public void timed() { boolean captured = false; try { captured = lock.tryLock(100, TimeUnit.MILLISECONDS); } catch(InterruptedException e) { throw new RuntimeException(e); } try { System.out.println("tryLock(20, TimeUnit.SECONDS) :" + captured); } finally { if(captured) { lock.unlock(); } } }
-
volatile关键字
在我们写程序时,有的时候,程序的中间变量才低级层次上可以发现,部分值是保存在本地处理器的缓存中的,这样的情况出现以为着,相同的任务,不同的线程有着不同的当前视图,也就是说一个线程对一个变量进行修改时,尽管这个变量是共用的,但是,可能由于暂时性的没有将缓存中的视图更新到主存中,这就会造成不可视,所以这时就出现了volatile关键字,volatile关键字保证了应用中的可视性,如果将一个域设置为了volatile的,那么当前这个域只要发生了写操作,那么所有的读操作都将可以看到这个修改。即使使用了本地缓存,volatile也会被写入到主存中。 -
原子类
Java SE5引入了原子类,AtomicInteger,AtomicLong,AtomicReference等特殊的原子性变量类,一般更新操作方式为使用方法类似compareAndSet(expectedValue, updateValue),原子类的方法调用操作为原子性的,所以不用担心在中间切换线程。但是大多情况下我们还是习惯使用synchronized或者lock,两种方式较之原子类,我们更常用,也更安全点。
代码如下:import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest implements Runnable { private AtomicInteger i = new AtomicInteger(0); public int getValue() { return i.get(); } public void evenIncrement() { i.addAndGet(2); } public void run() { while(true) { evenIncrement(); } } public static void main(String [] args) { new Timer().schedule(new TimerTask() { public void run() { System.err.println("Aborting"); System.exit(0); } }, 5000); ExecutorService exec = Executors.newCachedThreadPool(); AtomicIntegerTest ait = new AtomicIntegerTest(); exec.execute(ait); while(true) { int val = ait.getValue(); if(val % 2 != 0) { System.out.println(val); System.exit(0); } } } }
原子类属于java.util.concurrent中的类。
-
临界区
有的时候,我们并不需要类中的整个方法都被设置为synchronized,我们需要的仅是其中一部分代码,这部分代码段我们称之为临界区(critical section ),一般我们可以使用同步块的方式写一段代码,这段代码就是一段临界区;
代码如下:class PairManager2 extends PairManager { public void increment() { Pair temp; synchronized(this) { p.incrementX(); p.incrementY(); temp = getPair(); } store(temp); } }
这里可以在当前对象上同步,也可以在其他对象上同步,就是值synchronized()中的参数是哪个对象,这里面填哪个对象,那么对应对象的所有synchronized临界区会共用一个锁。
-
线程本地存储
这里仅引入一个ThreadLocal的概念,使用这个类创造的对象,那么在执行任务的线程中,会有一个相应对象的本地存储,不同线程不同的本地存储,不共享。
代码如下:
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
这里仅是声明一个Integer对象,当在一个任务里使用这个对象时,那么相应线程就会创造一个本地存储放置这个Integer对象。