什么是多线程
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread)。简单来说,多线程就是在同一时间同时处理多个事情,但是在单核处理器上并不能真正实现多线程“同时”处理事情,计算机的运行速度很快,上下文切换的很快人根本感知不到切换,导致人出现一种假象,程序在同时进行。
为什么要使用多线程?
资源利用率: 在某些情况下,程序必须等待外部的操作执行完成,比如等待用户的输入,如果不使用多线程,用户输入一直没有操作,那么程序就会一直停在用户输入的那个地方
便利性: 通常来说,在计算多个任务时使用多个线程,在必要的时候相互通信,更容易实现
发挥多核处理器性能: 现在的处理器都是多核运行,使用多线程可以充分发挥处理器性能
怎么使用多线程?
1、继承Thread类,重写run方法
public class MyThread extends Thread{
public void run(){
for(int i = 0;i<100;i++){
System.out.println(“”)
}
}
}
public class ThreadDemo(){
public static void main(String[] args){
MyThread mt1 = new MyThread();
MyThread mt2 - new MyThread();
mt1.setName(“线程1”);
mt2.setName(“线程2”);
mt1.start();
mt2.start();
}
}
}
2、实现Runnable接口,重写run方法
步骤:
1、创建MyRunnable类实现runnable接口
2、重写run方法
3、创建MyRunnable类的对象
4、创建Thread类的对象,并把MyRunnable类对象作为参数传递
public MyRunnable implements Runnable{
public void run(){
for(int i = 0;i<100;i++){
System.out.println(i);
}
}
}
public class MyRunnableDemo{
public static void main(String[] args){
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my,”线程1”);
Thread t2 = new Thread(my,”线程2”);
t1.start();
t2.start();
}
}
}
3、实现callable接口,重写call方法,可提供返回值
class C implements Callable<String> {
@Override
public String call() throws Exception {
log.info("我是实现Callable的任务");
return "success";
}
}
//测试类
public class CallableDemo{
public static void main(String[] args) throws Exception{
FutureTask<String> target = new FutureTask<>(new C());
new Thread(target).start();
log.info(target.get());
}
}
线程的五种状态
java中的线程的生命周期大体可分为5种状态。
- 新建(NEW):新创建了一个线程对象。
- 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
- 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
- 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lockpool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(longms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
- 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生
一般在使用多线程的时候,一般使用线程池来创建线程
为什么要使用线程池?
提高程序的执行效率
如果程序中有大量短时间任务的线程任务,由于创建和销毁线程需要和底层操作系统交互,大量时间都耗费在创建和销毁线程上,因而比较浪费时间,系统效率很低
而线程池里的每一个线程任务结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率
控制线程的数量,防止程序崩溃
如果不加限制地创建和启动线程很容易造成程序崩溃,比如高并发1000W个线程,JVM就需要有保存1000W个线程的空间,这样极易出现内存溢出
线程池中线程数量是一定的,可以有效避免出现内存溢出
线程池的注意事项
死锁 任何多线程应用程序都有死锁风险。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象
Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。
资源不足 如果线程池中的线程数目非常多,这些线程会消耗包括内存和其他系统资源在内的大量资源,从而严重影响系统性能。
并发错误 线程池和其它排队机制依靠使用 wait() 和 notify()
方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心。而最好使用现有的、已经知道能工作的实现,例如util.concurrent 包。
线程过载
当工作队列中有大量排队等候执行的任务时,这些任务本身可能会消耗太多的系统资源而引起系统资源缺乏。比如:向服务器发送大量请求。因此,服务器应根据系统的承载能力,限制客户并发连接的数目。当客户并发连接的数目超过了限制值,服务器可以拒绝连接请求,并友好地告知客户:服务器正忙,请稍后再试。
创建线程池的两种方式ThreadPoolExecutor和Executors
ThreadPoolExecutor
可以灵活的自定义的创建线程池, 实际中很少使用
Executors
可以创建4种线程池,这四种线程池基本上已经包含了所有需求,将来根据业务特点选用就好 使用非常简单 实际中很常用
newFixedThreadPool(int corePoolSize)
创建一个线程数固定(corePoolSize=maximumPoolSize)的线程池。核心线程会一直运行,到了线程池最大容量后,如果有任务完成让出占用线程,此线程就会一直处于等待状态,不会消亡。如果一个核心线程出现异常,在提交任务时会新创建一个线程
无界队列 LinkedBlockingQueue
(当大量任务超过线程池最大容量需要处理时,队列无线增大,会使服务器资源迅速耗尽)
newSingleThreadExecutor
创建一个线程数固定(corePoolSize=maximumPoolSize==1)的线程池 核心线程会一直运行
无界队列LinkedBlockingQueue
所有task都是串行执行的(被提交任务按优先级依次执行,且同一时刻只有一个任务在执行)
newCachedThreadPool
corePoolSize=0(线程数量不确定,只要有空闲线程空闲时间超过keepAliveTime,就会终止,执行新任务,先使用空闲线程,若不够,再新建线程。)
maximumPoolSize=Integer.MAX_VALUE(线程池没有最大线程数量限制,所以当大量线程蜂拥而至,会造成资源耗尽)
队列:SynchronousQueue
newScheduledThreadPool(int corePoolSize)
长度为输入参数自定义支持定时周期任务执行,可根据时间需要在指定时间对线程进行调度
线程池的状态
RUNNING:接受新任务并处理排队的任务
SHUTDOWN:不接受新任务,但处理排队的任务
STOP:不接受新任务,不处理排队的任务,中断正在进行的任务
TIDYING:所有任务都已终止,WorkerCount为零,线程转换为状态整理将运行终止的()hook方法
TERMINATED:已终止()已完成
线程池的关闭
线程池的关闭有两种方法:
shutdown:提供一种有序的关机,会等待当前缓存队列任务全部执行完成才会关闭,但不会再接收新的任务(相对较优雅)。
shutdownNow:会立即关闭线程池,会打断正在执行的任务并且会清空缓存队列中的任务,返回的是尚未执行的任务。
多线程带来的弊端
线程的安全性问题:在没有同步的情况下,多线程的操作顺序是不可预测的,甚至产生奇怪的结果。
死锁:程序不出错,也不终止,无限期等待
资源不足,多线程如果使用不当会直接耗尽服务器性能,同时创建大量的线程
线程安全的问题尤为重要,解决安全问题首先想到的是加锁。