Preface
最近看了一下<Java Concurrency In Practice> 这本书, 总体来说还是一本不错的书, 不过粒度不够细, 是从大的角度, 例如: 设计整体项目上如何考虑并发的多方面因素,不过总体上来说还是一本不错的书,结合部分网络上的资料,总结一下自己的知识,免的忘了。
下面是一些最基本的知识,不想再写了,反正网上多的是,挑了一篇还不错的转过来,大家要支持别人的成果O:
http://yanxuxin.iteye.com/blog/547261 对于进程的概念我们都很熟悉,它是应用程序级的隔离,不同的应用程序之间的进程几乎不共享任何资源。而线程则可以说是应用程序内的隔离,一种相对低级别的隔离。一个进程可以有多个线程,它们之间隔离的内容大致包括:a.自身的堆栈,b.程序计数器,c.局部变量;共享应用的内容大致包括:a.内存,b.文件句柄,c.进程状态等。线程不是Java自身的概念,它是操作系统底层的概念。Java作为一种应用语言把线程的操作通过API提升到应用开发的支持,但是在并发性的支持上并不是那么美好。 Java在设计时,每个对象都有一个隐式的锁,这个锁的使用则是通过synchronized关键字来显式的使用。在JDK5.0以后引用了java.util.concurrent.ReentrantLock作为synchronized之外的选择,配和Condition可以以一种条件锁的机制来管理并发的线程,之后的总结再介绍。提到synchronized,多数的初学者都知道Object的wait(),notify(),notifyAll()是配和其使用的,但是为什么要在同步内才能用对象的这些方法呢(不然抛IllegalMonitorStateException)? 我想因为没有synchronized让对象的隐式锁发挥作用,那么方法或者方法块内的线程在同一时间可能存在多个,假设wait()可用,它会把这些线程统统的加到wait set中等待被唤醒,这样永远没有多余的线程去唤醒它们。每个对象管理调用其wait(),notify()的线程,使得别的对象即使想帮忙也帮不上忙。这样的结果就是多线程永远完成不了多任务,基于此Java在设计时使其必须与synchronized一起使用,这样获得隐式锁的线程同一时间只有一个,当此线程被对象的wait()扔到wait set中时,线程会释放这个对象的隐式锁等待被唤醒的机会,这样的设计会大大降低死锁。另外同一个对象隐式锁作用下的多个方法或者方法块在没有锁的限制下可以同时允许多个线程在不同的方法内wait和notify,严重的竞争条件使得死锁轻而易举。所以Java设计者试图通过Monitor Object模式解决这些问题,每个对象都是Monitor用于监视拥有其使用权的线程。 但是synchronized这种获得隐式锁的方式本身也是有隐患问题的:
a.不能中断正在试图获得锁的线程,
b.试图获得锁时不能设定超时,
c.每个锁只有一个条件太少。
对于最后一项的设计前面提到的JDK5的方案是可以弥补的,一个ReentrantLock可以有多个Condition,每个条件管理获得对象锁满足条件的线程,通过await(),signalAll()使只关于Condition自己放倒的线程继续运行,或者放倒一些线程,而不是全部唤醒等等。但对于前两者的极端情况会出现死锁。下面的这个例子: - class DeadLockSample{
- public final Object lock1 = new Object();
- public final Object lock2 = new Object();
-
- public void methodOne(){
- synchronized(lock1){
- ...
- synchronized(lock2){...}
- }
- }
-
- public void methodTwo(){
- synchronized(lock2){
- ...
- synchronized(lock1){...}
- }
- }
- }
假设场景:线程A调用methodOne(),获得lock1的隐式锁后,在获得lock2的隐式锁之前线程B进入运行,调用methodTwo(),抢先获得了lock2的隐式锁,此时线程A等着线程B交出lock2,线程B等着lock1进入方法块,死锁就这样被创造出来了。 以上的例子不直观的话,再看一个实例顺便看看wait()的缺陷: - import java.util.LinkedList;
- import java.util.List;
-
- /**
- * User: yanxuxin
- * Date: Dec 9, 2009
- * Time: 5:58:39 PM
- */
- public class DeadLockSample {
- public static void main(String[] args) {
- final WaitAndNotify wan = new WaitAndNotify();
-
- Thread t1 = new Thread(new Runnable(){
- public void run() {
- wan.pop();
- }
- });
-
- Thread t2 = new Thread(new Runnable(){
- public void run() {
- wan.push("a");
- }
- });
-
- t1.start();
- t2.start();
- }
- }
-
- class WaitAndNotify {
-
- final List<String> list = new LinkedList<String>();
-
- public synchronized void push(String x) {
- synchronized(list) {
- list.add(x);
- notify();
- }
- }
-
- public synchronized Object pop() {
- synchronized(list) {
- if(list.size() <= 0) {
- try {
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return list.size();
- }
- }
-
- }
上面的这个例子也会出现死锁,为什么呢?首先看WaitAndNotify这个类,在push和pop方法上有synchronized关键字,方法内部也有synchronized,那么当WaitAndNotify实例化时会有两个对象的隐式锁,一个是WaitAndNotify对象自身的,作用在方法上;另一个就是方法内部同步用到的list的。主线程开启两个线程t1和t2,t1进入pop方法此时list为空,它先后获得了wan和list的隐式锁,接着就被wait扔进wait set等待去了。注意这个wait()方法是谁的?答案是wan的,所以它释放了wan的隐式锁,但是把list的死死的抓着不放。此时t2终于得到了wan的隐式锁进入push方法,但是不幸的是list的隐式锁它这辈子也得不到了。。。
此外synchronized的重点说的简单:它就是配和对象的隐式锁使用的,注意一定是对象的隐式锁!那么下面的这个例子又怎么解释呢? -
-
-
-
-
- public class ImplicitLockSample {
-
- public static void main(String[] args) {
- final ImplicitLock sample = new ImplicitLock();
-
- new Thread(new Runnable() {
- public void run() {
-
- sample.method1();
- }
- }).start();
-
- new Thread(new Runnable() {
- public void run() {
- sample.method2();
- }
- }).start();
- }
- }
-
-
- class ImplicitLock {
-
- public static synchronized void method1() {
- System.out.println("method1 executing...");
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- public synchronized void method2() {
- System.out.println("method2 executing...");
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
这里ImplicitLock有两个同步方法,一个是static的,一个是普通的。ImplicitLockSample是一个测试主程序,实例化一个ImplicitLock对象,并且开启两个线程,每个线程分别调用对象的method1和method2方法。每个进入方法的线程都会强制休眠3秒。那么执行的现象是什么呢? 要知道答案有以下几点要清楚:a.Class和Object的关系,b.static方法的含义,c.synchronized的机制,d.sleep的作用。清楚的知道这些之后,一眼就能辨别method1和method2方法上的synchronized配和的是两把不同的对象隐式锁。答案也就清晰的知道这两个线程执行的打印语句根本就不会相差近3秒的等待,而是几乎同时。下面我试着解释一下。 Class是Object的子类,说明了Class是特殊的对象,它自然也有对象隐式锁。static声明方法意味着这个方法不依赖于类的实例,而是可以理解成去掉了隐式参数this的,类对象的方法。synchronized是与对象隐式锁绑定的,这代表了将其置于方法声明上它将与方法的持有对象绑定。所以method1的同步锁是ImplicitLock类对象的隐式锁,而method2的同步锁是ImplicitLock实例对象的隐式锁。sleep虽然能让当前的线程休眠,但是它不会释放持有的隐式锁。这样主程序执行是虽然用同一个实例让两个线程分别去调用两个方法,但是它们之间并没有任何竞争锁的关系,所以几乎同时打印,不会有近3秒的间隔。把method1的调用改成已注释的代码将更容易理解。如果method1的synchronized去掉,或者method2加上synchronized的声明,那么它们将竞争同一个隐式锁。先获得锁的线程将在3秒后交出锁,后面的线程才能执行打印。 写这篇补遗源自于对懒汉式单例的重新理解,之前对synchronized的机制不明了时,只知道使用synchronized关键字在static方法上声明就能保证单例的线程安全,但是确不知道那算是误打误撞的理解。构造这个验证例子之前,static和synchronized的共同使用让我对synchronized隐式锁有了更清晰的认识。所以打算再写写来分享这段体会。
volatile关键字
Volatile是JDK7里的核心,为什么,随便看一下LinkedBlockQueue, head 等关键就会发现不懂volatile那什么都是扯。。
volatile就是被认为“轻量级的synchronized”,但是使用其虽然可以简化同步的编码,并且运行开销相对于JVM没有优化的竞争线程同步低,但是滥用将不能保证程序的正确性。锁的两个特性是:互斥和可见。互斥保证了同时只有一个线程持有对象锁进行共享数据的操作,从而保证了数据操作的原子性,而可见则保证共享数据的修改在下一个线程获得锁后看到更新后的数据。volatile仅仅保证了无锁的可见性,但是不提供原子性操作的保证!这是因为volatile关键字作用的设计是JVM阻止volatile变量的值放入处理器的寄存器,在写入值以后会被从处理器的cache中flush掉,写到内存中去。这样读的时候限制处理器的cache是无效的,只能从内存读取值,保证了可见性。从这个实现可以看出volatile的使用场景:多线程大量的读取,极少量或者一次性的写入,并且还有其他限制。 由于其无法保证“读-修改-写”这样操作的原子性(当然java.util.concurrent.atomic包内的实现满足这些操作,主要是通过CAS--比较交换的机制,后续会尝试写写。),所以像++,--,+=,-=这样的变量操作,即使声明volatile也不会保证正确性。围绕这个原理的主题,我们可以大致的整理一下volatile代替synchronized的条件:对变量的写操作不依赖自身的状态。所以除了刚刚介绍的操作外,例如: - private volatile boolean flag;
- if(!flag) {
- flag == true;
- }
类似这样的操作也是违反volatile使用条件的,很可能造成程序的问题。所以使用volatile的简单场景是一次性的写入之后,大量线程的读取并且不再改变变量的值(如果这样的话,都不是并发了)。这个关键字的优势还是在于多线程的读取,既保证了读取的低开销(与单线程程序变量差不多),又能保证读到的是最新的值。所以利用这个优势我们可以结合synchronized使用实现低开销读写锁: -
-
-
-
-
- public class AnotherSyncSample {
- private volatile int counter;
-
- public int getCounter() {
- return counter;
- }
-
- public synchronized void add() {
- counter++;
- }
- }
这个简单的例子在读的方法上没有使用synchronized关键字,所以读的操作几乎没有等待;而由于写的操作是原子性的违反了使用条件,不能得到保证,所以使用synchronized同步得到写的正确性保证,这个模型在多读取少写入的实际场景中应该要比都用synchronized的性能有不小的提升。 另外还有一个使用volatile的好处,得自于其原理:内部禁止改变两个volatile变量的赋值或者初始化顺序,并且严格限制volatile变量和其周围非volatile变量的赋值或者初始化顺序。 -
-
-
-
-
- public class VolatileTest {
- public static void main(String[] args) {
- final VolatileSample sample = new VolatileSample();
-
- new Thread(new Runnable(){
- public void run() {
- sample.finish();
- }
- }).start();
-
- new Thread(new Runnable(){
- public void run() {
- sample.doSomething();
- }
- }).start();
- }
- }
-
- class VolatileSample {
- private volatile boolean finished;
- private int lucky;
-
- public void doSomething() {
- if(finished) {
- System.out.println("lucky: " + lucky);
- }
- }
-
- public void finish() {
- lucky = 7;
- finished = true;
- }
- }
这里首先线程A执行finish(),完成finished变量的赋值后,线程B进入方法doSomething()读到了finish的值为true,打印lucky的值,预想状态下为7,这样完美的执行结束了。但是,事实是如果finished变量不是声明了volatile的话,过程就有可能是这样的:线程A执行finish()先对finished赋值,与此同时线程B进入doSomething()得到finished的值为true,打印lucky的值为0,镜头切回线程A,接着给lucky赋值为7,可怜的是这个幸运数字不幸杯具了。因为这里发生了扯淡的事情:JVM或许为了优化执行把两者的赋值顺序调换了。这个结果在单线程的程序中简直绝对一定肯定就是不可能,遗憾的是多线程存在这个隐患。
ThreadLocal
作为一个JDK5以后支持范型的类,主要是想利用范型把非线程安全的共享变量,封装成绑定线程的安全不共享变量。这样的解释我想我们多半能猜出它的实现思路:把一个共享变量在每个线程使用时,初始化一个副本,并且和线程绑定。以后所有的线程对共享变量的操作都是对线程内部那个副本,完全的线程内部变量的操作。 要实现这样功能类的设计,主要技术点是要能把副本和线程绑定映射,程序可以安全查找到当前线程的副本,修改后安全的绑定给线程。所以我们想到了Map的存储结构,ThreadLocal内部就是使用了线程安全的Map形式的存储把currentThread和变量副本一一映射。 既然要把共享的变成不共享的,那么就要变量满足一个场景:变量的状态不需要共享。例如无状态的bean在多线程之间是安全的,因为线程之间不需要同步bean的状态,用了就走(很不负责啊),想用就用。但是对于有状态的bean在线程之间则必须小心,线程A刚看到状态是a,正想利用a做事情,线程B把bean的状态改为了b,结果做了不该做的。但是如果有状态的bean不需要共享状态,每个线程看到状态a或者b都可以做出自己的行为,这种情况下不同步的选择就是ThreadLocal了。 利用ThreadLocal的优势就在于根本不用担心有状态的bean为了状态的一致而牺牲性能,去使用synchronized限制只有一个线程在同一时间做出关于bean状态的行为。而是多个线程同时根据自己持有的bean的副本的状态做出行为,这样的转变对于并发的支持是那么的不可思议。例如一个Dao内有个Connection的属性,当多个线程使用Dao的同一个实例时,问题就来了:多个线程用一个Connection,而且它还是有连接,关闭等等的状态转变的,我们很敏感的想到这个属性不安全!再看这个属性,其实它是多么的想告诉线程哥哥们:我的这些状态根本就不想共享,不要因为我的状态而不敢一起追求。线程哥哥们也郁闷:你要是有多胞胎姐妹该多好啊!这时候ThreadLocal大哥过来说:小菜,我来搞定!你们这些线程一人一个Connection,你想关就关,想连接就连接,再也不用抱怨说它把你的连接关了。这样Dao的实例再也不用因为自己有个不安全的属性而自卑了。当然ThreadLocal的思路虽然是很好的,但是官方的说法是最初的实现性能并不好,随着Map结构和Thread.currentThread的改进,性能较之synchronized才有了明显的优势。所以要是使用的是JDK1.2,JDK1.3等等,也不要妄想麻雀变凤凰... 再看ThreadLocal和synchronized的本质。前者不在乎多占点空间,但是绝对的忍受不了等待;后者对等待无所谓,但是就是不喜欢浪费空间。这也反映出了算法的一个规律:通常是使用场景决定时间和空间的比例,既省时又省地的算法多数情况下只存在于幻想之中。下面写个简单的例子解释一下,不过个人觉得设计的例子不太好,以后有实际的启发再替换吧。 - import java.util.concurrent.atomic.AtomicInteger;
-
- /**
- * User: yanxuxin
- * Date: Dec 14, 2009
- * Time: 9:26:41 PM
- */
- public class ThreadLocalSample extends Thread {
- private OperationSample2 operationSample;
-
- public ThreadLocalSample(OperationSample2 operationSample) {
- this.operationSample = operationSample;
- }
-
- @Override
- public void run() {
- operationSample.printAndIncrementNum();
- }
-
- public static void main(String[] args) {
-
- final OperationSample2 operation = new OperationSample2();//The shared Object for threads.
-
- for (int i = 0; i < 5; i++) {
- new ThreadLocalSample(operation).start();
- }
- }
- }
-
- class OperationSample {
- private int num;
-
- //public synchronized void printAndIncrementNum() {
- public void printAndIncrementNum() {
- for (int i = 0; i < 2; i++) {
- System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");
- num += 10;
- }
- }
- }
-
- class OperationSample2 {
-
- private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 0;
- }
- };
-
- public void printAndIncrementNum() {
- for (int i = 0; i < 2; i++) {
- int num = threadArg.get();
- threadArg.set(num + 10);
- System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");
- }
- }
- }
-
- class OperationSample3 {
-
- private static final AtomicInteger uniqueId = new AtomicInteger(0);
- private static ThreadLocal<Integer> threadArg = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return uniqueId.getAndIncrement();
- }
- };
-
- public void printAndIncrementNum() {
- for (int i = 0; i < 2; i++) {
- int num = threadArg.get();
- threadArg.set(num + 10);
- System.out.println(Thread.currentThread().getName() + "[id=" + num + "]");
- }
- }
- }
这个例子中ThreadLocalSample继承自Thread持有OperationSample三个版本中的一个引用,并且在线程运行时执行printAndIncrementNum()方法。 首先看版本1:OperationSample有个共享变量num,printAndIncrementNum()方法没有同步保护,方法就是循环给num赋新值并打印改变值的线程名。因为没有任何的同步保护,所以原本打算每个线程打印出的值是相邻递加10的结果变成了不确定的递加。有可能线程1的循环第一次打印0,第二次就打印50。这时候我们使用被注释的方法声明,结果就是预想的同一个线程的两次结果是相邻的递加,因为同一时刻只有一个线程获得OperationSample实例的隐式锁完成循环释放锁。 再看版本2:假设我们有个递增10的简单计数器,但是是对每个线程的计数。也就是说我们有一个Integer计数器负责每个线程的计数。虽然它是有状态的,会变的,但是因为每个线程之间不需要共享变化,所以可以用ThreadLocal管理这个Integer。在这里看到我们的ThreadLocal变量的initialValue()方法被覆写了,这个方法的作用就是当调用ThreadLocal的get()获取线程绑定的副本时如果还没绑定则调用这个方法在Map中添加当前线程的绑定映射。这里我们返回0,表示每个线程的初始副本在ThreadLocal的Map的纪录都是0。再看printAndIncrementNum()方法,没有任何的同步保护,所以多个线程可以同时进入。但是,每个线程通过threadArg.get()拿到的仅仅是自己的Integer副本,threadArg.set(num + 10)的也是自己的副本值。所以结果就是虽然线程的两次循环打印有快有慢,但是每个线程的两次结果都是0和10。 最后是版本3:和版本2的不同在于新加了一个uniqueId的变量。这个变量是java.util.concurrent.atomic包下的原子变量类。这是基于硬件支持的CAS(比较交换)原语的实现,所以保证了++,--,+=,-=等操作的原子性。所以在ThreadLocal变量的initialValue()方法中使用uniqueId.getAndIncrement()将为每个线程初始化唯一不会重复的递加1的Integer副本值。而结果就会变成5个线程的首次打印是0~4的5个数字,第二次每个线程的打印是线程对应的首次数字加10的值。 对于ThreadLocal的使用,Spring的源码中有大量的应用,主要是要支持Singleton的实例管理,那么自身的一些Singleton的实现内非线程安全的变量,属性要用ThreadLocal隔离共享。同时我们在使用Spring的IOC时也要注意有可能多线程调用的注册到IOC容器的Singleton型实例是否真的线程安全。另外java.util.concurrent.atomic内的原子变量类简单的提了一下,再看看怎么能瞎编出东西来吧。
posted on 2012-06-25 09:37
Daniel 阅读(1391)
评论(0) 编辑 收藏 所属分类:
CoreJava