这一章主要讲述线程之间数据的共享,数据共享最大的难点就是资源竞争


3.1 Synchronized关键字的使用 (The Synchronized Keyword)

书中例子太繁琐了,我找了一个简单的例子


package com.yellow.chapteThree;
public class Test implements Runnable {
    public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()
                        + " synchronized loop " + i);
            }
    }
    public static void main(String[] args) {
        Test t = new Test();
        Thread ta = new Thread(t, "A");
        Thread tb = new Thread(t, "B");
        ta.start();
        tb.start();
    }
}


我们让两个线程从1打印到5,结果我们会发现,在A打印的过程中B也在打印,两个线程都进入了这个方法,那怎么办呢~


第一个办法,使用synchronized代码块


package com.yellow.chapteOne;
public class Test implements Runnable {
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()
                        + " synchronized loop " + i);
            }
        }
    }
    public static void main(String[] args) {
        Test t = new Test();
        Thread ta = new Thread(t, "A");
        Thread tb = new Thread(t, "B");
        ta.start();
        tb.start();
    }
}


synchronized是由监听器(monitors)实现的,每个对象都有一个monitors,所以synchronized()的括号里面可以填任意对象,当一个线程试图进入一个synchronized代码块的时候,必须得到这个代码块的monitors,一旦有一个线程得到了这个代码块的monitors后,其他所有在同一monitors下的线程都必须等待,获得monitors的线程执行完代码后会自动释放monitors的所有权,好让其他线程进入


我们让synchronized使用t对象的monitors,很明显,A,B两个线程处于同一个monitor下面,当A抢到资源执行run方法,B只能等待


如果我们稍作变化如下:


package com.yellow.chapteOne;
public class Test implements Runnable {
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()
                        + " synchronized loop " + i);
            }
        }
    }
    public static void main(String[] args) {
        Test t1 = new Test();
        Test t2 = new Test();
        Thread ta = new Thread(t1, "A");
        Thread tb = new Thread(t2, "B");
        ta.start();
        tb.start();
    }
}


我们会发现结果并没有还是会交替打印,没有起到同步的作用,因为,A,B两个线程处于不同对象的monitor,可以同时进入synchronized代码块


第二种方式就是使用synchronized方法,如下


package com.yellow.chapteOne;
public class Test implements Runnable {
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()
                    + " synchronized loop " + i);
        }
    }
    public static void main(String[] args) {
        Test t = new Test();
        Thread ta = new Thread(t, "A");
        Thread tb = new Thread(t, "B");
        ta.start();
        tb.start();
    }
}


非static的synchronized方法用的是this的monitor,static的synchronized方法用类的class对象的monitor


3.2 Volatile 关键字(The Volatile Keyword)

把一个变量声明成volatile意味着这个变量的值将不会换成在线程的本地空间,而是直接操作main memory


直接上个例子来解释:


public class VolatileObjectTest {
    /**
     * 相信绝大多数使用JAVA的人都没试出volatile变量的区别。献给那些一直想知道volatile是如何工作的而又试验不出区别的人。
     * 成员变量boolValue使用volatile和不使用volatile会有明显区别的。 本程序需要多试几次,就能知道两者之间的区别的。
     *
     * @param args
     */
    public static void main(String[] args) {
        final VolatileObjectTest volObj = new VolatileObjectTest();
        Thread t2 = new Thread() {
            public void run() {
                System.out.println("t1 start");
                for (;;) {
                    volObj.waitToExit();
                }
            }
        };
        t2.start();
        Thread t1 = new Thread() {
            public void run() {
                System.out.println("t2 start");
                for (;;) {
                    volObj.swap();
                }
            }
        };
        t1.start();
    }
     boolean boolValue;// 加上volatile 修饰的是时候,程序会很快退出,因为volatile
                        // 保证各个线程工作内存的变量值和主存一致。所以boolValue == !boolValue就成为了可能。
    public void waitToExit() {
        if (boolValue == !boolValue)
            System.exit(0);// 非原子操作,理论上应该很快会被打断。实际不是,因为此时的boolValue在线程自己内部的工作内存的拷贝,因为它不会强制和主存区域同步,线程2修改了boolValue很少有机会传递到线程一的工作内存中。所以照成了假的“原子现象”。
    }
    public void swap() {// 不断反复修改boolValue,以期打断线程1.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolValue = !boolValue;
        System.out.println(boolValue);
    }
}


我们用volatile用的最广泛的地方是 作为停止请求的标志( "stop request" flag ),好吧,我也知道翻译的不像人话,看代码


public class StoppableTask extends Thread {
  private volatile boolean pleaseStop;
  public void run() {
    while (!pleaseStop) {
      // do some stuff...
    }
  }
  public void tellMeToStop() {
    pleaseStop = true;
  }
}


如果pleaseStop变量没有被声明为volatile的话,别的线程改变了pleaseStop的值,而这个线程不知道(因为它不会从主存里面同步,会读自己本地空间),就会一直循环下去


volatile和synchronized的区别:


1)volatile可以修饰primitive变量,synchronized不能

2)线程进入synchronized的时候会获得锁,但是volatile不会获得锁

3)因为进入volatile没有获得锁,所以不要试图用volatile实现原子操作

4)volatile可以修饰null


然后我查资料的时候发现了一个有意思的东西,可以用 volatile和双重检查加锁可以实现多线程单例模式


1

/**
 * 双重检查加锁 单例模式,据说JDK1.5之后可以用  volatile 和 双重检查加速来实现单例模式
 * @author yellowbaby
 *
 */
public class SingletonOne {
    /**
     *  volatile 的作用是让变量不再缓存在当前线程的本地空间,而是直接去操作main memory
     *  我的问题是,这里为什么要使用 volatile?
     */
    private volatile static SingletonOne singleton = null;
    public SingletonOne() {
    }
    public static SingletonOne getInstance() {
        if (null == singleton) {// 1 检查实例,如果不存在就进入同步区块
            synchronized (SingletonOne.class) {// 2  注意,只有第一次才彻底执行这里的代码
                if (null != singleton) {// 3
                    singleton = new SingletonOne();
                }
            }
        }
        return singleton;
    }
}


假如不用 volatile会发生什么呢?


假设同时有两个线程(A和B)进入了1的if块,A进入2,B等待,A出了Syn块,B进入,B判断3这个时候它会直接从自己的线程内存中读取singleton的值,发现为空然后就会又new一个出来


然后分享另一种多线程单例模式~


/**
 * 使用内部静态类来得到实例,因为只有在调用InnerSingleFactory.SINGLETON的时候才会加载SingletonTwo,所以也是懒汉型
 * @author yellowbaby
 *
 */
public class SingletonTwo {
        private volatile static SingletonTwo singleton = null;
        private SingletonTwo() {
        }
        public static SingletonTwo getInstance() {
                if(singleton == null){
                        singleton = InnerSingleFactory.SINGLETON;
                }
                return singleton;
        }
                                                                                                                                                                                                                                                                                                                                                                                                                                   
        private final static class InnerSingleFactory {
                final static SingletonTwo SINGLETON = new SingletonTwo();
        }
}


用static来解决同步是个好办法,但是常规的使用static的写法是饿汉型的,但是如果丢在内部类里面就可以解决这个麻烦的问题了


3.3 资源竞争(More on Race Conditions)


什么是资源竞争?


race condition 发送在 两个线程共享同一个数据,并且试图同时修改它,因为线程调度算法让线程执行的先后不是固定的,数据最终改变的结果取决于线程的执行顺序


问题往外出现在 “check-then-act”的操作中


举个例子


if (x == 5) // The "Check"检查 1
{
   y = x * 2; // The "Act" 操作 2
                                                                                                                                                                                                                                                                                                                           
   //  如果另一个线程在 1 和 2 直接修改了 x 的值,那结果就不会是 10 了
}



为了解决这个问题,我们需要在合适的地方加上锁来确保只有一个线程修改这个数据


// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released.
              // Therefore y = 10
}
// release lock for x


一般都是使用synchronized代码块或者synchronized方法来同步,前面我面提到过的~


3.4 显示锁(Explicit Locking)


上面说了synchronized的一些用法,synchronized是好,但是并不是完美的,比如你不能中断一个正在等待的线程,有可能一个线程得不到锁就一直傻傻的等下去,JDK 5 之后出现了一个新东西既可以实现synchronized的作用也可以实现这些它做不到的东西,这就是 显示锁


看例子


Lock lock = new ReentrantLock();
        lock.lock();
        try {
          // 等价于加上了synchronized代码块
        }
        finally {
          lock.unlock();
        }


显示锁在性能上面比synchronized强,而且可以实现一些synchronized不能实现的,但是也有缺点


1)必须要手动释放锁,使用synchronized会自动的释放锁,一旦忘记就能难查出问题出现在什么地方

2)还有,显示锁不兼容 JDK 1.5 以前的版本


那什么时候用显示锁呢,答案就是,在你真正需要某些synchronized无法实现的功能的时候,大部分时候synchronized是可以足够的


3.8 死锁(Deadlock)

死锁就是两个线程都在等待对方释放释放资源,导致两个线程一直堵塞

看一个例子

public class MyDeadLock{
    public static void main(String[] args) {
                                                                        
        final Robber robber = new Robber();
        final Victim victim = new Victim();
        robber.setVictim(victim);
        victim.setRobber(robber);
                                                                        
        new Thread(new Runnable() {
                                                                            
            @Override
            public void run() {
                robber.rob();
            }
        }).start();
                                                                        
        new Thread(new Runnable() {
                                                                            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                victim.beRobbed();
            }
        }).start();
    }
                                                                    
}
class Robber  {
                                                                    
    Victim victim;
                                                                    
    public Robber() {
                                                                        
    }
                                                                    
    synchronized void rob(){
        System.out.println("我是劫匪");
        victim.giveYouMoney();//给钱
        letYouGo();//让你走
    }
                                                                    
    synchronized void letYouGo(){
        System.out.println("放人");
    }
                                                                    
    public void setVictim(Victim victim) {
        this.victim = victim;
    }
}
class Victim  {
                                                                    
    Robber robber;
                                                                    
    public Victim() {
                                                                    
    }
                                                                    
    synchronized void beRobbed(){
        System.out.println("我是被抢劫的人");
        robber.letYouGo();//让我走
        giveYouMoney();//给钱
    }
                                                                    
    synchronized void giveYouMoney(){
        System.out.println("给你钱");
    }
                                                                    
    public void setRobber(Robber robber) {
        this.robber = robber;
    }
}


劫匪和受害者一个想要对方先给钱,一个想让对方先放人,一直在等待,然后就死锁了


当第一个robber对象进入rob方法时,得到了robber的对象锁,试图调用victim的方法,因为giveYouMoney是synchronized的,所以需要等待得到victim的对象锁,但是victim的对象锁被在调用beRobbed的时候被victim获得了,而也想得到robber的对象锁,两边互不相让,然后就死锁了~