漫谈并发编程(二):java线程的创建与基本控制

java线程的创建

定义任务

在java中使用任务这个名词来表示一个线程控制流的代码段,用Runnable接口来标记一个任务,该接口的run方法为线程执行的代码段。

public class LiftOff implements Runnable {
     protected int countDown = 10;
     private static int taskCount = 0;
     private final int id = taskCount++;
     public void run() {
          while(countDown-- > 0 ) {
               System.out.print("#"+id+"("+countDown+"). ");
               Thread.yield();
          }
     }
}

Thread.yield()的调用时对线程调度器的一种建议,意思是现在是现在愿意主动放弃cpu时间片的占用, 交于其他线程使用。

现在我们使用该类。

public class MainThread {
     public static void main(String[] args) {
          LiftOff lauch = new LiftOff();
          lauch.run();
     }
}

上面的用法是在主线程中直接调用了该类对象的run方法,而并非创建了一个新的线程执行该任务。要想实现线程行为,你必须显式的将一个任务附着到线程上。在java中使用Thread类来创建一个线程。

public class BasicThreads {
     public static void main(String[] args) {
          Thread t = new Thread(new LiftOff());
          t.start();
          System.out.println("Waiting for LiftOff");
     }
}

一个线程只能对应一个任务,但是一个任务可以被多个线程所执行。在一个任务被多个线程执行的情况下,该任务内的成员对象也被多个线程共享。如:

public class MoreBasicThreads {
     public static void main(String args[]) {
          LiftOff liftOff = new LiftOff();
          for(int i = 0; i < 5; i++) {
               new Thread(liftOff).start();
          }
     }
}

可以看到输出结果是非常怪异的:顺序颠倒了,且"5"直接消失了。在这里是由于countDown的自减操作与输出之间的空隙会被其他线程插入,以及对于taskCount变量还存在可见性的问题(jvm会对其进行优化,导致每次操作并非都在内存中进行,所以每个线程所看到的变量状态是不一致的)。程序每次的运行结果都会不同,这种现象被称为线程竞速。在后面的文章中,我们将介绍线程间如何安全的协作。

(在这里,不再介绍以继承Thread方式来定义任务并启动线程的方式,因为那种方式受继承及线程间不能在任务内共享资源等问题的局限。)

Executor

JDK5为我们提供了Executor(线程执行器)工具来帮助我们管理线程,借助该工具,我们可以高效管理多个线程。

常用有三种行为的Executor。

使用CachedThreadPool的Executor

CacheThreadPool将为每个任务都创建一个线程,该线程池中线程的数量没有上限。如果在该线程池中的一个线程运行结束,那么该线程将被线程池回收等待下次创建新线程使用。例:

public class CachedThreadPool {
     public static void main(String []args) {
          ExecutorService exec = Executors.newCachedThreadPool();
          for(int i = 0 ; i < 5; i++)
               exec.execute( new LiftOff() );
          exec.shutdown();
}
}

/*Output :(Sample)

#1(9). #1(8). #1(7). #1(6). #1(5). #1(4). #3(9). #4(9). #2(9).

#0(9). #2(8). #4(8). #3(8). #3(7). #3(6). #3(5). #3(4). #3(3).

#3(2). #3(1). #3(0). #1(3). #4(7). #4(6). #4(5). #2(7). #2(6).

#0(8). #0(7). #0(6). #0(5). #0(4). #0(3). #0(2). #0(1). #0(0).

#4(4). #1(2). #4(3). #1(1). #4(2). #1(0). #4(1). #4(0). #2(5).

#2(4). #2(3). #2(2). #2(1). #2(0).

对shutdown()方法的调用可以防止新任务被提交给这个Executor,但是已提交的任务会继续运行直到完成。

使用FixedThreadPool的Executor

FixedThreadPool会预先定制好线程池的容量大小(线程数量),即在创建该线程池的时候线程已经被分配,后面不再允许线程数量的扩充。使用这种线程池能将代价高昂的线程分配的工作一次性执行完成,并且避免你滥用线程。创建线程例:

public class FixedThreadPool{
     public static void main(String []args) {
          ExecutorService exec = Executors.FixedThreadPool();
          for(int i = 0 ; i < 5; i++)
               exec.execute( new LiftOff() );
          exec.shutdown();
     }
}

SingleThreadExecutor

该Executor只使用一个线程,就像是线程数量为1的FixedThreadPool。如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务都将使用相同的线程。

public class SingleThreadExecutor {
     public static void main(String []args) {
          ExecutorService exec = Executors.newSingleThreadExecutor();
          for(int i = 0 ; i < 5; i++)
               exec.execute( new LiftOff() );
          exec.shutdown();
     }
}

/*Output :

#0(9). #0(8). #0(7). #0(6). #0(5). #0(4). #0(3). #0(2).

#0(1). #0(0). #1(9). #1(8). #1(7). #1(6). #1(5). #1(4).

#1(3). #1(2). #1(1). #1(0). #2(9). #2(8). #2(7). #2(6).

#2(5). #2(4). #2(3). #2(2). #2(1). #2(0). #3(9). #3(8).

#3(7). #3(6). #3(5). #3(4). #3(3). #3(2). #3(1). #3(0).

#4(9). #4(8). #4(7). #4(6). #4(5). #4(4). #4(3). #4(2).

#4(1). #4(0).

SingleThreadExecutor能够提供一定程度上的并发保证,即如果一个任务只被该类型的Exector所执行的话,那么便不会存在线程竞速的问题。对于逻辑上独立的任务,而并非性能要求需要使用线程的情况下,使用该Executor来管理线程是不二的选择。

从任务中产生返回值

Runnable是执行工作时的独立任务,但是它不返回任何值。如果你希望任务在完成时能够返回一个值,使用Callable接口而不是Runnable接口。Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它,下面是一个示例:

class TaskWithResult implements Callable<String> {
     private int id;
     public TaskWithResult(int id) {
          this.id = id;
     }

     @Override
     public String call() throws Exception {
          return "result of TaskWithResult " + id;
     }
}

public class CallableDemo {
     public static void main(String[] args) {
          ExecutorService exec = Executors.newCachedThreadPool();
          ArrayList<Future<String>> results = new ArrayList<Future<String>>();
          for(int i = 0; i < 10;i++) {
               results.add( exec.submit(new TaskWithResult(i)));
          }
          for(Future<String> fs : results)
               try {
                    System.out.println(fs.get());
               } catch (InterruptedException e) {
                    System.out.println(e);
               } catch (ExecutionException e) {
                    System.out.println(e);
               }
          exec.shutdown();
     }
}

submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。你可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你可以不用isDone()方法进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪。你还可以在试图调用get()来获取结果之前,先调用具有超时get(),或者调用isDone()方法来查看任务是否完成。

对于Callable应该要知道两点:1. 使用Runnable来创建的线程在运行中产生的对象或数据一般会使用额外的对象进行管理,而使用Callable产生的数据或对象可以不使用额外对象管理,线程的执行结果直接通过Future的get方法返回。2. 产生的Future对象的get方法具有阻塞的特性,所以可以利用此方法进行一些协作的操作,从而避免引入一些其他的同步设施,如原子锁等。

休眠

Thread.sleep(long)函数可使调用该函数的线程休眠,单位为毫秒。除了这种方式,还可以TimeUnit类进行睡眠,如TimeUnit.MILLISECONDS.sleep(1000),这个方法允许你指定sleep()延迟的时间单元,因此可以提供更好的可阅读性。

优先级

线程的优先级将该线程的重要性传递给了调度器。调度器将倾向于将优先权最高的线程先执行,优先权较低的线程执行的频率较低。在绝大多数时间里,所有的线程都应该以默认的优先级运行。试图操纵线程优先级通常是一种错误。

范例如下:

public class SimplePriorities implements Runnable{
     private int countDown = 5;
     private double d;
     private int priority;
     public SimplePriorities(int priority) {
          this.priority = priority;
     }
     public String toString( ) {
          return Thread.currentThread() + " : " + countDown;
     }
     public void run( ) {
          Thread.currentThread().setPriority( priority );
          while(true) {
               for(int i = 1 ; i < 100000 ; i++) {
                    d += (Math.PI + Math.E) / (double)i;
               if(i % 1000 == 0)
                    Thread.yield();
          }
          System.out.println(this);
          if( --countDown == 0) return;
     }
}
     public static void main(String[] args) {
          ExecutorService exec = Executors.newCachedThreadPool();
          for(int i = 0; i < 5; i++)
               exec.execute( new SimplePriorities(Thread.MIN_PRIORITY));
               exec.execute( new SimplePriorities(Thread.MAX_PRIORITY));
               exec.shutdown();
     }
}

/output:

Thread[pool-1-thread-6,10,main] : 5

Thread[pool-1-thread-4,1,main] : 5

Thread[pool-1-thread-6,10,main] : 4

Thread[pool-1-thread-6,10,main] : 3

Thread[pool-1-thread-6,10,main] : 2

Thread[pool-1-thread-3,1,main] : 5

Thread[pool-1-thread-4,1,main] : 4

Thread[pool-1-thread-2,1,main] : 5

..................................

在上面的代码中使用了Thread.toString()方法来打印线程的名称、线程的优先级以及线程所属的"线程组"。你可以通过构造器来设置这个名称,这里是自动生成的名称。除此之外,通过Thread.currrentThread()可以获取当前线程的引用。

尽管JDK有10个优先级,但它与多数操作系统都不能映射的很好,因为每个系统的优先级规则不同。唯一可移植的方法是当调整优先级的时候,只使用MAX_PRIORITY、NORM_PRIORITY、和MIN_PRIORITY三种级别。

让步

使用yield()方法可以给线程调度机制一个暗示:你的工作已经做得差不多了,可以让别的线程使用CPU了(不过这只是一个暗示,没有任何机制保证它将会被采纳)。当调用yield时,你也是在建议具有相同优先级的其他线程可以运行。

使用Thread.yield()时常可以产生分布良好的处理机制。但是,大体上,对于任何重要的控制或在调整应用时,都不能依赖yield()。实际上,yield()经常被误用。

后台线程

所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。比如,执行main()的就是一个非后台线程。

public class SimpleDaemons implements Runnable {
     public void run() {
          try{
               while(true) {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println(Thread.currentThread()+" "+this);
               }
          } catch (InterruptedException e) {
               System.out.println("sleep() interrupted");
          }
     }
     public static void main(String []args) throws Exception {
          for(int i = 0; i < 10; i++) {
               Thread daemon = new Thread(new SimpleDaemons());
               daemon.setDaemon(true);
               daemon.start();
          }
          System.out.println("All daemons started");
          TimeUnit.MILLISECONDS.sleep( 175 );
     }
}  

/* Output: (Sample)

All daemons started

Thread[Thread-0,5,main] [email protected]

Thread[Thread-7,5,main] [email protected]

Thread[Thread-2,5,main] [email protected]

Thread[Thread-5,5,main] [email protected]

Thread[Thread-3,5,main] [email protected]

Thread[Thread-6,5,main] [email protected]

Thread[Thread-9,5,main] [email protected]

Thread[Thread-1,5,main] [email protected]

Thread[Thread-4,5,main] [email protected]

Thread[Thread-8,5,main] [email protected]

必须在线程启动之前调用setDaemon()方法,才能将它设置为后台线程。

如果我们要产生很多的后台线程,可以使用ThreadFactory定制由Executor创建的线程的属性(后台、优先级、名称):

public class DaemonThreadFactory implements ThreadFactory{
     public Thread newThread(Runnable r) {
          Thread t = new Thread(r);
          t.setDaemon(true);
          return t;
     }
}     

public class DaemonFromFactory implements Runnable {
     @Override
     public void run() {
          try{
               while(true) {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println( Thread.currentThread() + " " +this);
               }
          } catch (InterruptedException e) {
               System.out.println("Interrupted");
          }
     }
     public static void main(String args[]) throws Exception {
          ExecutorService exec = Executors.newCachedThreadPool();
          for(int i = 0; i < 10; i++)
               exec.execute(new DaemonFromFactory());
          System.out.println("All daemons started");
          TimeUnit.MILLISECONDS.sleep(500);
     }
}

可以通过调用Thread对象的isDaemon()方法来确定线程是否是一个后台线程,如果是一个后台线程,那么它创建的任何线程都将被自动设置成后台线程。

后台线程的finally子句可能得不到执行。当最后一个非后台线程终止时,后台线程会"突然"终止。因此,一旦main()退出,JVM就会立即关闭所有的后台进程,而不会有任何你希望出现的确认形式。

加入一个线程

一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复(t.isALive()返回为假)。

也可以在调用join()时带上一个超时参数(单位可以是毫秒,或者纳秒),如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。

捕获异常

由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常,使用Executor来解决这个问题。

为了解决这个问题,我们要修改Executor产生线程的方式。Thread.UncaughtException-Handler是Jdk5.0的引入的接口,它允许你在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。为了使用它,我们创建一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler。我们将这个工厂传递给Executors创建新的ExecutorService的方法:

class ExceptionThread2 implements Runnable {
     public void run() {
          Thread t = Thread.currentThread();
          System.out.println("run() by " + t);
          System.out.println("eh = " + t.getUncaughtExceptionHandler());
          throw new RuntimeException();
     }
}

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
     public void uncaughtException(Thread t, Throwable e) {
          System.out.println("caught " + e);
     }
}

class HandlerThreadFactory implements ThreadFactory {
     public Thread newThread(Runnable r) {
          System.out.println(this + " creating new Thread");
          Thread t = new Thread(r);
          System.out.println("created " + t);
          t.setUncaughtExceptionHandler( new MyUncaughtExceptionHandler());
          System.out.println("eh = " + t.getUncaughtExceptionHandler());
          return t;
     }
}

public class CaptureUncaughtException {
     public static void main(String []args) {
          ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
          exec.execute(new ExceptionThread2());
     }
}

/* Output:

[email protected] creating new Thread

created Thread[Thread-0,5,main]

eh = [email protected]

run() by Thread[Thread-0,5,main]

eh = [email protected]

caught java.lang.RuntimeException

上面的示例使得你可以按照具体情况逐个地设置处理器。如果你知道要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器:

public class SettingDefaultHandler {
     public static void main(String[] args) {
          Thread.setDefaultUncaughtExceptionHandler(
               new MyUncaughtExceptionHandler( ) );
          ExecutorService exec = Executors.newCachedThreadPool();
          exec.execute(new ExceptionThread());
     }
} 

这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。系统会检查线程的专有版本,如果没有发现,则检查线程组是否有专有的uncaughtException()方法,如果也没有,再调用defaultUncaughtExceptionHandler。

时间: 2024-08-07 21:20:18

漫谈并发编程(二):java线程的创建与基本控制的相关文章

Java并发编程(01):线程的创建方式,状态周期管理

本文源码:GitHub·点这里 || GitEE·点这里 一.并发编程简介 1.基础概念 程序 与计算机系统操作有关的计算机程序.规程.规则,以及可能有的文件.文档及数据. 进程 进程是计算机中的程序,关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.在早期面向进程设计的计算机结构中,进程是程序的基本执行实体:在面向线程设计的计算机结构中,进程是线程的容器.程序是指令.数据及其组织形式的描述,进程是程序的实体. 线程 线程是操作系统能够进行运算调度的最小单

并发编程总结——java线程基础1

最近没事,顺便看看java并发编程的东西,然后总结纪录下来,大家如果能看到帮忙指正指正哈哈,另外一方面也为以后自己回顾的时候可以看看. 关于并发编程,准备从几个点切入: 1.java线程几本知识 2.juc原子类 3.锁 4.juc集合 5.线程池 ------------------------------------------------------------------- 分割线开始.... 说到多线程,java中超父类Object中有wait()和notify()方法,包括Runna

Java并发编程:Java线程池核心ThreadPoolExecutor的使用和原理分析

目录 引出线程池 Executor框架 ThreadPoolExecutor详解 构造函数 重要的变量 线程池执行流程 任务队列workQueue 任务拒绝策略 线程池的关闭 ThreadPoolExecutor创建线程池实例 参考: 引出线程池 线程是并发编程的基础,前面的文章里,我们的实例基本都是基于线程开发作为实例,并且都是使用的时候就创建一个线程.这种方式比较简单,但是存在一个问题,那就是线程的数量问题. 假设有一个系统比较复杂,需要的线程数很多,如果都是采用这种方式来创建线程的话,那么

JAVA 并发编程之守护线程的创建与运行

java里有一种特殊的线程叫做守护线程(Daemon)线程.这种线程的优先级很低,通常来说,当同一个应用程序里没有其他的线程运行的时候,守护线程才运行.当程序中唯一运行的的线程是守护线程时,并且守护线程执行结束后 ,JVM也就结束了这个程序. 因为这种特性,守护线程通常被用来作为同一程序中普通线程(用户线程)的服务提供者.它们通常是无线循环的,以等待服务请求或者执行线程的任务.它们不能做重要工作,因为我们不可能知道守护线程什么时候获取CPU时钟,并且,在没有其他线程运行时,守护线程随时可以结束.

漫谈并发编程(五):线程之间的协作

编写多线程程序需要进行线程协作,前面介绍的利用互斥来防止线程竞速是来解决线程协作的衍生危害的.编写线程协作程序的关键是解决线程之间的协调问题,在这些任务中,某些可以并行执行,但是某些步骤需要所有的任务都结束之后才能开动. wait()与notifyAll() wait()使你可以等待某个条件发生变化,wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化. 调用sleep

漫谈并发编程(一) - 并发简介

并发编程是每个程序员进阶的必修之课,想写一个安全稳定,性能强劲的并发程序可没那么容易.我将在未来的日子里,与大家分享一个并发小白成长路上的所思所想.并发编程的思想是通的,但是例子得要是具现的,在该系列中将使用java语言用以演示. 此文作为为漫谈并发编程系列的第一篇,由于本人喜欢先论理再论事,而非先论事再论理,所以就以一篇对并发的文字描述开头了. 并发编程由来 早年的计算机中没有操作系统,在某个时间段内只支持运行一个程序,并且这个程序能访问计算机的所有资源.在这个程序完全执行完后,再执行下一个程

漫谈并发编程(一) - 并发简单介绍

并发编程是每一个程序猿进阶的必修之课,想写一个安全稳定,性能强劲的并发程序可没那么easy.我将在未来的日子里,与大家分享一个并发小白成长路上的所思所想.并发编程的思想是通的,可是样例得要是具现的,在该系列中将使用java语言用以演示. 此文作为为漫谈并发编程系列的第一篇,探本溯源,以一篇对并发的文字描写叙述开头. 并发编程由来 早年的计算机中没有操作系统,在某个时间段内仅仅支持运行一个程序,而且这个程序能訪问计算机的全部资源.在这个程序全然运行完后,再运行下一个程序. 在此时,引入并发编程的优

Java线程:创建与启动

Java线程:创建与启动 一.定义线程 1.扩展java.lang.Thread类. 此类中有个run()方法,应该注意其用法: public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法:否则,该方法不执行任何操作并返回.   Thread 的子类应该重写该方法. 2.实现java.lang.Runnable接口. void run() 使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独

【Java并发编程二】同步容器和并发容器

一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchornized. 另一个是Collections类中提供的静态工厂方法创建的同步包装类. 同步容器都是线程安全的.但是对于复合操作(迭代.缺少即加入.导航:根据一定的顺序寻找下一个元素),有时可能需要使用额外的客户端加锁进行保护.在一个同步容器中,复合操作是安全