Java 线程组,Callable,Future,线程池,ThreadLocal类,包装线程不安全的集合

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/WarEric/article/details/51582499

线程组和未处理的异常

Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,对线程组的控制相当于同时控制这批线程.用户所创建的所有线程都属于指定线程组,若没有显示指定线程组,则属于默认线程组.默认情况下子线程和创建它的副线程处于同一个线程组内;例如:A创建了B,则默认A和B处于同一个线程组.

一但加入某个线程组,则中途不能改变该线程的线程组,直到该线程死亡.

Thread提供以下几种方式创建新的线程

  • Thread(ThreadGroup group, Runnable target) 以target的run作为线程体创建新的线程,属于group组
  • Thread(ThreadGroup group, Runnable target, String name) 以target的run作为线程体创建新的线程,属于group组,名字为name
  • Thread(ThreadGroup group, String name) 创建新的线程,属于group组

因为中途不可改变所属线程组,所以没有setThreadGroup()方法,但可以通过getThreadGroup()来返回所属ThreadGroup对象.ThreadGroup类有如下两个构造器

  • ThreadGroup(String name) 以指定的名字创建新的线程组
  • ThreadGroup(ThreadGroup parent, String name) 以指定的名字,父线程组来创建新的线程组

ThreadGroup提供了以下几种常用的方法来管理线程组内的所有线程

  • int activeCount(): 返回此线程组中活动线程的数目
  • interrupt(): 中断此线程组中的所有线程
  • isDaemon(): 判断该线程组是否是后台线程
  • setDaemon(boolean daemon): 这是是否后台线程
  • setMaxPriority(int pri): 设置线程组的最高优先级

接下来就其中一些方法进行使用演示

public class TestThread extends Thread{
    public TestThread(String name){
        super(name);
    }

    public TestThread(ThreadGroup group,String name){
        super(group,name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(getName()+"线程i的变量"+i);
        }
    }
}

测试

public class Test {
    public static void main(String []args){
        ThreadGroup mainGroup=Thread.currentThread().getThreadGroup();
        System.out.println("主线程组的名字:"+mainGroup.getName());
        System.out.println("主线程组是否是后台线程:"+mainGroup.isDaemon());
        new TestThread("主线程组的线程").start();

        ThreadGroup threadGroup=new ThreadGroup("新线程组");
        threadGroup.setDaemon(true);
        System.out.println("threadGroup线程组是否是后台线程组:"+threadGroup.isDaemon());
        TestThread A=new TestThread(threadGroup,"threadGroup线程组A");
        A.start();
        TestThread B=new TestThread(threadGroup,"threadGroup线程组B");
        B.start();
    }
}

运行结果:

主线程组的名字:main
主线程组是否是后台线程:false
threadGroup线程组是否是后台线程组:true
主线程组的线程线程i的变量0
主线程组的线程线程i的变量1
threadGroup线程组A线程i的变量0
threadGroup线程组A线程i的变量1
threadGroup线程组B线程i的变量0
threadGroup线程组B线程i的变量1

ThreadGroup内还定义了一个比较有用的方法:void uncaughtException(Thread t,Throwable e),该方法可以处理该线程组内的线程所抛出的未处理异常.

从JDK1.5开始,Java加强了线程的异常处理,如果执行过程中抛出了一个未处理了异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常.

Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,该接口内只有一个方法:void uncaughtException(Thread t,Throwable e),方法中t代表出现异常的线程,而e代表该线程抛出的异常.

Thread类中提供了两个方法来设置异常处理器:

  • static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh): 为该线程类的所有线程实例设置默认的异常处理器
  • void setUncaughtExceptionHandler(UncaughtExceptionHandler eh): 为指定的线程实例设置异常处理器

ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器.当一个线程抛出未处理异常时,JVM会首先查找该线程对应的异常处理器(setDefaultUncaughtExceptionHandler方法设置的异常处理器),如果找到该异常处理器,将调用该异常处理器处理异常.否则,JVM将会调用该线程所属的线程组对象的uncaughtException方法来处理该异常.

我们来看一下ThreadGroup里是如何实现Thread.UncaughtExceptionHandler里的接口uncaughtException的

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
  1. 如果该线程组有父线程,则调用父线程组的uncaughtException方法来处理该异常.
  2. 如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器,此异常处理器是为该线程所有实例设置的),那就调用该处理器来处理器处理该异常.
  3. 如果该异常对象是ThreadDeath对象,将不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程.

接下来我们自己实现Thread.UncaughtExceptionHandler进行演示

public class MyExHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t+"线程出现了异常:"+e);
    }
}

在主线程进行测试

public class TestHandler {
    public static void main(String []args){
        Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
        int a=5/0;//除数为0导致出现错误
    }
}

输出结果

Thread[main,5,main]线程出现了异常:java.lang.ArithmeticException: / by zero

Callable和Future

为了让线程可以有返回值,从JDK1.5开始,Java提供Callable接口,Callable接口也提供了一个call()方法可以作为线程执行体,但call方法比run方法功能更强大

  • call()方法可以有返回值
  • call()方法可以抛出异常

因此我们完全可以提供一个Callable对象作为Thread的target对象,执行体就是call方法.问题是Callable不是Runnable接口的子接口,所以Callable不能直接作为Thread的target.

JDK1.5提供了Future接口来代表Callable接口里的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口–可以作为Thread类的target.

在Future里定义了如下几个公共方法来控制它关联的Callable任务

  • boolean cancel(boolean mayInterruptIfRunning): 取消该Future里关联的Callable任务.
  • V get(): 返回Callable任务里call的返回值,该方法会导致程序阻塞,必须等到子线程结束才会得到返回值.
  • V get(long timeout, TimeUnit unit): 指定阻塞时间,如果超过时间还没返回值则抛出TimeoutException异常.
  • boolean isCancelled(): 如果Callable任务正常执行完前被取消,返回true.
  • boolean isDone(): 如果Callable任务已经完成,则返回true.

创建并启动有返回值的线程过程如下:

  1. 创建Callable接口的实现类,并实现call方法.注意Callable接口有泛型限制,Callable接口里的泛型参数必须和call的返回值相同.
  2. 创建Callable的实例,用FutureTask类来包装Callable对象,该FutureTask封装了call的返回值.
  3. 使用FutureTask对象作为Thread的target创建,启动新线程.
  4. 使用FutureTask对象的方法来获得子线程执行结束的返回值

下面通过一段程序来演示,先实现Callable接口

public class RtnThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "的循环变量i=" +i);
        }
        return i;
    }
}

创建,启动

public class CallableTest {
    public static void main(String []args){
        RtnThread rtnThread=new RtnThread();
        FutureTask<Integer> task=new FutureTask<Integer>(rtnThread);
        new Thread(task,"有返回值的线程").start();
        try{
            System.out.println("子线程的返回值:"+task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

运行结果

有返回值的线程的循环变量i=17
有返回值的线程的循环变量i=18
有返回值的线程的循环变量i=19
子线程的返回值:20

成功得到了返回值,只是调用get()方法时会阻塞主线程

线程池

系统启动一个线程成本是比较高的,因为涉及与操作系统交互.这时应考虑使用线程池来提高性能.线程池在创建时就启动大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当该线程执行结束后并不会死亡,而是返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法.

使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时会导致JVM性能急剧下降,甚至导致崩溃,而线程池可以控制系统中并发线程的数量

JDK1.5以前都需要自己实现,以后的提供了一个Executors工厂类来产生线程池,而该工厂类包含如下几个静态工厂方法来创建线程池:

  • newCachedThreadPool(): 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存到线程池中.
  • newFixedThreadPool(int nThreads): 创建一个可重用,具有固定线程数的线程池.
  • newSingleThreadExecutor(): 创建只有一条线程的线程池.
  • ScheduledExecutorService newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池,它可以在指定延迟后执行任务.
  • newSingleThreadScheduledExecutor(): 创建只有一条线程的线程池,它可以在指定延迟后执行任务.

上面的前三个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程.而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务.

ExecutorService代表尽快执行线程的线程池,ExecutorService里提供了如下三个方法:

1.  Future<?> submit(Runnable task): 将一个Runnable对象交给指定的线程池.线程池将在有空闲的时执行Runnable对象所代表的任务.其中Future对象代表Runnable任务的返回值--但run没有返回值,所以Future对象在run执行结束后返回null.但可以调用Future的isDone(),isCancelled()方法来判断Runnable对象的执行状态.
2.  <T> Future<T> submit(Runnable task, T result): 将一个Runnable对象交给指定的线程池.线程池将在有空闲的时执行Runnable对象所代表的任务.result显示指定线程执行结束后的返回值,所以Future对象将在run方法执行结束后返回result.
3.  <T> Future<T> submit(Callable<T> task): 将一个Callable对象交给指定的线程池.线程池将在有空闲的时执行Callable对象所代表的任务.Future代表Callable对象里的call的返回值.

ScheduledExecutorService代表可以在指定延迟或周期性执行线程任务的线程池,它提供了如下4个方法:

1.  ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit): 指定任务在延迟delay后执行
2.  <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit): 指定任务在延迟delay后执行
3.  public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit): 在延迟之后按照一定的周期执行
4.  ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit): initialDelay表示启动之前的延迟,delay表示启动以后,在每一次执行终止和下一次执行开始之间都给定delay延迟.如果任务的一次执行遇到异常就会取消后续执行

当用完一个线程池后应该调用shutdown()方法,该方法将启动线程池的关闭序列,调用了shutdown()的线程池不再接受新的任务,但会将以前提交的任务执完.当线程池中所有的任务都执行完,线程池就会死亡.另外也可以调用shutdownNow()方法来关闭线程池,该方法将停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行任务的列表.

使用线程池的步骤如下:

  1. 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池.
  2. 创建Runnable或Callable的实例,作为线程执行任务.
  3. 调用ExecutorService对象的submit方法来提交Runnable或Callable的实例.
  4. 当不想提交任何任务时调用ExecutorService对象的shutdown()方法关闭线程池

下面进行一个简单的演示,更多复杂的用法请自己看api

public class TestThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"的i值为"+i);
        }
    }
}
public class ThreadPoolTest {
    public static void main(String []args){
        ExecutorService pool= Executors.newFixedThreadPool(7);
        pool.submit(new TestThread());
        pool.submit(new TestThread());
        pool.shutdown();
    }
}

运行结果

pool-1-thread-1的i值为98
pool-1-thread-1的i值为99
pool-1-thread-2的i值为0
pool-1-thread-2的i值为1
pool-1-thread-2的i值为2

线程相关类

ThreadLocal类

ThreadLocal是Thread Local Variable(局部线程变量)的意思.局部线程变量的使用其实很简单,就是为每一个使用该变量的线程都提供一个该变量值的副本,每一个线程都可以独自的改变自己的副本,而不会和其它的线程副本冲突.从线程的角度来看,就好象每一个线程完全拥有了该变量.

ThreadLocal提供了如下三个方法:

  • T get(): 返回此线程局部变量当前的副本值
  • void remove() 删除此线程局部变量当前的副本值
  • set(T value) 设置此线程局部变量当前的副本值

下面来演示ThreadLoacl的作用

public class Account {

    private ThreadLocal<String> name = new ThreadLocal<String>();

    public Account(String name) {
        this.name.set(name);
    }

    public String getName() {
        return this.name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }

}
public class MyTest extends Thread {
    private Account account;
    private String name;

    public MyTest(Account account, String name) {
        this.account = account;
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(account.getName() + "的i=" + i);
        }
        account.setName(name);
        for (int i = 0; i < 2; i++) {
            System.out.println(account.getName() + "的i=" + i);
        }
    }
}
public class ThreadLocalTest {

    public static void main(String[] args) {
        Account account = new Account("Eric");
        new MyTest(account, "A").start();
        new MyTest(account, "B").start();
    }

}

运行结果

nulli=0
nulli=0
nulli=1
nulli=1
Ai=0
Bi=0
Ai=1
Bi=1

从上面的null可以看出其实在account一进入新的Thread的时候就赋予了新的副本,因为我们的account本身是赋予了”Eric”这个名字的,但开始却读到了null,说明已经给予了新的副本

ThreadLocal只是将冲突的资源进行了多份复制,各自使用各自的,因此之间不能通信.ThreadLocal和锁机制解决的问题是有本质的区别的.

包装线程不安全的集合

Java中的ArrayList,LinkedList,HashSet,TreeSet,HashMap等都是线程不安全的,也就是有可能当多个线程向这个集合中放入一个元素时,可能会破坏这些数据集合的完整性.

如果程序有多条线程需要访问以上集合,我们可以使用Collections提供的静态方法把这些集合包装成线程安全的集合.Collections提供了如下几个静态方法:

1.   static <T> Collection<T> synchronizedCollection(Collection<T> c): 返回指定collection对应的线程安全的collection.
2.  static <T> List<T> synchronizedList(List<T> list):  返回指定List对应的线程安全的List对象.
3.  static <K,V> Map<K,V> synchronizedMap(Map<K,V> m): 返回指定Map对应的线程安全的Map对象.
4.  static <T> Set<T> synchronizedSet(Set<T> s): 返回指定Set对应的线程安全的Set对象.
5.  static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m): 返回指定SortedMap对应的线程安全的SortedMap对象.
6.  static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s): 返回指定SortedSet对应的线程安全的SortedSet对象.

例如我们要使用多线程中线程安全的HashMap对象,可用如下方式:

    Map m=Collections.synchronizedMap(new HashMap<>());

线程安全的集合类

从JDK1.5开始,在java.util.concurrent包下提供了ConcurrentHashMap和ConcurrentLinkedDeque支持并发访问的集合,由于内部使用非常复杂的算法,支持并发访写,具体使用可以查看api

展开阅读全文

没有更多推荐了,返回首页