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


从上面的结果可以看出,使用同步块和同步方法的输出结果都是一样的。讲述了线程的所有知识点后,下面将使用一个生产者和消费者的综合模型实例,巩固多线程编程的方法。

说明

从结果显示来看,可能与读者机器上的结果不同,这也是线程的一个特色,每次运行结果都可能不同。