多线程优点
- 资源利用率更好(在发生IO等待时,利用处理器做其他事情)
- 程序设计在某些情况下更简单
-
程序响应更快
多线程代价
- 设计复杂(线程交互复杂,错误难以发现,重现和修复)
- 上下文切换开销
-
增加资源消耗(需要内存维持线程的本地堆栈)
创建和运行Java线程
- 创建Thread的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class MyThread extends Thread { public void run(){ System.out.println("MyThread running"); } } MyThread myThread = new MyThread(); myTread.start();
Thread thread = new Thread(){ public void run(){ System.out.println("Thread Running"); } }; thread.start();
|
- 实现Runable接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class MyRunnable implements Runnable { public void run(){ System.out.println("MyRunnable running"); } } Thread thread = new Thread(new MyRunnable()); thread.start();
Runnable myRunnable = new Runnable(){ public void run(){ System.out.println("Runnable running"); } } Thread thread = new Thread(myRunnable); thread.start();
|
- 线程名
1 2 3 4 | MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable, "New Thread"); thread.start(); System.out.println(thread.getName());
|
线程安全和共享资源
- 局部变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。 - 局部的对象引用
对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。 如果在某个方法中创建的对象不会逃逸出(即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。 实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。 - 对象成员
对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。 -
线程控制逃逸规则
1.如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
2.即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。 比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。线程安全及不可变性
- 创建不可变的共享对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class ImmutableValue{ private int value = 0; //没有set方法
public ImmutableValue(int value){ this.value = value; }
public int getValue(){ return this.value; }
/* **构造新对象返回 */ public ImmutableValue add(int valueToAdd){ return new ImmutableValue(this.value + valueToAdd); } }
|
- ImmutableValue类是线程安全的,但使用它的类未必安全,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public void Calculator{ private ImmutableValue currentValue = null;
public ImmutableValue getValue(){ return currentValue; }
public void setValue(ImmutableValue newValue){ this.currentValue = newValue; }
public void add(int newValue){ this.currentValue = this.currentValue.add(newValue); } } |
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可
Java同步块
- 实例方法同步(锁定实例对象,一个实例一个线程)
1 2 3 | public synchronized void add(int value){ this.count += value; }
|
- 静态方法同步(锁定Class对象,一个类一个线程)
1 2 3 | public static synchronized void add(int value){ count += value; }
|
- 实例方法中的同步块(锁定this对象,一个this对象一个线程)
1 2 3 4 5 | public void add(int value){ synchronized(this){ this.count += value; } }
|
- 静态方法中的同步块(锁定Class对象,一个类一个线程)
1 2 3 4 5 6 7 8 | public class MyClass { public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2); } } }
|
线程通信
- 通过共享对象通信
1 2 3 4 5 6 7 8 9 10 11 12 | public class MySignal{
protected boolean hasDataToProcess = false;
public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; }
public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
|
- 忙等待
1 2 3 4 5 6 7 | protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
|
- wait(),notify()和notifyAll()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class MonitorObject{ }
public class MyWaitNotify{
MonitorObject myMonitorObject = new MonitorObject();
public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } }
public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
|
- 丢失的信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class MyWaitNotify2{
MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false;
public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ //防止notify之后才wait,再也无法醒来 try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } }
public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
|
- 假唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class MyWaitNotify3{
MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false;
public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ //如果发生假唤醒,wasSignalled为false,将进入循环再次睡眠 try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } }
public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
|
-
多个线程等待相同信号 如果你有多个线程在等待,被notifyAll()唤醒,但只有一个被允许继续执行,使用while循环也是个好方法。 每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出wait()调用并清除wasSignalled标志(设为false)。 一旦这个线程退出doWait()的同步块,其他线程退出wait()调用,并在while循环里检查wasSignalled变量值。 但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。
- 不要在字符串常量或全局对象中调用wait()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class MyWaitNotify{
String myMonitorObject = ""; boolean wasSignalled = false;
public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } }
public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } } |
在空字符串作为锁的同步块(或者其他常量字符串)里调用wait()和notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。 这意味着,即使你有2个不同的MyWaitNotify实例,它们都引用了相同的空字符串实例。 同时也意味着存在这样的风险:在第一个MyWaitNotify实例上调用doWait()的线程会被在第二个MyWaitNotify实例上调用doNotify()的线程唤醒。
死锁
简单的死锁
Thread 1 locks A, waits for B
Thread 2 locks B, waits for A
更复杂的死锁
Thread 1 locks A, waits for B
Thread 2 locks B, waits for C
Thread 3 locks C, waits for D
Thread 4 locks D, waits for A
数据库的死锁
Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.
避免死锁
- 加锁顺序
- 加锁时限
- 死锁检测
-
加锁顺序(按照相同的顺序获得锁)
Thread 1
lock A
lock B
Thread 2
wait for A
lock C (when A locked)
Thread 3
wait for A
wait for B
wait for C
-
加锁时限
Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.
如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)
-
死锁检测
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。
当检测出死锁时:
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级
饥饿和公平
1、Java中导致饥饿的原因:
- 高优先级线程吞噬所有低优先级线程的CPU时间(线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来说,你最好是不要改变其优先级值)
- 线程被永久堵塞在一个等待进入同步块的状态(运气太差,总是得不到运行机会)
-
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法,运气太差,总是没有被唤醒)
2、在Java中实现公平性方案,需要:
- 使用锁,而不是同步块
- 公平锁
-
注意性能方面
公平锁代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | public class FairLock { private boolean isLocked = false; private Thread lockingThread = null; private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();
public void lock() throws InterruptedException{ QueueObject queueObject = new QueueObject(); boolean isLockedForThisThread = true; synchronized(this){ waitingThreads.add(queueObject); }
while(isLockedForThisThread){ synchronized(this){ isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject; if(!isLockedForThisThread){ isLocked = true; waitingThreads.remove(queueObject); lockingThread = Thread.currentThread(); return; } } try{ queueObject.doWait(); }catch(InterruptedException e){ synchronized(this) { waitingThreads.remove(queueObject); } throw e; } } }
public synchronized void unlock(){ if(this.lockingThread != Thread.currentThread()){ throw new IllegalMonitorStateException( "Calling thread has not locked this lock"); } isLocked = false; lockingThread = null; if(waitingThreads.size() > 0){ waitingThreads.get(0).doNotify(); } } }
/*****************************************************/ public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while(!isNotified){ this.wait(); }
this.isNotified = false;
}
public synchronized void doNotify() { this.isNotified = true; this.notify(); }
public boolean equals(Object o) { return this == o; } } |
FairLock新创建了一个QueueObject的实例,并对每个调用lock()的线程进行入队列。调用unlock()的线程将从队列头部获取QueueObject,并对其调用doNotify(),以唤醒在该对象上等待的线程。通过这种方式,在同一时间仅有一个等待线程获得唤醒,而不是所有的等待线程。这也是实现FairLock公平性的核心所在。
还需注意到,QueueObject实际是一个semaphore。doWait()和doNotify()方法在QueueObject中保存着信号。这样做以避免一个线程在调用queueObject.doWait()之前被另一个调用unlock()并随之调用queueObject.doNotify()的线程重入,从而导致信号丢失。queueObject.doWait()调用放置在synchronized(this)块之外,以避免被monitor嵌套锁死,所以另外的线程可以解锁,只要当没有线程在lock方法的synchronized(this)块中执行即可。
最后,注意到queueObject.doWait()在try – catch块中是怎样调用的。在InterruptedException抛出的情况下,线程得以离开lock(),并需让它从队列中移除。