声明和开启一个线程 当我们创建一个线程时,必须告诉这个线程将要运行什么代码!有两种方式来实现它: 提供一个Runnable对象:Runnable是一个接口,接口定义了唯一一个方法run(),这个方法的中代码就是线程要执行的代码。当我们创建一个Thread实例的时候,需要把Runnable对象(实现了该接口的类的实例)作为Thread构造方法的参数。例如: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class HelloRunnable implements Runnable {
@override
public void run (){//新线程要执行的代码
System.out.println("a thread");
}
public static void main(String[] args){
HelloRunnable rObject = new HelloRunnable();//创建一个Runnable对象
Thread newThread = new Thread(rObject);//创建新线程,把Runnable对象作为Thread构造方法的参数
newThread.start();//启动新线程
}
}
|
子类化Thread:Thread类本身实现了Runnable接口,但是Thread类中的run()没有做任何事情。所以你可以创建一个Thread类的子类,提供你自己的run的实现。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class HelloThread extends Thread {
@override
public void run(){
System.out.println("a thread");
}
public static void main(String[] args){
HelloThread newThread = new HelloThread();//创建新线程
newThread.start();//启动新线程
}
}
|
同步多个线程的之间的通信主要是通过访问共享数据,例如字段和字段引用的对象,这种通信的方式是极其方便的,但是也可能会产生两中错误—–线程干扰和内存一致性错误。阻止这些错误的工具就是同步(synchronization)。 线程干扰(Thread Interference) 思考下面的这个类Counter 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
|
在这个类中,每调用一次方法increment()成员变量c就会加1,每调用一次方法decrement()成员变量c就减1。然而,如果一个Counter 对象被多个线程引用,那么这些线程之间的相互干扰可能无法得到预期结果。 线程干扰(Thread Interference):当两个运算作用同一个数据,且这个运算在不同的线程上运行。(注:这个两个运算是多个步骤组成的) 例如:上面代码中的表达式c++,能被分解成3步: - 取出(retrieve)当前c的值
- 把取出的值进行加1就算
- 把加1计算出的值,储存到c中
当然表达式c--也会以同样的方式分解。 如果有一个线程A调用increment,与此同时另个线程B调用decrement。假如c的初始值为0。那么两个方法可能是交叉执行的,其顺序可能是这样的: - Thread A: 取出c的值
- Thread B: 取出c的值
- Thread A: 对取出的值进行加1运算。结果为 1。
- Thread B: 对取出的值进行减1运算。结果为 -1。
- Thread A: 把计算的结果储存到c中,则c的值为 1。
- Thread B: 把计算的结果储存到c中,则c的值为 -1。
我们可以看到最终c的值为-1,线程A的结果丢失了,被线程B重写了。上面的情况仅仅是线程干扰的一种可能,在不同的环境下,有可能线程B的结果丢失,也有可能线程没有发生干扰结果正确。由于线程干扰的结果是不可预测的,所以线程干扰导致的bugs是难以侦测和修改的。 内存一致性错误(Memory Consistency Errors) 关于什么是内存一致性错误?形象的一点说就是:当多个线程对同一个数据应该是什么样子持有不同意见(英文原文:Memory consistency errors occur when different threads have inconsistent views of what should be the same data)。内存一致性错误产生的原因是复杂的,超出我们讨论的范围。其实我们没有必要知道产生它的原因,我们只需知道如何避免它的发生就好。 理解内存一致性错误的关键是理解偏序关系(happens-before relationship),这种关系也保证了一个对内存的写操作的影响对另一个操作是可见的。为了理解它,我们看一个例子,假如有一个int类型的字段counter的声明和初始化如下 这个字段被两个线程A,B共享,如果线程A对counter经行加1操作 线程B来打印counter的值。 1
| System.out.println(counter);
|
如果上面的两个语句在同一线程执行,那么输出的值为1。但是两个语句分别在两个线程中执行,那么打印的值可能是0。 因为不能保证线程A对counter的改变对线程B是可见的,除非两个语句建立了偏序关系(简单的理解就是两个语句的执行顺序) 有许多方法来建立偏序关系,其中之一就是同步(synchronization)。
同步方法 Java语言提供两种基本的同步语法:同步方法和同步语句。 要使方法同步,只需将同步关键字synchronization添加到方法的声明中即可。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
|
如果counter_obj是SynchronizedCounter的一个实例。那么对同步方法的调用会有如下影响。 - 如果在一线程中counter_obj 调用了某一同步方法,在另一线程中counter_obj也调用了某一同步方法,那么两个同步方法不可能在两个线程中交替执行(也就是阻止了线程干扰的可能)。只有一个线程中同步方法执行完,另一个线程的同步方法才能执行。因为当线程中counter_obj调用同步方法时,该线程就会得到counter_obj的内部锁(intrinsic lock或者minitor lock ),如果其他线程中counter_obj再调用同步方法时就会等待,直到持有counter_obj内部锁的线程把锁释放掉(即同步方法执行完)
- 其次,当一个同步方法存在时,它会自动与同一实例后续的同步方法调用建立偏序关系。这保证对该实例改变对其他线程是可见的。
需要注意的是构造方法不能声明为同步的,在构造方法的声明上使用synchronization关键字是语法错误。 同步语句上一节我们在讨论同步方法的时候,出现了一个概念内部锁(intrinsic lock)。在讨论同步语句之前,我们来学习一下内部锁和同步的关系。 其实同步是建立在一个名为内部锁(intrinsic lock ),也叫监视器锁(minitor lock),在API文档中也称之为监视器(minitor)。每一个实例都一个与之关联的内部锁。 当一个线程想要单独的访问某实例的字段时,线程不得不先要获得这个实例的内部锁,线程完成操作后就会释放内部锁。在线程获得内部锁和线程释放内部锁这段期间内,我们称之为线程持有内部锁。只要一线程持有某实例的内部锁,试图获得这个内部锁的其他线程那么就会阻塞。也就是同一时刻只能有一个线程持有同一内部锁。 当同步方法在线程执行时,线程就会自动的获得同步方法调用者的内部锁。同步方法返回后(也就是方法执行完毕)线程释放内部锁。你可能会有疑惑一个静态(static)的同步方法被调用将会发生什么,因为静态方法的调用者是类,而不是一个实例。因为每个类其实就是Class类的一个实例,因此每个类中也有与之相关的内部锁,当调用静态同步方法的时候,线程会自动获得当前类的内部锁。 下面我们看看同步语句的语法: 1
2
3
4
5
6
7
| public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
|
在上面的例子中我们能看到,首先,与同步方法不同,同步语句必须指定提供内部锁的实例。其次,addName 方法同步 改变lastName和nameCount,又避免了同步调用其他实例方法,这里的“其他实例方法的调用”指的是nameList.add(name),因为在同步代码中调用其他实例的方法可能产生被称为liveness的问题(以后我们会讨论这个问题)。所以同步在并发时能够实现更细粒度的同步。 下面我们再看一个例子 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
|
类MsLunch有两个个字段C1和C2,它们永远不会同时使用。这些字段的所有更新必须同步,但允许他们更新时能够交错执行。我们就可以像例子那样,使用两个对象提供锁,以代替同步方法和使用this的同步语句。这样减少了不必要的阻塞增加了并发性。 可重入同步通过前面的学习我们知道,一个线程不能获得以被其他线程持有的内部锁,但是一个线程能够再次获得自已已持有的锁。也就是说同一线程可以多次获得同一把内部锁,这保证可重入同步。留一个思考,想想下面的描述的场景如果不支持可重入同步将会发生什么现象?——–当同步代码中直接或者间接的调用了一个方法,而这个方法也有同步代码,并且这两个同步代码使用的是同一把锁。
|