理解线程的同步
多线程,能够让CPU同时处理多个任务,其实并不是同一时间,而是CPU使用了时间片轮选机制,而且速度很快,我们感觉起来好像是多个任务同时进行一样。
Java中,能通过两种方法使用线程。一是继承Thread类,二是实现Runnable接口。具体怎么使用,请参考相应的资料。该文档主要介绍的是多线程的同步问题。
我们能够创建许许多多的线程,来处理许许多多的事情。但线程的运行是不确定的,我们无法知道哪个线程会先被CPU执行,设置优先权也不能解决,因为CPU只要有一点点地空闲,它都会去执行其他的线程,高优先级线程只是抢资源比较狠一点而已。由于线程的不确定性,使得当多个线程同时对某一对象进行操作的时候,便有可能出现不可预知的情况。举个例子,当某个线程对一个文件进行操作,比如是往文件里写数据,而另一个线程又对该文件进行写数据,那最后这个文件里到底是什么数据呢?由于线程的不确定性,所以我们无法预知结果。怎么解决线程的不确定性呢?嗯,对了,线程的同步(synchronized),能使线程按照你自己的意愿,先处理什么,后处理什么。
在讲线程的同步之前,先讲讲同步线程(需要同步的线程)的几种状态吧。休眠状态、等待状态和执行状态是同步线程所特有的,其他的一些比如新状态、阻塞状态、死亡状态等是所有线程都有的,在这里不再讨论。
休眠状态:所有在同步代码中调用wait方法的线程都会进入该状态,在没有被唤醒或没到达休眠结束时间前,状态是不会改变的。休眠前,线程会自动记录本身的一些状态,比如局部变量的值等(非局部的值是不保存的)。休眠状态的线程会存放在休眠池中,由系统管理。
等待状态:被唤醒的或到达休眠结束时间的休眠线程,和进入同步代码(*1)的但没有得到钥匙(*2)的线程,都会进入等待状态。在没有得到钥匙前,线程的状态是不会改变的。等待状态的线程会存放在等待池中,由系统管理。只有在等待池中的线程,才有资格抢夺钥匙。
运行状态:即正在运行的线程。运行状态的线程,能够调用wait方法,让出钥匙,使自己休眠,进入到休眠池中,并会记录当前的运行状态,以便下次夺取钥匙的时候,能继续执行休眠前的动作。
注解:(*1:同步代码即同步方法或者同步块中的代码)
(*2:这里讲到的钥匙,即其他资料中所说的锁,得到锁的线程能够运行同步代码,使线程进入运行状态,本人觉得不太好理解,所以就换成个人觉得比较好理解的钥匙了)
转入正题:先看一下下面的程序代码
代码一:
public class ThreadTest implements Runnable {
public synchronized void run() {
for (int i = 0; i < 10; i++) {
System.out.print(" " + i);
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Runnable r1 = new ThreadTest();
Runnable r2 = new ThreadTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
运行结果为:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
代码二:
public class ThreadTest implements Runnable {
public synchronized void run() {
for (int i = 0; i < 10; i++) {
System.out.print(" " + i);
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
运行结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
两次的运行结果很显然不一样,代码一的同步方法好像没有起到作用,代码二的就起到作用了。为什么了?看清楚没有?对了,就是t1和t2所指向的线程所包含的Runnable对象不一致引起的。代码一中,t1包含的是r1,t2包含的是r2,显然r1和r2不是同一个对象,而同步代码针对的是同一对象,所以在这里不钩成同步。代码二中,t1包含r,t2也是包含r,两个线程处理同一个对象,所以同步方法便起了同步的作用,达到了程序的同步。
嗯,很好!知道了线程的同步是针对同一对象来说的,但为什么会这样?OK,接下来我便讲讲线程同步的机制吧。
再看一下下面的代码。
代码三:
public class ThreadTest implements Runnable {
public synchronized void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "抢到了钥匙,开始执行!");
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("/n" + name + "运行结束,释放了钥匙!");
}
public static void main(String[] args) {
Runnable r = new ThreadTest();
Thread t1 = new Thread(r, "t1");
Thread t2 = new Thread(r, "t2");
Thread t3 = new Thread(r, "t3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
t1抢到了钥匙,开始执行!
0 1 2 3 4 5 6 7 8 9
t1运行结束,释放了钥匙!
t2抢到了钥匙,开始执行!
0 1 2 3 4 5 6 7 8 9
t2运行结束,释放了钥匙!
t3抢到了钥匙,开始执行!
0 1 2 3 4 5 6 7 8 9
t3运行结束,释放了钥匙!
看到上面的运行结果没?没错,每个对象都有且仅有一把钥匙,只有得到该钥匙的线程能够执行该对象的同步代码,那是不是同步代码也需要钥匙才能执行呢?“不是,非同步的我们不管,你想执行就执行吧,我们只管同步代码”,系统很明确的说。嗯,没错,其实可以这样理解,同步代码都是加了锁的代码,只有得到钥匙后,才能把锁打开,进入到同步代码中,没钥匙的只能在门口吃西北风。当得到钥匙的线程执行完任务后,便把钥匙归还给对象,站在门口的线程一看到钥匙归还了,就开始争夺钥匙,强到钥匙的线程,便能够把锁打开,执行任务了。对照一下上面所讲的线程状态,站在门口(等待池)等的,是等待状态线程,它们等待钥匙,并准备争夺钥匙;得到钥匙的就不用多说了吧,是运行状态的线程;那休眠状态的在哪?没错,它们太懒惰了,没有人叫醒他们,他们都在家(休眠池)里睡觉。
上面都是用同步方法来解释线程的同步,现在开始用另一种同步代码—同步块来解释线程的同步吧。先看下面的代码。
代码四:
public class ThreadTest implements Runnable {
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "的同步块之前");
synchronized (this) {
System.out.println(name + "的同步块内");
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("/n" + name + "同步块之后");
}
public static void main(String[] args) {
Runnable r = new ThreadTest();
Thread t1 = new Thread(r, "t1");
Thread t2 = new Thread(r, "t2");
Thread t3 = new Thread(r, "t3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
t1的同步块之前
t1的同步块内
0 t2的同步块之前
t3的同步块之前
1 2 3 4 5 6 7 8 9 t2的同步块内
0
t1同步块之后
1 2 3 4 5 6 7 8 9 t3的同步块内
0
t2同步块之后
1 2 3 4 5 6 7 8 9
t3同步块之后
结果有问题啊,都乱掉了!不,结果没问题,因为同步块的作用就是使同步块里的代码进行同步,同步块外的不管。换句话说,就是同步块把同步块内的代码上锁了,没有得到钥匙的线程不能进入,只能在外面等待。你可能会问,为什么输出第一个0和1之间会输出“t2的同步块之前”,“t3的同步块之前”这两句话,不是进入到同步状态了吗?t1得到了钥匙,t2和t3没有钥匙,为什么能照样执行呢?嗯,问得很好,请再看一下代码,打印的那行(灰底)在同步块之外,因此不受同步的约束。可以这样理解,那行打印的程序并没有上锁,因此并不需要得到钥匙才能执行,所有线程都能自由的执行。在没有遇到关键字synchronized之前,线程会一直运行下去的。当线程运行到关键字synchronized的时候,便会进入到等待池中,然后查看对象的钥匙是否没人使用,是的话就去争夺,夺取钥匙后便能进入上了锁的同步代码中,没有夺到就只能在等待池中等待。
那同步块synchronized后面的this(红底)是什么意思呢?他的意思是:使用哪一个对象的对象管理器,来对同步块进行加锁呢?上述代码使用的是当前对象(this)的对象管理器来对同步块进行加锁,只能有得到当前对象(this)的钥匙才能够解锁,进入到同步块中。一个对象可以有多把锁,但只有一把钥匙,使用了这个对象的锁,就只能使用这个对象的钥匙来开锁。即这里的同步代码由这个对象的锁和钥匙来管理。同步方法没有明确的说明用哪个对象的对象管理器啊?嗯,java规定同步方法都只能由当前对象的对象管理器控制。
上面都明白了的话,就来点提高吧,先看下面代码。
代码五:(为了简便,我把要导入的包和异常处理都删掉了)
public class OperatorFile {
static Object obj;
ReadThread rt;
WriteThread wt;
String fileName;
boolean canRead;
OperatorFile(String fileName) {
this.fileName = fileName;
obj = new Object();
rt = new ReadThread();
wt = new WriteThread();
rt.start();
wt.start();
}
public void readFile(String fileName) {
synchronized (obj) {
while (!canRead){
obj.wait(); //使当前线程进入休眠状态
}
FileInputStream fis = new FileInputStream(new File(fileName));
byte[] data = new byte[fis.available()];
fis.read(data);
System.out.println(new String(data));
fis.close();
}
}
public void writeFile(String fileName) {
synchronized (obj) {
FileOutputStream fos = new FileOutputStream(fileName);
for (int i = 'a'; i <= 'z'; i++) {
fos.write(i);
}
fos.close();
canRead = true;
obj.notify(); //唤醒在休眠池中的任意一个线程
}
}
class ReadThread extends Thread {
public void run() {
readFile(fileName);
}
}
class WriteThread extends Thread {
public void run() {
writeFile(fileName);
}
}
public static void main(String[] args) {
new OperatorFile("test.txt");
}
}
运行结果:
abcdefghijklmnopqrstuvwxyz
常理来说,先调用rt的start方法,应该会先运行readFile去读文件,此时“test.txt”里没有内容,应该是什么也不会打印的,但为什么会先调用wt的shart方法,执行writeFile,把a-z写到文件中,然后再把回到readFile去读文件呢?嗯,你猜得没错,就是因为readFile里有一个wait()方法,此方法使得当前的线程进入到休眠池中休眠,并且释放了obj对象的钥匙,因为同步块使用了obj的对象管理器进行对同步线程的管理。此时在等待池中等待的wt把空闲的钥匙夺了过来,所以能够把锁给打开,进入到同步块中,然后对文件进行写操作。注意,读和写是在不同的同步块中实现的,只要是属于同一对象的同步代码,便会给这些同步代码加上该对象的锁,由此可知,一个对象的锁可以有多个,但一个对象有且仅有一把钥匙。只有得到这把钥匙,才能随心所欲的打开任意一把锁。接下来writeFile把数据都写到文件中了,然后调用notify()方法,唤醒此对象管理器(这里是obj)中的一个线程,便把该线程从休眠池中移动到等待池中,rt线程被唤醒后,发觉钥匙空闲,因此把钥匙夺过来,然后继续上次运行,把文件的数据输出。
代码五有几个值得注意的地方。什么?你看出来了?没错,第一个是synchronized所使用的对象管理器,这段代码是使用obj的对象管理器来管理同步线程的,因此需要在synchronized后面的括号中填上obj。obj是静态的?对,没错,我们的同步线程只能由一个对象来管理,多个对象是管理不了同步线程的,这里定义obj为静态是为了确保obj只有一份,从而达到我们的要求。
第二个要注意的地方是:wait和notify这两个方法,必须是obj的方法,不能是其他对象的方法。你自己可以去改一下,用this的wait和notify方法,一定会有异常产生。在java doc上是这样写的“如果当前的线程不是此对象监视器的所有者”时,调用wait、notify和notifyAll便会产生“IllegalMonitorStateException”异常。说简单一点,使用哪个对象的对象管理器去控制同步线程,就只能使用该对象的wait、notify和notifyAll来控制同步。
第三个要注意的地方是:wait方法外面的while循环。为什么要用循环语句,并且要定义一个标示符来控制循环呢?这样做能够明确的指明所需唤醒的线程。例如现在有三个线程(A、B、C)在休眠,现在我想让在运行态中D线程来唤醒A线程,应该如何实现呢?使用一般的方法是实现不了的,只能按照上面所说的,循环语句结合标示符来实现。看一下下面的代码:
代码六:
public class Test {
boolean isA;
boolean isB;
boolean isC;
synchronized void doA(){
while(!isA){
wait();
}
//do someting......
}
synchronized void doB(){
while(!isB){
wait();
}
//do someting......
}
synchronized void doC(){
while(!isC){
wait();
}
//do someting......
}
synchronized void doD(){
isA = true;
notifyAll();
//do someting......
}
}
分析:当执行doD方法的时候,令isA = true,然后唤醒所有的线程。此时doA中,由于isA为true,跳出循环,所以不会执行到wait方法,线程A能够开始执行。而doB和doC中,由于isB和isC还是false,因此进入到循环体中,执行wait方法继续休眠,达到了目的。所以通常情况下都是在循环内做休眠动作,并用一标示符控制线程是否应该被唤醒。
到此结束,可能和官方的解释略有不同,一切以官方的为准。以上是我对线程同步的一点点见解,希望对你们有所帮助,如果有不当之处,望能指出,万分感谢。
作者:RaymondLin
时间: 2007-1-1
