java并发(四)如何创建并运行java线程
创建Thread的子类
创建Thread子类的一个实例并重写run方法,run方法会在调用start()方法之后被执行。例子如下:
- public class MyThread extends Thread {
- public void run(){
- System.out.println("MyThread running");
- }
- }
可以用如下方式创建并运行上述Thread子类
- MyThread myThread = new MyThread();
- myTread.start();
一旦线程启动后start方法就会立即返回,而不会等待到run方法执行完毕才返回。就好像run方法是在另外一个cpu上执行一样。当run方法执行后,将会打印出字符串MyThread running。
你也可以如下创建一个Thread的匿名子类:
- Thread thread = new Thread(){
- public void run(){
- System.out.println("Thread Running");
- }
- };
- thread.start();
当新的线程的run方法执行以后,计算机将会打印出字符串”Thread Running”。
实现Runnable接口
第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法可以被线程调用。下面给出例子:
- public class MyRunnable implements Runnable {
- public void run(){
- System.out.println("MyRunnable running");
- }
- }
为了使线程能够执行run()方法,需要在Thread类的构造函数中传入 MyRunnable的实例对象。示例如下:
- Thread thread = new Thread(new MyRunnable());
- thread.start();
当线程运行时,它将会调用实现了Runnable接口的run方法。上例中将会打印出”MyRunnable running”。
同样,也可以创建一个实现了Runnable接口的匿名类,如下所示:
- Runnable myRunnable = new Runnable(){
- public void run(){
- System.out.println("Runnable running");
- }
- };
- Thread thread = new Thread(myRunnable);
- thread.start();
创建子类还是实现Runnable接口?
对于这两种方式哪种好并没有一个确定的答案,它们都能满足要求。就我个人意见,我更倾向于实现Runnable接口这种方法。因为线程池可以有效的管理实现了Runnable接口的线程,如果线程池满了,新的线程就会排队等候执行,直到线程池空闲出来为止。而如果线程是通过实现Thread子类实现的,这将会复杂一些。
有时我们要同时融合实现Runnable接口和Thread子类两种方式。例如,实现了Thread子类的实例可以执行多个实现了Runnable接口的线程。一个典型的应用就是线程池。
常见错误:调用run()方法而非start()方法
创建并运行一个线程所犯的常见错误是调用线程的run()方法而非start()方法,如下所示:
- Thread newThread = new Thread(MyRunnable());
- newThread.run(); //should be start();
起初你并不会感觉到有什么不妥,因为run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行run()方法,必须调用新线程的start方法。
线程名
当创建一个线程的时候,可以给线程起一个名字。它有助于我们区分不同的线程。例如:如果有多个线程写入System.out,我们就能够通过线程名容易的找出是哪个线程正在输出。例子如下:
- MyRunnable runnable = new MyRunnable();
- Thread thread = new Thread(runnable, "New Thread");
- thread.start();
- System.out.println(thread.getName());
需要注意的是,因为MyRunnable并非Thread的子类,所以MyRunnable类并没有getName()方法。可以通过以下方式得到当前线程的引用:
- Thread.currentThread();
因此,通过如下代码可以得到当前线程的名字:
- String threadName = Thread.currentThread().getName();
线程代码举例:
这里是一个小小的例子。首先输出执行main()方法线程名字。这个线程JVM分配的。然后开启10个线程,命名为1~10。每个线程输出自己的名字后就退出。
- public class ThreadExample {
- public static void main(String[] args){
- System.out.println(Thread.currentThread().getName());
- for(int i=0; i<10; i++){
- new Thread("" + i){
- public void run(){
- System.out.println("Thread: " + getName() + "running");
- }
- }.start();
- }
- }
- }
需要注意的是,尽管启动线程的顺序是有序的,但是执行的顺序并非是有序的。也就是说,1号线程并不一定是第一个将自己名字输出到控制台的线程。这是因为线程是并行执行而非顺序的。Jvm和操作系统一起决定了线程的执行顺序,他和线程的启动顺序并非一定是一致的。
使用Sleep方法暂停一个线程
使用Thread.sleep()方法可以暂停当前线程一段时间。这是一种使处理器时间可以被其他线程或者运用程序使用的有效方式。sleep()方法还可以用于调整线程执行节奏和等待其他有执行时间需求的线程。
在Thread中有两个不同的sleep()方法,一个使用毫秒表示休眠的时间,而另一个是用纳秒。由于操作系统的限制休眠时间并不能保证十分精确。休眠周期可以被interrups所终止,我们将在后面看到这样的例子。不管在任何情况下,我们都不应该假定调用了sleep()方法就可以将一个线程暂停一个十分精确的时间周期。
SleepMessages程序为我们展示了使用sleep()方法每四秒打印一个信息的例子
- public class SleepMessages {
- public static void main(String args[])
- throws InterruptedException {
- String importantInfo[] = {
- "Mares eat oats",
- "Does eat oats",
- "Little lambs eat ivy",
- "A kid will eat ivy too"
- };
- for (int i = 0; i < importantInfo.length;i++) {
- //Pause for 4 seconds
- Thread.sleep(4000);
- //Print a message
- System.out.println(importantInfo[i]);
- }
- }
- }
main()方法声明了它有可能抛出InterruptedException。当其他线程中断当前线程时,sleep()方法就会抛出该异常。由于这个应用程序并没有定义其他的线程,所以并不用关心如何处理该异常。
中断(Interrupts)
中断是给线程的一个指示,告诉它应该停止正在做的事并去做其他事情。一个线程究竟要怎么响应中断请求取决于程序员,不过让其终止是很普遍的做法。这是本文重点强调的用法。
一个线程通过调用对被中断线程的Thread对象的interrupt()方法,发送中断信号。为了让中断机制正常工作,被中断的线程必须支持它自己的中断(即要自己处理中断)
中断支持
线程如何支持自身的中断?这取决于它当前正在做什么。如果线程正在频繁调用会抛InterruptedException异常的方法,在捕获异常之后,它只是从run()方法中返回。例如,假设在SleepMessages的例子中,关键的消息循环在线程的Runnable对象的run方法中,代码可能会被修改成下面这样以支持中断:
- for (int i = 0; i < importantInfo.length; i++) {
- // Pause for 4 seconds
- try {
- Thread.sleep(4000);
- } catch (InterruptedException e) {
- // We've been interrupted: no more messages.
- return;
- }
- // Print a message
- System.out.println(importantInfo[i]);
- }
许多会抛InterruptedException异常的方法(如sleep()),被设计成接收到中断后取消它们当前的操作,并在立即返回。
如果一个线程长时间运行而不调用会抛InterruptedException异常的方法会怎样? 那它必须周期性地调用Thread.interrupted()方法,该方法在接收到中断请求后返回true。例如:
- for (int i = 0; i < inputs.length; i++) {
- heavyCrunch(inputs[i]);
- if (Thread.interrupted()) {
- // We've been interrupted: no more crunching.
- return;
- }
- }
在这个简单的例子中,代码只是检测中断,并在收到中断后退出线程。在更复杂的应用中,抛出一个InterruptedException异常可能更有意义。
- if (Thread.interrupted()){
- throw new InterruptedException();
- }
这使得中断处理代码能集中在catch语句中。
下面的例子中,有线程池模拟了一个终端的场景。1个是基于sleep的终端,另外一个是循环检测终端的状态,抛出终端的异常。线程池executor.shutdownNow();方法,会立刻终端所有线程。
- package com.chinaso.phl;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class InterruptedTest {
- private static ExecutorService executor = Executors.newFixedThreadPool(10);
- public static void main(String[] args) throws Exception {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- try {
- for (int i = 0; i < Integer.MAX_VALUE; i++) {
- if (Thread.currentThread().isInterrupted()) {
- throw new InterruptedException("循环被中断了");
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- executor.execute(new Runnable() {
- @Override
- public void run() {
- try {
- Thread.sleep(Integer.MAX_VALUE);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- System.out.println("begin");
- Thread.sleep(1000);
- System.out.println("sleep 1000");
- executor.shutdownNow();
- System.out.println("end");
- }
- }
输出
begin
sleep 1000
end
java.lang.InterruptedException: 循环被中断了
at com.chinaso.phl.InterruptedTest$1.run(InterruptedTest.java:18)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.chinaso.phl.InterruptedTest$2.run(InterruptedTest.java:31)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
中断状态标记
中断机制通过使用称为中断状态的内部标记来实现。调用Thread.interrupt()设置这个标记。当线程通过调用静态方法Thread.interrupted()检测中断时,中断状态会被清除。非静态的isInterrupted()方法被线程用来检测其他线程的中断状态,不改变中断状态标记。
按照惯例,任何通过抛出一个InterruptedException异常退出的方法,当抛该异常时会清除中断状态。不过,通过其他的线程调用interrupt()方法,中断状态总是有可能会立即被重新设置。
Joins
Join()方法可以让一个线程等待另一个线程执行完成。若t是一个正在执行的Thread对象,
- t.join();
将会使当前线程暂停执行并等待t执行完成。重载的join()方法可以让开发者自定义等待周期。然而,和sleep()方法一样join()方法依赖于操作系统的时间处理机制,你不能假定join()方法将会精确的等待你所定义的时长。
如同sleep()方法,join()方法响应中断并在中断时抛出InterruptedException。
该例子中有2个线程,根据线程的名字判断是否需要等待,当然也可以设置其他的条件。
- package com.chinaso.phl;
- public class JoinTest {
- public static void main(String[] args) throws Exception {
- MyThread t1 = new MyThread("t1");
- MyThread t2 = new MyThread("t2");
- t1.setThread(t2);
- t2.setThread(t1);
- t1.start();
- t2.start();
- }
- }
- class MyThread extends Thread {
- private Thread thread;
- public MyThread(String name) {
- super(name);
- }
- @Override
- public void run() {
- for (int i = 0; i < 3; i++) {
- try {
- // 如果当前线程是t2,则暂停让其他线程完成执行
- if (Thread.currentThread().getName().equals("t2")) {
- thread.join();
- }
- Thread.sleep(500);
- System.out.println("current thread is :" + this.getName() + ", thread.isAlive()=" + thread.isAlive());
- } catch (Exception e) {
- }
- }
- }
- public Thread getThread() {
- return thread;
- }
- public void setThread(Thread thread) {
- this.thread = thread;
- }
- }
输出结果
current thread is :t1, thread.isAlive()=true
current thread is :t1, thread.isAlive()=true
current thread is :t1, thread.isAlive()=true
current thread is :t2, thread.isAlive()=false
current thread is :t2, thread.isAlive()=false
current thread is :t2, thread.isAlive()=false
java并发(五)竞态条件与临界区
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
多线程同时执行下面的代码可能会出错:
- public class Counter {
- protected long count = 0;
- public void add(long value){
- this.count = this.count + value;
- }
- }
想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:
从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存
观察线程A和B交错执行会发生什么:
this.count = 0;
A: 读取 this.count 到一个寄存器 (0)
B: 读取 this.count 到一个寄存器 (0)
B: 将寄存器的值加2
B: 回写寄存器值(2)到内存. this.count 现在等于 2
A: 将寄存器的值加3
A: 回写寄存器值(3)到内存. this.count 现在等于 3
两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。
竞态条件 & 临界区
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。
java并发(六)线程安全与共享资源
允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。
局部变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:
- public void someMethod(){
- long threadSafeInt = 0;
- threadSafeInt++;
- }
局部的对象引用
对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(译者注:即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:
- public void someMethod(){
- LocalObject localObject = new LocalObject();
- localObject.callMethod();
- method2(localObject);
- }
- public void method2(LocalObject localObject){
- localObject.setValue("value");
- }
样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。
对象成员
对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:
- public class NotThreadSafe{
- StringBuilder builder = new StringBuilder();
- public add(String text){
- this.builder.append(text);
- }
- }
如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:
- NotThreadSafe sharedInstance = new NotThreadSafe();
- new Thread(new MyRunnable(sharedInstance)).start();
- new Thread(new MyRunnable(sharedInstance)).start();
- public class MyRunnable implements Runnable{
- NotThreadSafe instance = null;
- public MyRunnable(NotThreadSafe instance){
- this.instance = instance;
- }
- public void run(){
- this.instance.add("some text");
- }
- }
注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。
当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:
- new Thread(new MyRunnable(new NotThreadSafe())).start();
- new Thread(new MyRunnable(new NotThreadSafe())).start();
现在两个线程都有自己单独的NotThreadSafe对象,调用add()方法时就会互不干扰,再也不会有竞态条件问题了。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。
线程控制逃逸规则
线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。
且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。
即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下代码:
如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:
线程2检查记录X是否存在。检查结果:不存在
线程1插入记录X
线程2插入记录X
同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。
java并发(七)线程安全及不可变性
当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。
我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:
- public class ImmutableValue{
- private int value = 0;
- public ImmutableValue(int value){
- this.value = value;
- }
- public int getValue(){
- return this.value;
- }
- }
请注意ImmutableValue类的成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。
(译者注:注意,“不变”(Immutable)和“只读”(Read Only)是不同的。当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。比如,一个人的出生年月日是“不变”属性,而一个人的年龄便是“只读”属性,但是不是“不变”属性。随着时间的变化,一个人的年龄会随之发生变化,而一个人的出生年月日则不会变化。这就是“不变”和“只读”的区别。(摘自《Java与模式》第34章))
如果你需要对ImmutableValue类的实例进行操作,可以通过得到value变量后创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例:
- public class ImmutableValue{
- private int value = 0;
- public ImmutableValue(int value){
- this.value = value;
- }
- public int getValue(){
- return this.value;
- }
- public ImmutableValue add(int valueToAdd){
- return new ImmutableValue(this.value + valueToAdd);
- }
- }
请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作。
引用不是线程安全的!
重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:
- public void Calculator{
- private ImmutableValue currentValue = null;
- public ImmutableValue getValue(){
- return currentValue;
- }
- public void setValue(ImmutableValue newValue){
- this.currentValue = newValue;
- }
- public void add(int newValue){
- this.currentValue = this.currentValue.add(newValue);
- }
- }
Calculator类持有一个指向ImmutableValue实例的引用。注意,通过setValue()方法和add()方法可能会改变这个引用。因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,因此Calculator类不是线程安全的。换句话说:ImmutableValue类是线程安全的,但使用它的类不是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可。