多线程的好处
1、并行编程可以使程序执行速度极大的提高,java本身是一种多线程语言。
2、使用多线程可以利用机器额外的处理器,资源充分利用。
简单介绍
java的线程机制是抢占式的,这表示调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合
理的时间去驱动它的任务,并发编程使我们可以将程序划分为多个分离的、独立运行的任务。
任务实现
通过实现Runnable接口实现线程:
public class LiftOff {
//打印100以内的数
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
当从Runnable导出一个类时,必须具有run()方法,但是这个方法并无特殊之处————它不会产生任何内在的县城能力,要实现线程行为,你必须显式地将一个任务附着到线程上。
将Runnable类提交给Thread的构造器,通过Thrad.start()启动线程,调用之后主线程立即返回执行下面的打印逻辑,启动的新线程再执行它的run()方法。
public class TestThread {
public static void main(String[] args) {
Thread t = new Thread(new LiftOff());
t.start();
System.out.println("waiting for thread !");
}}
通过集成Thread类来实现线程:
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
super(Integer.toString(++threadCount)); //通过构造器为线程赋名,改名字可以通过Thread.getName()获得
start();
}
@Override
public String toString() {
return "#" + getName() + "(" + countDown + ") .";
}
@Override
public void run() {
while (true){
System.out.println(this);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SimpleThread();
}
}
}
通过实现Callable接口实现线程:
如果希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。
public 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 is " + id ;
}
}
测试主类:
public class TestCallThread {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(executorService.submit(new TaskWithResult(i)));
}
for (Future<String> fs : results) {
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
System.out.println(e);
return;
} catch (ExecutionException e) {
e.printStackTrace();
return;
}finally {
executorService.shutdown();
}
}
}
}
ExecutorService.add()方法添加一个任务之后,会返回一个Future,通过Future的Future.isDone()可以查询任务是否已经完成,如果完成了可以调用Future.get(),此时get()方法会阻塞,直至结果准备就绪。
线程池的使用
为什么使用线程池:
1、每次new Thread() 新建对象,性能差;
2、线程缺乏统一的管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM(内存溢出);
3、缺少更多的功能,如更多执行,定期执行,线程中断。
线程池的好处:
1、重用 存在的线程,减少对象的创建、消亡的开销,性能佳;
2、可有效的控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞;
3、提供定时执行,定期执行,单线程,并发控制等高级功能。
在实际多线程开发中,使用线程池管理线程是优选方法。
线程池的类:
ThreadPoolExecutor
三个重要参数:
corePoolSize:核心线程数量; 线程数少于该值,创建新线程,即使存在空闲线程。
maximumPoolSize:线程最大线程数量;线程数少于该值,且大于corePoolSize,只有当阻塞队列满的时候会创建线程。
workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。当线程达到最大线程数量时,新提交的任务会添加到阻塞队列里,当阻塞队列满的时候,会使用下文的拒绝策略。
keepAliveTime:线程没有任务执行时最多保持多久时间终止。当线程数量大于核心线程数的时候,如果一个线程超过这个时间,没有任务提交,线程即销毁。
unit:keepAliveTime的时间单位。
ThreadFactory:线程工厂,用来创建线程,默认会使用默认的线程工厂来创建线程,使用默认的线程工厂创建线程的时候,线程具有相同的优先级,非守护的线程,具有线程名称的线程。
rejecthandler:当拒绝处理任务时的策略。
a、直接抛出异常(默认策略)(AbortPolicy)
b、用调用者的线程处理任务 (CallerRunsPolicy)
c、丢弃队列中最靠前的任务。(DiscardOldestPolicy)
d、直接丢弃任务。(DiscardPolicy)
如果想使cpu有一个合理的利用:建议设置较大的阻塞队列大小,较小的线程池容量。
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new LiftOff());
executorService.shutdown(); //使线程池继续执行已经提交的任务,但是不再接受新任务
}
}
几种不同的Executor:
- FixedThreadPool:一次性预先的执行代价高昂的线程分配,因而也就限制了线程的数量了。
- 超出的数量会在队列中等待
- CachedThraedPool: 这种执行器在执行过程中通常会创建于所需数量相同的线程,然后在他回收旧线程时停止创建新线程,因此他是合理的Executor的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool;
- Executors.newCachedThreadPool()–>可缓存的线程池
- SingleThreadPool:线程数量为1的FixedThradPool,这种线程池,如果提交了多个任务,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程每个任务都是按照他们被提交的顺序,并且是在下一个任务开始之前完成的。SingleThreadExecutor会序列化所有提交给他的任务,并会维护它自己的(隐藏)的悬挂任务队列。
- ScheduledThreadPool:也是定长的线程池,支持定时,周期性的任务执行
用法:
//延迟3秒执行public static void main(String[] args) {
//定义一个实例
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello!");
}
},3,TimeUnit.SECONDS);
}
//延迟1秒,每隔3秒执行一次
public static void main(String[] args) {
//定义一个实例
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("hello!");
}
},1,3,TimeUnit.SECONDS);
}
线程池的几种状态:
1、Running:接受新提交的任务,并且也能处理阻塞队列当中的任务;
2、ShutDown:不能再接受新提交的任务,但能提交阻塞队列保存的任务;
3、Stop:不再接受新提交的任务,也不处理阻塞队列中的任务;
4、Tidying:所有的线程终止,线程池中的工作线程数量为0;
5、Terminated:
线程池重要方法:
1、execute():提交任务,交给线程池执行。
2、submit():提交任务,能够返回执行结果,execute+Future。
3、shutDown():关闭线程,等待任务都执行完。
4、shutDownNow():关闭线程,不等待任务执行完,立即关闭。
以下几个方法可以对线程的实例进行监控:
5、getTaskCount():线程池已执行和未执行的任务总数
6、getCompleteTaskCount():已完成的任务数量
7、getPoolSize():线程池当前的线程数量
8、getActiveCount():当前线程池中正在执行任务的线程数量
线程池的合理配置:
1、CPU密集型任务:需要尽量压榨CPU,参考值可以设为N(CPU + 1);
2、IO密集型任务,参考值可以设置为2*N(CPU);
线程方法
休眠 Thread.sleep()
该方法使线程中止执行给定的时间。
设置优先级 Thread.setPriority()
线程的优先级将该线程的重要性传递给了调度器。尽管cpu处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级最高的线程先执行。
public class SimplePriorities implements Runnable {
private int countDown = 5;
private volatile double d ;
private int priority;
public SimplePriorities(int priority) {
this.priority = priority;
}
@Override
public String toString() {
return Thread.currentThread() + ": " + countDown;
}
@Override
public void run() {
Thread.currentThread().setPriority(priority);
while (true){
for (int i = 0; i < 100000; i++) {
d += (Math.PI + Math.E) / (double)i;
if(i % 1000 == 0){
Thread.yield();
}
System.out.println(this);
if(--countDown == 0) return;
}
}
}
}
由于考虑到线程级别与操作系统兼容映射的问题,建议调整优先级的时候,只使用MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY三种级别。
让步 Thread.yield()
该方法使建议具有相同优先级的其他线程可以运行,但是建议不一定被采用,继续执行的可能还是这个线程。
后台线程(守护线程)
后台线程(守护线程)是指在程序运行的时候在后台提供一种通用的服务的线程,并且这种线程并不属于程序中不可或缺的部分。
特点:非守护线程全部结束时,程序终止,同时会杀死进程中所有的后台线程。
必须在启动线程之前调用 Thread.setDaemon()方法,才能把它设置为守护线程。
isDaemon()可以用来判断一个线程是否是守护线程,如果是一个守护线程,那么他创建的所有线程都是守护线程。
当非守护线程结束之后,会立即关闭正在执行的守护线程,从而不会按照既定的逻辑再次往下走,比如try…catch(){}finally{} fianlly中的语句也不会再终止时执行。