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
public class LeftRightDeadLock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() throws InterruptedException {
synchronized (left) {
Thread.sleep(1000);
synchronized (right) {
System.out.println("leftRight...");
}
}
}
public void rightLeft() throws InterruptedException {
synchronized (right) {
Thread.sleep(1000);
synchronized (left) {
System.out.println("rightLeft...");
}
}
}
public static void main(String[] args) {
LeftRightDeadLock lr = new LeftRightDeadLock();
new AThread(lr).start();
new BThread(lr).start();
}
}
class AThread extends Thread{
private LeftRightDeadLock lr;
public AThread(LeftRightDeadLock lr) {
this.lr = lr;
}
@Override
public void run(){
try {
lr.leftRight();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class BThread extends Thread{
private LeftRightDeadLock lr;
public BThread(LeftRightDeadLock lr) {
this.lr = lr;
}
@Override
public void run(){
try {
lr.rightLeft();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行时候将什么也没有输出,因为两个线程都在等待对方释放锁。

通常解决的方法有下面两种:

  1. 按照顺序加锁:例如将上面的加锁顺序改为都是先对right加锁,然后才对left加锁。
  2. 使用定时锁,避免进入无限循环。例如可以显式使用Lock类中的定时tryLock功能来代替内置锁机制。

饥饿


当线程由于无法访问它所需要的资源而不能继续执行时,就发生饥饿

引发饥饿的最常见的资源就是CPU的时钟周期。如果在Java程序中对线程的优先级使用不当,或者在持有锁时执行一些无法结束的方法(例如无限循环),那么也可能导致饥饿,因为其他需要这个锁的线程将无法得到它。

通常,我们尽量不要改变线程的优先级,因为改变了优先级,程序的行为将和平台有关,并且会导致发生饥饿问题的风险。

活锁


活锁是另外一种形式的活跃性问题。该问题虽然不会阻塞线程,但是也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。

当多个相互协作的线程对于彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上相遇,他们彼此都让出了对方的路,然而又在另外一条路上遇上了。因此他们就这样反复地避让下去。

要解决这种活锁问题,需要在重试机制中引入随机性。通过等待随机长度的时间和回退可以有效地避免活锁的产生。

性能问题


线程的最主要的目的是提高程序的运行性能。

尽管使用多个线程的目标是提升整体性能,但与单线程的方法相比,使用多个线程总会引入一些额外的性能开销。

造成这些开销的操作包括:线程之间的协调(例如加锁、触发信号以及内存同步)、增加的上下文切换、线程的创建和销毁、以及线程的调度等。

通常为了减少这些开销,提高整体的吞吐量和伸缩性,可以通过下面的方法:

  1. 减少锁的持有时间
  2. 降低锁的粒度:例如可以使用同步块代替同步方法,或者使用显式锁来代替内置锁。
  3. 使用非独占的锁或者非阻塞的锁来代替锁:例如并发容器、volatile变量、原子变量等。