Java多线程---2
线程的生命周期
与人有生老病死一样,线程也同样要经历开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制。下面给出了Thread类中和这四种状态相关的方法。
1 // 开始线程 2 public void start( ); 3 public void run( ); 4 5 // 挂起和唤醒线程 6 public void resume( ); // 不建议使用 7 public void suspend( ); // 不建议使用 8 public static void sleep(long millis); 9 public static void sleep(long millis, int nanos); 10 11 // 终止线程 12 public void stop( ); // 不建议使用 13 public void interrupt( ); 14 15 // 得到线程状态 16 public boolean isAlive( ); 17 public boolean isInterrupted( ); 18 public static boolean interrupted( ); 19 20 // join方法 21 public void join( ) throws InterruptedException;
一、创建并运行线程
线程在建立后并不马上执行run方法中的代码,而是处于等待状态。线程处于等待状态时,可以通过Thread类的方法来设置线程不各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。
可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。下面的代码演示了线程的创建、运行和停止三个状态之间的切换,并输出了相应的isAlive返回值。
1 package chapter2; 2 3 public class LifeCycle extends Thread 4 { 5 public void run() 6 { 7 int n = 0; 8 while ((++n) < 1000); 9 } 10 11 public static void main(String[] args) throws Exception 12 { 13 LifeCycle thread1 = new LifeCycle(); 14 System.out.println("isAlive: " + thread1.isAlive()); 15 thread1.start(); 16 System.out.println("isAlive: " + thread1.isAlive()); 17 thread1.join(); // 等线程thread1结束后再继续执行 18 System.out.println("thread1已经结束!"); 19 System.out.println("isAlive: " + thread1.isAlive()); 20 } 21 }
要注意一下,在上面的代码中使用了join方法,这个方法的主要功能是保证线程的run方法完成后程序才继续运行,这个方法将在后面的文章中介绍上面代码的运行结果
- isAlive: true
thread1已经结束!
isAlive: false
- isAlive: false
二、挂起和唤醒线程
一但线程开始执行run方法,就会一直到这个run方法执行完成这个线程才退出。但在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两个方法是suspend和sleep。在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。
虽然suspend和resume可以很方便地使线程挂起和唤醒,但由于使用这两个方法可能会造成一些不可预料的事情发生,因此,这两个方法被标识为deprecated(抗议)标记,这表明在以后的jdk版本中这两个方法可能被删除,所以尽量不要使用这两个方法来操作线程。下面的代码演示了sleep、suspend和resume三个方法的使用。
1 public class MyThread extends Thread 2 { 3 class SleepThread extends Thread 4 { 5 public void run() 6 { 7 try 8 { 9 sleep(2000); 10 } catch (Exception e) 11 { 12 } 13 } 14 } 15 16 public void run() 17 { 18 while (true) 19 System.out.println(new java.util.Date().getTime()); 20 } 21 22 public static void main(String[] args) throws Exception 23 { 24 MyThread thread = new MyThread(); 25 SleepThread sleepThread = thread.new SleepThread(); 26 sleepThread.start(); // 开始运行线程sleepThread 27 sleepThread.join(); // 使线程sleepThread延迟2秒 28 thread.start(); 29 boolean flag = false; 30 while (true) 31 { 32 sleep(5000); // 使主线程延迟5秒 33 flag = !flag; 34 if (flag) 35 thread.suspend(); 36 else 37 thread.resume(); 38 } 39 } 40 }
从表面上看,使用sleep和suspend所产生的效果类似,但sleep方法并不等同于suspend。它们之间最大的一个区别是可以在一个线程中通过suspend方法来挂起另外一个线程,如上面代码中在主线程中挂起了thread线程。而sleep只对当前正在执行的线程起作用。在上面代码中分别使sleepThread和主线程休眠了2秒和5秒。在使用sleep时要注意,不能在一个线程中来休眠另一个线程。如main方法中使用thread.sleep(2000)方法是无法使thread线程休眠2秒的,而只能使主线程休眠2秒。
在使用sleep方法时有两点需要注意:
1. sleep方法有两个重载形式,其中一个重载形式不仅可以设毫秒,而且还可以设纳秒(1,000,000纳秒等于1毫秒)。但大多数操作系统平台上的Java虚拟机都无法精确到纳秒,因此,如果对sleep设置了纳秒,Java虚拟机将取最接近这个值的毫秒。
2. 在使用sleep方法时必须使用throws或try{...}catch{...}。因为run方法无法使用throws,所以只能使用try{...}catch{...}。当在线程休眠的过程中,使用interrupt方法(这个方法将在2.3.3中讨论)中断线程时sleep会抛出一个InterruptedException异常。sleep方法的定义如下:
public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
三、终止线程的三种方法
有三种方法可以使终止线程。
1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2. 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
3. 使用interrupt方法中断线程。
1. 使用退出标志终止线程
当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){...}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。下面给出了一个利用退出标志终止线程的例子。
1 package chapter2; 2 3 public class ThreadFlag extends Thread 4 { 5 public volatile boolean exit = false; 6 7 public void run() 8 { 9 while (!exit); 10 } 11 public static void main(String[] args) throws Exception 12 { 13 ThreadFlag thread = new ThreadFlag(); 14 thread.start(); 15 sleep(5000); // 主线程延迟5秒 16 thread.exit = true; // 终止线程thread 17 thread.join(); 18 System.out.println("线程退出!"); 19 } 20 }
在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false。在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值
2. 使用stop方法终止线程
使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:
thread.stop();
虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
3. 使用interrupt方法终止线程
使用interrupt方法来终端线程可分为两种情况:
(1)线程处于阻塞状态,如使用了sleep方法。
(2)使用while(!isInterrupted()){...}来判断线程是否被中断。在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。下面的代码演示了在第一种情况下使用interrupt方法。
1 package chapter2; 2 3 public class ThreadInterrupt extends Thread 4 { 5 public void run() 6 { 7 try 8 { 9 sleep(50000); // 延迟50秒 10 } 11 catch (InterruptedException e) 12 { 13 System.out.println(e.getMessage()); 14 } 15 } 16 public static void main(String[] args) throws Exception 17 { 18 Thread thread = new ThreadInterrupt(); 19 thread.start(); 20 System.out.println("在50秒之内按任意键中断线程!"); 21 System.in.read(); 22 thread.interrupt(); 23 thread.join(); 24 System.out.println("线程已经退出!"); 25 } 26 }
上面代码的运行结果如下:
在50秒之内按任意键中断线程!
sleep interrupted
线程已经退出!
在调用interrupt方法后, sleep方法抛出异常,然后输出错误信息:sleep interrupted。
注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。
以上就是线程的生命周期。要进一步学习Java多线程,务必要对Java线程生命周期有着足够的认识。
join方法的使用
在上面的例子中多次使用到了Thread类的join方法。我想大家可能已经猜出来join方法的功能是什么了。对,join方法的功能就是使异步执行的线程变成同步执行。也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。而使用join方法后,直到这个线程退出,程序才会往下执行。下面的代码演示了join的用法。
1 public class JoinThread extends Thread 2 { 3 public static int n = 0; 4 5 static synchronized void inc() 6 { 7 n++; 8 } 9 10 public void run() 11 { 12 for (int i = 0; i < 10; i++) 13 try 14 { 15 inc(); 16 sleep(3); // 为了使运行结果更随机,延迟3毫秒 17 18 } catch (Exception e) 19 { 20 } 21 } 22 23 public static void main(String[] args) throws Exception 24 { 25 26 Thread threads[] = new Thread[100]; 27 for (int i = 0; i < threads.length; i++) 28 // 建立100个线程 29 threads[i] = new JoinThread(); 30 for (int i = 0; i < threads.length; i++) 31 // 运行刚才建立的100个线程 32 threads[i].start(); 33 if (args.length > 0) 34 for (int i = 0; i < threads.length; i++) 35 // 100个线程都执行完后继续 36 threads[i].join(); 37 System.out.println("n=" + JoinThread.n); 38 } 39 }
在例程2-8中建立了100个线程,每个线程使静态变量n增加10。如果在这100个线程都执行完后输出n,这个n值应该是1000。
1. 测试1
使用如下的命令运行上面程序:
java mythread.JoinThread
程序的运行结果如下:
n=442
这个运行结果可能在不同的运行环境下有一些差异,但一般n不会等于1000。从上面的结果可以肯定,这100个线程并未都执行完就将n输出了。
2. 测试2
使用如下的命令运行上面的代码:
在上面的命令行中有一个参数join,其实在命令行中可以使用任何参数,只要有一个参数就可以,这里使用join,只是为了表明要使用join方法使这100个线程同步执行。
程序的运行结果如下:
n=1000
无论在什么样的运行环境下运行上面的命令,都会得到相同的结果:n=1000。这充分说明了这100个线程肯定是都执行完了,因此,n一定会等于1000。
慎重使用volatile关键字
volatile关键字相信了解Java多线程的读者都很清楚它的作用。volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:
1 package mythread; 2 3 public class JoinThread extends Thread 4 { 5 public static int n = 0; 6 7 public static synchronized void inc() 8 { 9 n++; 10 } 11 12 public void run() 13 { 14 for (int i = 0; i < 10; i++) 15 try 16 { 17 inc(); // n = n + 1 改成了 inc(); 18 sleep(3); // 为了使运行结果更随机,延迟3毫秒 19 } catch (Exception e) 20 { 21 } 22 } 23 24 public static void main(String[] args) throws Exception 25 { 26 Thread threads[] = new Thread[100]; 27 for (int i = 0; i < threads.length; i++) 28 // 建立100个线程 29 threads[i] = new JoinThread(); 30 for (int i = 0; i < threads.length; i++) 31 // 运行刚才建立的100个线程 32 threads[i].start(); 33 for (int i = 0; i < threads.length; i++) 34 // 100个线程都执行完后继续 35 threads[i].join(); 36 System.out.println("n=" + JoinThread.n); 37 } 38 39 }
如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:
n = n + 1;
n++;
如果要想使这种情况变成原子操作,需要使用synchronized关键字,如上的代码可以改成如下的形式:
-
1 package mythread; 2 3 public class JoinThread extends Thread 4 { 5 public static int n = 0; 6 7 public static synchronized void inc() 8 { 9 n++; 10 } 11 public void run() 12 { 13 for (int i = 0; i < 10; i++) 14 try 15 { 16 inc(); // n = n + 1 改成了 inc(); 17 sleep(3); // 为了使运行结果更随机,延迟3毫秒 18 19 } 20 catch (Exception e) 21 { 22 } 23 } 24 25 public static void main(String[] args) throws Exception 26 { 27 28 Thread threads[] = new Thread[100]; 29 for (int i = 0; i < threads.length; i++) 30 // 建立100个线程 31 threads[i] = new JoinThread(); 32 for (int i = 0; i < threads.length; i++) 33 // 运行刚才建立的100个线程 34 threads[i].start(); 35 for (int i = 0; i < threads.length; i++) 36 // 100个线程都执行完后继续 37 threads[i].join(); 38 System.out.println("n=" + JoinThread.n); 39 } 40 }
- 上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
向线程传递数据的三种方法
在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。本文就以上原因介绍了几种用于向线程传递数据的方法,在下一篇文章中将介绍从线程中返回数据的方法。
欲先取之,必先予之。一般在使用线程时都需要有一些初始化数据,然后线程利用这些数据进行加工处理,并返回结果。在这个过程中最先要做的就是向线程中传递数据。
一、通过构造方法传递数据
在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据:
1 class Data 2 { 3 public int value = 0; 4 } 5 6 class Work 7 { 8 public void process(Data data, Integer... numbers) 9 { 10 for (int n : numbers) 11 { 12 data.value += n; 13 } 14 } 15 } 16 17 public class MyThread3 extends Thread 18 { 19 private Work work; 20 21 public MyThread3(Work work) 22 { 23 this.work = work; 24 } 25 26 public void run() 27 { 28 java.util.Random random = new java.util.Random(); 29 Data data = new Data(); 30 int n1 = random.nextInt(1000); 31 int n2 = random.nextInt(2000); 32 int n3 = random.nextInt(3000); 33 work.process(data, n1, n2, n3); // 使用回调函数 34 System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 35 + String.valueOf(n3) + "=" + data.value); 36 } 37 38 public static void main(String[] args) 39 { 40 Thread thread = new MyThread3(new Work()); 41 thread.start(); 42 } 43 }
由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。
二、通过变量和方法传递数据
向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置name变量:
1 package mythread; 2 3 public class MyThread2 implements Runnable 4 { 5 private String name; 6 7 public void setName(String name) 8 { 9 this.name = name; 10 } 11 public void run() 12 { 13 System.out.println("hello " + name); 14 } 15 public static void main(String[] args) 16 { 17 MyThread2 myThread = new MyThread2(); 18 myThread.setName("world"); 19 Thread thread = new Thread(myThread); 20 thread.start(); 21 } 22 }
三、通过回调函数传递数据
上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个value是无法事先就传入线程类的。
1 class Data 2 { 3 public int value = 0; 4 } 5 6 class Work 7 { 8 public void process(Data data, Integer... numbers) 9 { 10 for (int n : numbers) 11 { 12 data.value += n; 13 } 14 } 15 } 16 17 public class MyThread3 extends Thread 18 { 19 private Work work; 20 21 public MyThread3(Work work) 22 { 23 this.work = work; 24 } 25 26 public void run() 27 { 28 java.util.Random random = new java.util.Random(); 29 Data data = new Data(); 30 int n1 = random.nextInt(1000); 31 int n2 = random.nextInt(2000); 32 int n3 = random.nextInt(3000); 33 work.process(data, n1, n2, n3); // 使用回调函数 34 System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 35 + String.valueOf(n3) + "=" + data.value); 36 } 37 38 public static void main(String[] args) 39 { 40 Thread thread = new MyThread3(new Work()); 41 thread.start(); 42 } 43 }
在上面代码中的process方法被称为回调函数。从本质上说,回调函数就是事件函数。在Windows API中常使用回调函数和调用API的程序之间进行数据交互。因此,调用回调函数的过程就是最原始的引发事件的过程。在这个例子中调用了process方法来获得数据也就相当于在run方法中引发了一个事件。
从线程返回数据的两种方法
从线程中返回数据和向线程传递数据类似。也可以通过类成员以及回调函数来返回数据。但类成员在返回数据和传递数据时有一些区别,下面让我们来看看它们区别在哪。
一、通过类变量和方法返回数据
使用这种方法返回数据需要在调用start方法后才能通过类变量或方法得到数据。让我们先来看看例程2-13会得到什么结果。
1 package mythread; 2 3 public class MyThread extends Thread 4 { 5 private String value1; 6 private String value2; 7 8 public void run() 9 { 10 value1 = "通过成员变量返回数据"; 11 value2 = "通过成员方法返回数据"; 12 } 13 public static void main(String[] args) throws Exception 14 { 15 MyThread thread = new MyThread(); 16 thread.start(); 17 System.out.println("value1:" + thread.value1); 18 System.out.println("value2:" + thread.value2); 19 } 20 }
运行上面的代码有可能输出如下的结果:
value1:null
value2:null
从上面的运行结果看很不正常。在run方法中已经对value1和value2赋了值,而返回的却是null。发生这种情况的原因是调用start方法后就立刻输出了value1和value2的值,而这里run方法还没有执行到为value1和value2赋值的语句。要避免这种情况的发生,就需要等run方法执行完后才执行输出value1和value2的代码。因此,我们可以想到使用sleep方法将主线程进行延迟,如可以在thread.start()后加一行如下的语句:
sleep(1000);
这样做可以使主线程延迟1秒后再往下执行,但这样做有一个问题,就是我们怎么知道要延迟多长时间。在这个例子的run方法中只有两条赋值语句,而且只创建了一个线程,因此,延迟1秒已经足够,但如果run方法中的语句很复杂,这个时间就很难预测,因此,这种方法并不稳定。
我们的目的就是得到value1和value2的值,因此,只要判断value1和value2是否为null。如果它们都不为null时,就可以输出这两个值了。我们可以使用如下的代码来达到这个目的:
while (thread.value1 == null || thread.value2 == null);
使用上面的语句可以很稳定地避免这种情况发生,但这种方法太耗费系统资源。大家可以设想,如果run方法中的代码很复杂,value1和value2需要很长时间才能被赋值,这样while循环就必须一直执行下去,直到value1和value2都不为空为止。因此,我们可以对上面的语句做如下的改进:
while (thread.value1 == null || thread.value2 == null)
sleep(100);
在while循环中第判断一次value1和value2的值后休眠100毫秒,然后再判断这两个值。这样所占用的系统资源会小一些。
上面的方法虽然可以很好地解决,但Java的线程模型为我们提供了更好的解决方案,这就是join方法。在前面已经讨论过,join的功能就是使用线程从异步执行变成同步执行。当线程变成同步执行后,就和从普通的方法中得到返回数据没有什么区别了。因此,可以使用如下的代码更有效地解决这个问题:
thread.start();
thread.join();
在thread.join()执行完后,线程thread的run方法已经退出了,也就是说线程thread已经结束了。因此,在thread.join()后面可以放心大胆地使用MyThread类的任何资源来得到返回数据
二、通过回调函数返回数据
其实这种方法已经在《向线程传递数据的三种方法》中介绍了。在《向线程传递数据的三种方法》一文的例子中通过Work类的process方法向线程中传递了计算结果,但同时,也通过process方法从线程中得到了三个随机数。因此,这种方法既可以向线程中传递数据,也可以从线程中获得数据。
使用Synchronized关键字同步类方法
要想解决“脏数据”的问题,最简单的方法就是使用synchronized关键字来使run方法同步,代码如下:
public synchronized void run()
{
}
从上面的代码可以看出,只要在void和public之间加上synchronized关键字,就可以使run方法同步,也就是说,对于同一个Java类的对象实例,run方法同时只能被一个线程调用,并当前的run执行完后,才能被其他的线程调用。即使当前线程执行到了run方法中的yield方法,也只是暂停了一下。由于其他线程无法执行run方法,因此,最终还是会由当前的线程来继续执行。先看看下面的代码:
sychronized关键字只和一个对象实例绑定
1 class Test 2 { 3 public synchronized void method() 4 { 5 6 } 7 } 8 9 public class Sync implements Runnable 10 { 11 private Test test; 12 public void run() 13 { 14 test.method(); 15 } 16 public Sync(Test test) 17 { 18 this.test = test; 19 } 20 public static void main(String[] args) throws Exception 21 { 22 Test test1 = new Test(); 23 Test test2 = new Test(); 24 Sync sync1 = new Sync(test1); 25 Sync sync2 = new Sync(test2); 26 new Thread(sync1).start(); 27 new Thread(sync2).start(); 28 } 29 }
在Test类中的method方法是同步的。但上面的代码建立了两个Test类的实例,因此,test1和test2的method方法是分别执行的。要想让method同步,必须在建立Sync类的实例时向它的构造方法中传入同一个Test类的实例,如下面的代码所示:
Sync sync1 = new Sync(test1); 不仅可以使用synchronized来同步非静态方法,也可以使用synchronized来同步静态方法。如可以按如下方式来定义method方法:
class Test
{
public static synchronized void method() { }
}
建立Test类的对象实例如下:
Test test = new Test();
对于静态方法来说,只要加上了synchronized关键字,这个方法就是同步的,无论是使用test.method(),还是使用Test.method()来调用method方法,method都是同步的,并不存在非静态方法的多个实例的问题。
在23种设计模式中的单件(Singleton)模式如果按传统的方法设计,也是线程不安全的,下面的代码是一个线程不安全的单件模式。
1 package test; 2 3 // 线程安全的Singleton模式 4 class Singleton 5 { 6 private static Singleton sample; 7 8 private Singleton() 9 { 10 } 11 public static Singleton getInstance() 12 { 13 if (sample == null) 14 { 15 Thread.yield(); // 为了放大Singleton模式的线程不安全性 16 sample = new Singleton(); 17 } 18 return sample; 19 } 20 } 21 public class MyThread extends Thread 22 { 23 public void run() 24 { 25 Singleton singleton = Singleton.getInstance(); 26 System.out.println(singleton.hashCode()); 27 } 28 public static void main(String[] args) 29 { 30 Thread threads[] = new Thread[5]; 31 for (int i = 0; i < threads.length; i++) 32 threads[i] = new MyThread(); 33 for (int i = 0; i < threads.length; i++) 34 threads[i].start(); 35 } 36 }
在上面的代码调用yield方法是为了使单件模式的线程不安全性表现出来,如果将这行去掉,上面的实现仍然是线程不安全的,只是出现的可能性小得多。
程序的运行结果如下:
25358555
26399554
7051261
29855319
5383406
上面的运行结果可能在不同的运行环境上有所有同,但一般这五行输出不会完全相同。从这个输出结果可以看出,通过getInstance方法得到的对象实例是五个,而不是我们期望的一个。这是因为当一个线程执行了Thread.yield()后,就将CPU资源交给了另外一个线程。由于在线程之间切换时并未执行到创建Singleton对象实例的语句,因此,这几个线程都通过了if判断,所以,就会产生了建立五个对象实例的情况(可能创建的是四个或三个对象实例,这取决于有多少个线程在创建Singleton对象之前通过了if判断,每次运行时可能结果会不一样)。
要想使上面的单件模式变成线程安全的,只要为getInstance加上synchronized关键字即可。代码如下:
public static synchronized Singleton getInstance() { }
当然,还有更简单的方法,就是在定义Singleton变量时就建立Singleton对象,代码如下:
private static final Singleton sample = new Singleton();
然后在getInstance方法中直接将sample返回即可。这种方式虽然简单,但不知在getInstance方法中创建Singleton对象灵活。读者可以根据具体的需求选择使用不同的方法来实现单件模式。
在使用synchronized关键字时有以下四点需要注意:
1. synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
在子类方法中加上synchronized关键字
class Parent
{
public synchronized void method() { }
}
class Child extends Parent
{
public synchronized void method() { }
}
在子类方法中调用父类的同步方法
class Parent
{
public synchronized void method() { }
}
class Child extends Parent
{
public void method() { super.method(); }
}
2. 在定义接口方法时不能使用synchronized关键字。
3. 构造方法不能使用synchronized关键字,但可以使用下节要讨论的synchronized块来进行同步。
4. synchronized可以自由放置。
在前面的例子中使用都是将synchronized关键字放在方法的返回类型前面。但这并不是synchronized可放置唯一位置。在非静态方法中,synchronized还可以放在方法定义的最前面,在静态方法中,synchronized可以放在static的前面,代码如下:
public synchronized void method();
synchronized public void method();
public static synchronized void method();
public synchronized static void method();
synchronized public static void method();
但要注意,synchronized不能放在方法返回类型的后面,如下面的代码是错误的:
public void synchronized method();
public static void synchronized method();
synchronized关键字只能用来同步方法,不能用来同步类变量,如下面的代码也是错误的。
public synchronized int n = 0;
public static synchronized int n = 0;
虽然使用synchronized关键字同步方法是最安全的同步方式,但大量使用synchronized关键字会造成不必要的资源消耗以及性能损失。虽然从表面上看synchronized锁定的是一个方法,但实际上synchronized锁定的是一个类。也就是说,如果在非静态方法method1和method2定义时都使用了synchronized,在method1未执行完之前,method2是不能执行的。静态方法和非静态方法的情况类似。但静态和非静态方法不会互相影响。看看如下的代码:
1 package test; 2 3 public class MyThread1 extends Thread 4 { 5 public String methodName; 6 7 public static void method(String s) 8 { 9 System.out.println(s); 10 while (true) 11 ; 12 } 13 public synchronized void method1() 14 { 15 method("非静态的method1方法"); 16 } 17 public synchronized void method2() 18 { 19 method("非静态的method2方法"); 20 } 21 public static synchronized void method3() 22 { 23 method("静态的method3方法"); 24 } 25 public static synchronized void method4() 26 { 27 method("静态的method4方法"); 28 } 29 public void run() 30 { 31 try 32 { 33 getClass().getMethod(methodName).invoke(this); 34 } 35 catch (Exception e) 36 { 37 } 38 } 39 public static void main(String[] args) throws Exception 40 { 41 MyThread1 myThread1 = new MyThread1(); 42 for (int i = 1; i <= 4; i++) 43 { 44 myThread1.methodName = "method" + String.valueOf(i); 45 new Thread(myThread1).start(); 46 sleep(100); 47 } 48 } 49 }
运行结果如下:
非静态的method1方法
静态的method3方法
从上面的运行结果可以看出,method2和method4在method1和method3未结束之前不能运行。因此,我们可以得出一个结论,如果在类中使用synchronized关键字来定义非静态方法,那将影响这个中的所有使用synchronized关键字定义的非静态方法。如果定义的是静态方法,那么将影响类中所有使用synchronized关键字定义的静态方法。这有点象数据表中的表锁,当修改一条记录时,系统就将整个表都锁住了,因此,大量使用这种同步方式会使程序的性能大幅度下降。
使用Synchronized块同步方法
synchronized关键字有两种用法。第一种就是在《使用Synchronized关键字同步类方法》一文中所介绍的直接用在方法的定义中。另外一种就是synchronized块。我们不仅可以通过synchronized块来同步一个对象变量。也可以使用synchronized块来同步类中的静态方法和非静态方法。
synchronized块的语法如下:
public void method()
{
… …
synchronized(表达式)
{
… …
}
}
一、非静态类方法的同步
从《使用Synchronized关键字同步类方法》一文中我们知道使用synchronized关键字来定义方法就会锁定类中所有使用synchronzied关键字定义的静态方法或非静态方法,但这并不好理解。而如果使用synchronized块来达到同样的效果,就不难理解为什么会产生这种效果了。如果想使用synchronized块来锁定类中所有的同步非静态方法,需要使用this做为synchronized块的参数传入synchronized块国,代码如下:
通过synchronized块同步非静态方法
public class SyncBlock
{
public void method1()
{
synchronized(this) // 相当于对method1方法使用synchronized关键字
{
… …
}
}
public void method2()
{
synchronized(this) // 相当于对method2方法使用synchronized关键字
{
… …
}
}
public synchronized void method3()
{
… …
}
}
在上面的代码中的method1和method2方法中使用了synchronized块。而第017行的method3方法仍然使用synchronized关键字来定义方法。在使用同一个SyncBlock类实例时,这三个方法只要有一个正在执行,其他两个方法就会因未获得同步锁而被阻塞。在使用synchronized块时要想达到和synchronized关键字同样的效果,必须将所有的代码都写在synchronized块中,否则,将无法使当前方法中的所有代码和其他的方法同步。
除了使用this做为synchronized块的参数外,还可以使用SyncBlock.this作为synchronized块的参数来达到同样的效果。
在内类(InnerClass)的方法中使用synchronized块来时,this只表示内类,和外类(OuterClass)没有关系。但内类的非静态方法可以和外类的非静态方法同步。如在内类InnerClass中加一个method4方法,并使method4方法和SyncBlock的三个方法同步,代码如下:
使内类的非静态方法和外类的非静态方法同步
public class SyncBlock
{
… …
class InnerClass
{
public void method4()
{
synchronized(SyncBlock.this)
{
… …
}
}
}
… …
}
在上面SyncBlock类的新版本中,InnerClass类的method4方法和SyncBlock类的其他三个方法同步,因此,method1、method2、method3和method4四个方法在同一时间只能有一个方法执行。
Synchronized块不管是正常执行完,还是因为程序出错而异常退出synchronized块,当前的synchronized块所持有的同步锁都会自动释放。因此,在使用synchronized块时不必担心同步锁的释放问题。
二、静态类方法的同步
由于在调用静态方法时,对象实例不一定被创建。因此,就不能使用this来同步静态方法,而必须使用Class对象来同步静态方法。代码如下:
通过synchronized块同步静态方法
public class StaticSyncBlock
{
public static void method1()
{
synchronized(StaticSyncBlock.class)
{
… …
}
}
public static synchronized void method2()
{
… …
}
}
在同步静态方法时可以使用类的静态字段class来得到Class对象。在上例中method1和method2方法同时只能有一个方法执行。除了使用class字段得到Class对象外,还可以使用实例的getClass方法来得到Class对象。上例中的代码可以修改如下:
使用getClass方法得到Class对象
public class StaticSyncBlock
{
public static StaticSyncBlock instance;
public StaticSyncBlock()
{
instance = this;
}
public static void method1()
{
synchronized(instance.getClass())
{
}
}
}
在上面代码中通过一个public的静态instance得到一个StaticSyncBlock类的实例,并通过这个实例的getClass方法得到了Class对象(一个类的所有实例通过getClass方法得到的都是同一个Class对象,因此,调用任何一个实例的getClass方法都可以)。我们还可以通过Class对象使不同类的静态方法同步,如Test类的静态方法method和StaticSyncBlock类的两个静态方法同步,代码如下:
Test类的method方法和StaticSyncBlock类的method1、method2方法同步
public class Test
{
public static void method()
{
synchronized(StaticSyncBlock.class)
{
}
}
}
注意:在使用synchronized块同步类方法时,非静态方法可以使用this来同步,而静态方法必须使用Class对象来同步。它们互不影响。当然,也可以在非静态方法中使用Class对象来同步静态方法。但在静态方法中不能使用this来同步非静态方法。这一点在使用synchronized块同步类方法时应注意。
使用Synchronized块同步变量
我们可以通过synchronized块来同步特定的静态或非静态方法。要想实现这种需求必须为这些特性的方法定义一个类变量,然后将这些方法的代码用synchronized块括起来,并将这个类变量作为参数传入synchronized块。下面的代码演示了如何同步特定的类方法:
1 package mythread; 2 3 public class SyncThread extends Thread 4 { 5 private static String sync = ""; 6 private String methodType = ""; 7 8 private static void method(String s) 9 { 10 synchronized (sync) 11 { 12 sync = s; 13 System.out.println(s); 14 while (true); 15 } 16 } 17 public void method1() 18 { 19 method("method1"); 20 } 21 public static void staticMethod1() 22 { 23 method("staticMethod1"); 24 } 25 public void run() 26 { 27 if (methodType.equals("static")) 28 staticMethod1(); 29 else if (methodType.equals("nonstatic")) 30 method1(); 31 } 32 public SyncThread(String methodType) 33 { 34 this.methodType = methodType; 35 } 36 public static void main(String[] args) throws Exception 37 { 38 SyncThread sample1 = new SyncThread("nonstatic"); 39 SyncThread sample2 = new SyncThread("static"); 40 sample1.start(); 41 sample2.start(); 42 } 43 }
运行结果如下:
method1
staticMethod1
看到上面的运行结果很多读者可能感到惊奇。在上面的代码中method1和staticMethod1方法使用了静态字符串变量sync进行同步。这两个方法只能有一个同时执行,而这两个方法都会执行014行的无限循环语句。因此,输出结果只能是method1和staticMethod1其中之一。但这个程序将这两个字符串都输出了。
出现这种结果的愿意很简单,我们看一下012行就知道了。原来在这一行将sync的值改变了。在这里要说一下Java中的String类型。String类型和Java中其他的复杂类型不同。在使用String型变量时,只要给这个变量赋一次值,Java就会创建个新的String类型的实例。如下面的代码所示:
String s = "hello";
System.out.println(s.hashCode());
s = "world";
System.out.println(s.hashCode());
在上面的代码中。第一个s和再次赋值后的s的hashCode的值是不一样的。由于创建String类的实例并不需要使用new,因此,在同步String类型的变量时要注意不要给这个变量赋值,否则会使变量无法同步。
由于在013行已经为sync创建了一个新的实例,假设method1先执行,当method1方法执行了013行的代码后,sync的值就已经不是最初那个值了,而method1方法锁定的仍然是sync变量最初的那个值。而在这时,staticMethod1正好执行到synchronized(sync),在staticMethod1方法中要锁定的这个sync和method1方法锁定的sync已经不是一个了,因此,这两个方法的同步性已经被破坏了。
解决以上问题的方法当然是将013行去掉。在本例中加上这行,只是为了说明使用类变量来同步方法时如果在synchronized块中将同步变量的值改变,就会破坏方法之间的同步。为了彻底避免这种情况发生,在定义同步变量时可以使用final关键字。如将上面的程序中的005行可改成如下形式:
private final static String sync = "";
使用final关键字后,sync只能在定义时为其赋值,并且以后不能再修改。如果在程序的其他地方给sync赋了值,程序就无法编译通过。在Eclipse等开发工具中,会直接在错误的地方给出提示。
我们可以从两个角度来理解synchronized块。如果从类方法的角度来理解,可以通过类变量来同步相应的方法。如果从类变量的角度来理解,可以使用synchronized块来保证某个类变量同时只能被一个方法访问。不管从哪个角度来理解,它们的实质都是一样的,就是利用类变量来获得同步锁,通过同步锁的互斥性来实现同步。
注意:在使用synchronized块时应注意,synchronized块只能使用对象作为它的参数。如果是简单类型的变量(如int、char、boolean等),不能使用synchronized来同步。