这一章主要讲述线程之间数据的共享,数据共享最大的难点就是资源竞争
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的对象锁,两边互不相让,然后就死锁了~