多线程-5

线程组和未处理的异常

Java使用ThreadGroup来代表线程组

一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组

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):创建新线程,新线程名为name,属于group线程组。

    创建线程组

    Thread类没有提供setThreadGroup的方法来改变线程所属的线程组,但提供了一个getThreadGroup()方法来返回该线程所属的线程组。
    getThreadGroup()方法的返回值是ThreadGroup对象,表示一个线程组。ThreadGroup类有如下两个简单的构造器来创建实例:
    ThreadGroup(String name):以指定线程组名字来创建新的线程组。
    ThreadGroup(ThreadGroup parent, String name):以指定的名字、指定的父线程组创建一个新线程组。

    怎样把线程放入指定的线程组中呢?
    在创建一个Thread实例时,通过传入的ThreadGroup对象,即可将该线程放入指定的线程组。

    通过线程组对这批线程进行整体的管理。

    ThreadGroup提供了如下两个方法:

    • setDaemon(boolean daemon):控制将线程组本身设置为后台线程组。并不是将它包含的线程设置为后台线层。 如果它包含的所有线程都死了,后台线程组本身也会自动销毁。
    • setMaxPriority(优先级):它是设置线程租的最高优先级。 该线程组已有的线程的优先级不会受影响。对以后新添加的线程的优先级才会有影响。

    在JDK1.5以前:如果线程出现了异常,系统会自动回调他所在的线程组的uncaughtException(Thread t,Throwable e)方法来修复该异常。
    在JDK 1.5之后,线程允许自行设置“异常处理器”,无需线程组。
    Thread.setDefaultUncaughtExceptionHandler:为所有线程(包括主线程)设置磨人的异常处理器。
    实例.setUncaughtExceptionHandler:为当前线程实例设置异常处理器。
    【实例设置的异常处理器会覆盖默认的异常处理器】

    线程组处理异常的默认流程

    1. 如果该线程组有父线程组,则调用父线程组的uncaughtException方法来处理该异常。
    2. 如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler方法设置的异常处理器),那就调用该异常处理器来处理该异常。
    3. 如果该异常对象是ThreadDeath的对象,将不做任何处理;否则将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。

程序实例

 class MyThread extends Thread
{
// 提供指定线程名的构造器
public MyThread(String name)
{
    super(name);
}
// 提供指定线程名、线程组的构造器
public MyThread(ThreadGroup group , String name)
{
    super(group, name);
}
public void run()
{
    for (int i = 0; i < 20 ; i++ )
    {
        System.out.println(getName() + " 线程的i变量" + i);
    }
}
}
public class ThreadGroupTest
{
public static void main(String[] args)
{
    // 获取主线程所在的线程组,这是所有线程默认的线程组
    ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
    System.out.println("主线程组的名字:"
        + mainGroup.getName());
    System.out.println("主线程组是否是后台线程组:"
        + mainGroup.isDaemon());
    new MyThread("主线程组的线程").start();
    ThreadGroup tg = new ThreadGroup("新线程组");
    tg.setDaemon(true);
    System.out.println("tg线程组是否是后台线程组:"
        + tg.isDaemon());
    MyThread tt = new MyThread(tg , "tg组的线程甲");
    tt.start();
    new MyThread(tg , "tg组的线程乙").start();
}
}  

线程池

池(Pool)的本质,就是一种“缓存”技术。

是否要缓存一个对象,取决于该对象的创建成本。当然也要考虑到系统内存大小。

缓存的本质:牺牲内存空间,来换取时间成本。

资源池(Resource Pool): 用于解决资源的频繁请求、释放所造成的性能下降。

线程对象的创建成本,比较大。虽然创建线程的成本,比创建进程的成本要小得多。但相比普通Java对象,Thread的创建成本依然较大。
为了解决这个问题,我们使用线程池。

系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法

JDK1.5提供了一个Executors工厂类来产生线程池,该工厂类里包含如下几个==静态工厂方法==来创建连接池:
* newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
* newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
* newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法时传入参数为1。
* newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
* newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。

ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个Runnable对象或Callable对象(代表线程任务)提交给该线程池即可,该线程池就会尽快执行该任务。ExecutorService里提供了如下三个方法:
1. Future

使用线程池的步骤

(1)调用Executors类的静态工厂方法创建一个ExecutorService对象或ScheduledExecutorService对象,其中前者代表简单的线程池,后者代表能以任务调度方式执行线程的线程池。
(2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
(3)调用ExecutorService对象的submit方法来提交Runnable实例或Callable实例;或调用ScheduledExecutorService的schedule来执行线程。
(4)当不想提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池。

==使用Lambda表达式直接创建对象,可以无需先创建实现类,再创建对象,详见P722==

程序实例

public class ThreadPoolTest {
public static void main(String[] args) throws Exception {
    //创建一个具有固定线程数(6)的线程池
    ExecutorService pool = Executors.newFixedThreadPool(6);
    //使用Lambda表达式创建Runnable对象
    Runnable target = () -> {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "的i的值为: " + i);
        }
    };
    //向线程池中提交两个线程
    pool.submit(target);
    pool.submit(target);
    //关闭线程池
    pool.shutdown();


    }

}

Java8增强的ForkJoinPool

Java提供了ForkJoinPool来支持充分利用多CPU、多核CPU的优势,可以把一个任务拆分成多个小任务,把多个小任物放到多个处理器核心上并行执行;当多个小任物执行完之后,再将这些执行结果合并起来。
详见P754

程序实例

1 没有返回值的任务 RecursiveAction

//继承RecursiveAction来实现“可分解”不需要返回值的任务
class PrintTask extends RecursiveAction {
//每个小任务最多打印50个数字
private static final int THRESHOLD = 50;
private int start;
private int end;

//打印从start到end的任务
public PrintTask(int start, int end) {
    this.start = start;
    this.end = end;
}

@Override
protected void compute() {
//当end与static之间的差小于THRESHOLD时,开始打印
    if (end - start < THRESHOLD) {
    for(int i=start;i<end;i++){
        System.out.println(Thread.currentThread().getName() + "的i值" + i);
    }

    }
    else
    {
        //当end与start之间的差大于50,将任务分解
        int middle = (start+end)/2;
        PrintTask left = new PrintTask(start,middle);
        PrintTask right= new PrintTask(middle,end);
        //并行执行两个小任务
        left.fork();
        right.fork();
    }
}
}

public class ForkJoinPoolTest {
public static void main(String[] args)throws Exception{
    ForkJoinPool pool = new ForkJoinPool();
    //提交可分解的PrintTask任务
    pool.submit(new PrintTask(0,300));
    pool.awaitTermination(2, TimeUnit.SECONDS);
    //关闭线程池
    pool.shutdown();
}
}

2 有返回值的任务: RecursiveTask , 泛型参数T代表了该任务的返回值

import java.util.concurrent.*;
import java.util.*;


// 继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer>
{
// 每个“小任务”只最多只累加20个数
private static final int THRESHOLD = 20;
private int arr[];
private int start;
private int end;
// 累加从start到end的数组元素
public CalTask(int[] arr , int start, int end)
{
    this.arr = arr;
    this.start = start;
    this.end = end;
}
@Override
protected Integer compute()
{
    int sum = 0;
    // 当end与start之间的差小于THRESHOLD时,开始进行实际累加
    if(end - start < THRESHOLD)
    {
        for (int i = start ; i < end ; i++ )
        {
            sum += arr[i];
        }
        return sum;
    }
    else
    {
        // 如果当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
        // 将大任务分解成两个小任务。
        int middle = (start + end) / 2;
        CalTask left = new CalTask(arr , start, middle);
        CalTask right = new CalTask(arr , middle, end);
        // 并行执行两个“小任务”
        left.fork();
        right.fork();
        // 把两个“小任务”累加的结果合并起来
        return left.join() + right.join();    // ①
    }
}
}
public class Sum
{
public static void main(String[] args)
        throws Exception
{
    int[] arr = new int[100];
    Random rand = new Random();
    int total = 0;
    // 初始化100个数字元素
    for (int i = 0 , len = arr.length; i < len ; i++ )
    {
        int tmp = rand.nextInt(20);
        System.out.println(tmp);
        // 对数组元素赋值,并将数组元素的值添加到sum总和中。
        total += (arr[i] = tmp);
    }
    System.out.println(total);
    // 创建一个通用池
    ForkJoinPool pool = ForkJoinPool.commonPool();
    // 提交可分解的CalTask任务
    Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
    System.out.println(future.get());
    // 关闭线程池
    pool.shutdown();
}
}

==其中Random类专门用来生成随机数, nextInt(20)方法就是代表生成0~20的随机数(包含0,但不包括20)。==


# 线程相关类
## ThreadLocal类
ThreadLocal,是Thread Local Variable(线程局部变量)的意思,也许将它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

ThreadLocal类的用法非常简单,它只提供了如下三个public方法:
1. T get():返回此线程局部变量中当前线程副本中的值。
2. void remove():删除此线程局部变量中当前线程的值。
3. void set(T value):设置此线程局部变量中当前线程副本中的值。

包装线程不安全的集合

如果程序有多条线程可能访问以上ArrayList、HashMap等集合,可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。Collections提供了如下几个静态方法:

  • static Collection synchronizedCollection(Collection c):返回指定 collection 对应的线程安全的collection。
  • static List synchronizedList(List list):返回指定List对应的线程安全的List对象。
  • static
//使用Collections的synchronizedMap方法将一个普通的HashMap包装成线程安全的类  
     HashMap m =Collections.synchronizedMap(new HashMap());

如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装。

线程安全的集合类

java.util.concurrent包下提供了提供了ConcurrentHashMap、ConcurrentLinkedQueue两个支持并发访问的集合,它们分别代表了支持并发访问的HashMap和支持并发访问的Queue。

当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择。
详见P760

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值