13.3.4 线程同步
本节将详细地讲述为什么要使用线程同步,并且通过现实生活中程序开发的实例,来说明线程同步的用法。
前面讲述过,线程的运行权通过一种叫抢占的方式获得。一个程序运行到一半时,突然被另一个线程抢占了运行权,此时这个线程数据处理了一半,而另一个线程也在处理这个数据,那么会出现重复操作数据的现象,最终整个系统将会混乱。
【实例13.10】下面先看一个实例,通过这个实例,了解如果线程不是同步的,将会出现什么样的结果。
01 ///这是一个主运行类
02 ///在主运行方法中,通过创建两个线程对象,让其交替执行
03 public class thread8
04 {
05 public static void main(String[]args)
06 {
07 compute t=new compute('a');
08 compute t1=new compute('b');
09 t.start();
10 t1.start();
11 }
12 }
13 ///创建一个线程类
14 ///在这个线程类中,使用循环语句输出字符
15 class compute extends Thread
16 {
17 char ch;
18 compute(char ch)
19 {
20 this.ch=ch;
21 }
22 public void print(char ch)
23 {
24 for(int i=0;i<10;i++)
25 {
26 System.out.print(ch);
27 }
28 }
29 public void run()
30 {
31 print(ch);
32 System.out.println();
33 }
34 }
【代码说明】第7~8创建两个线程,它们输出不同的字符,从结果可以看出,这两个字符是交替输出的。
【运行效果】
abababababababababab
【实例13.11】从这个程序可能看不出线程同步,如果将上面的程序段修改后,再来看看会出现什么样的变化。
01 ///这是一个主运行类
02 ///在主运行方法中,通过创建两个线程对象,让其交替执行
03 public class thread9
04 {
05 public static void main(String[]args)
06 {
07 compute t=new compute('a');
08 compute t1=new compute('b');
09 t.start();
10 t1.start();
11 }
12 }
13 ///创建一个线程类
14 ///在这个线程类中,使用循环语句输出字符
15 class compute extends Thread
16 {
17 char ch;
18 compute(char ch)
19 {
20 this.ch=ch;
21 }
22 public void print(char ch)
23 {
24 for(int i=0;i<10;i++)
25 {
26 System.out.print(ch);
27 }
28 }
29 public void run()
30 {
31 for(int i=1;i<10;i++)
32 {
33 print(ch);
34 System.out.println();
35 }
36 }
37 }
【代码说明】以上的程序段,可以看出两个线程循环输出时,出现抢占现象,于是整个输出就会混乱,一会儿输出“a”,一会儿输出“b”。
【运行效果】
abababababababababab
abababababababababab
abababababababababab
abababababababababab
abababababababababab
abababababababababab
abababababababababab
abababababababababab
abababababababababab
如何解决这个问题呢?这就涉及线程的同步。在Java语言中,解决同步问题的方法有两种:一种是同步块,一种是同步方法。
1.同步块
同步块是使具有某个对象监视点的线程,获得运行权限的一种方法,每个对象只能在拥有这个监视点的情况下,才能获得运行权限。举个例子,一个圆桌,有4个人吃饭,但是只有一个勺子,4人中只有一个人能吃饭,并且这个人必须是拥有勺子的人,而这个勺子就相当于同步块中的监视点。
同步块的结构如下:
synchronized(someobject)
{
代码段
}
“someobject”是一个监视点对象,可以是实际存在的,也可以是假设的。在很多程序段中,这个监视点对象都是假设的。其实这个监视点就相当于一把锁,给一个线程上了锁,那么其他线程就会被拒之门外,就无法得到这把锁。直到这个线程执行完了,才会将这个锁交给其他线程。其他的线程得到锁后,将自己的程序锁住,再将其他线程拒之门外。
【实例13.12】将前面的程序修改后,再来看看输出结果。
01///这是一个主运行类
02 ///在主运行方法中,通过创建三个线程对象,让其交替执行
03 public class thread10
04 {
05 public static void main(String[]args)
06 {
07 compute t=new compute('a');
08 compute t1=new compute('b');
09 compute t2=new compute('c');
10 t.start();
11 t1.start();
12 t2.start();
13 }
14 }
15 ///创建一个线程类
16 ///在这个线程类中,使用循环语句输出字符
17 ///在run方法中,使用同步块来给线程加一把锁
18 class compute extends Thread
19 {
20 char ch;
21 static Object obj=new Object();
22 compute(char ch)
23 {
24 this.ch=ch;
25 }
26 public void print(char ch)
27 {
28 for(int i=1;i<10;i++)
29 {
30 System.out.print(ch);
31 }
32 }
33 34
public void run()
35 {
36 synchronized(obj)
37 {
38 for(int i=1;i<10;i++)
39 {
40 print(ch);
41 System.out.println();
42 }
43 }
44 }
45 }
【代码说明】第36行使用了“synchronized()”方法同步线程,所以每个线程都是先完成再执行下一个线程。
【运行效果】
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
aaaaaaaaa
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
bbbbbbbbb
ccccccccc
ccccccccc
ccccccccc
ccccccccc
ccccccccc
ccccccccc
ccccccccc
ccccccccc
ccccccccc
在运行程序中添加一个监视点,那么将锁内的程序段执行完后,就会自动打开锁,再由另外两个线程抢占这个锁。然后反复执行这个同步块中的程序,这样一个线程执行完后,才会执行另一个线程。对于多线程操作同一个数据,就不会出现混乱的现象。
【实例13.13】下面再看一个有关同步块的程序段例子。
01///这是一个主运行类
02 ///在主运行方法中,通过创建三个线程对象,让其交替执行
03 public class thread11
04 {
05 public static void main(String[]args)
06 {
07 compute t=new compute();
08 new Thread(t).start();
09 new Thread(t).start();
10 new Thread(t).start();
11 }
12 }
13 ///创建一个线程类
14 ///在这个线程类中,使用循环语句输出字符
15 ///在run方法中,使用同步块来给线程加一把锁
16 class compute extends Thread
17 {
18 int i=10;
19 static Object obj=new Object();
20 public void print()
21 {
22 System.out.println(Thread.currentThread().getName()+":"+i);
23 i—;
24 }
25 public void run()
26 {
27 while(i>0)
28 {
29 synchronized(obj)
30 {
31 print();
32 }
33 try
34 {
35 sleep(1000);
36 }
37 catch(Exception e){}
38 }
39 }
40 }
【代码说明】从上面的程序段可以看出,3个线程操作同一个数,通过使用同步块,使得整个处理过程变得很有条理。线程1处理完数字10后,线程2处理数字9,之后,线程3再处理数字8,这样循环下去,使得每个线程都能单独地处理同一数据,而不受其他线程的影响。
【运行效果】
Thread-1:10
Thread-2:9
Thread-3:8
Thread-2:7
Thread-3:6
Thread-1:5
Thread-2:4
Thread-3:3
Thread-1:2
Thread-2:1
Thread-3:0
Thread-1:-1
2.同步方法
同步方法就是对整个方法进行同步。它的结构是:
synchronized void f()
{
代码
}
【实例13.14】下面使用同步方法来修改上面的程序段。
01 ///这是一个主运行类
02 ///在主运行方法中,通过创建三个线程对象,让其交替执行
03 public class thread12
04 {
05 public static void main(String[]args)
06 {
07 compute t=new compute();
08 new Thread(t).start();
09 new Thread(t).start();
10 new Thread(t).start();
11 }
12 }
13 ///创建一个线程类
14 ///在这个线程类中,使用循环语句输出字符
15 ///在run方法中,使用同步方法来给线程加一把锁
16 class compute extends Thread
17 {
18 int i=10;
19 static Object obj=new Object();
20 synchronized void print()
21 {
22 System.out.println(Thread.currentThread().getName()+":"+i);
23 i—;
24 }
25 public void run()
26 {
27 while(i>0)
28 {
29 print();
30 try
31 {
32 sleep(1000);
33 }
34 catch(Exception e){}
35 }
36 }
37 }
【代码说明】第20行使用了同步化关键字synchronized来同步方法print(),第32行使用了休眠方法sleep(),第8~10行开始运行3个进程。
【运行效果】
Thread-1:10
Thread-2:9
Thread-3:8
Thread-3:7
Thread-1:6
Thread-2:5
Thread-3:4
Thread-1:3
Thread-2:2
Thread-3:1
Thread-1:0
Thread-2:-1
从上面的结果可以看出,使用同步块和同步方法的输出结果都是一样的。讲述了线程的所有知识点后,下面将使用一个生产者和消费者的综合模型实例,巩固多线程编程的方法。
说明
从结果显示来看,可能与读者机器上的结果不同,这也是线程的一个特色,每次运行结果都可能不同。