进程(process)是正在运行的一个程序,是动态的,进程有它自己的产生,存在和消亡过程。
线程(thread)是进程中的一个执行流程,一个进程可以包含多个线程
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
方式一: 继承Thread类
java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务。
1、继承Thread创建线程类
思路:3步骤
- 创建一个继承于Thread类的子类
- 重写Thread类的run(),run()方法称为线程执行体。
- 创建Thread类的子类的对象,通过此对象调用start(),线程开始执行; Java虚拟机调用此线程的run方法。
1、把创建的子线程是要做啥的【线程执行体】,给写到run()方法。
public class MyThread extends Thread{
@Override
public void run() {
//这个线程任务是用来打印1-10之间的偶数
for (int i = 1; i <=10; i++) {
if(i%2==0){
//Thread.currentThread()获得当前线程对象
System.out.println("线程"+Thread.currentThread().getName()+"在处理任务,打印的数是:"+i);
}
}
}
}
2、main方法里创建线程对象并让子线程运行起来
//main方法的方法体代表主线程的线程执行体
public static void main(String[] args) {
//创建Thread类的子类对象
MyThread t1 = new MyThread();
//启动t1线程
t1.start();
}
默认情况下,主线程的名字为main,用户启动的多个线程名字依次为Thread-0,Thread-1,Thread-2,,
2、start()启动线程
调用线程的start()方法之后,该线程立即进入就绪状态(等待执行)。但线程并未真正进入运行状态
启动当前线程(什么是当前线程?谁调这个方法谁就是当前线程),并自动调用当前线程的run方法。
1、那么为啥不直接调用子类对象的run方法来打印输出呢,而要通过调start来调run方法。
答案肯定不可以。
失去了多线程的意义,不会开启新的线程,只是单纯的对象调用方法.
start()
方法内部调用了一个private native void start0()
方法,native
修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。private native void start0();
2、线程启动后,再次用该线程的调start()方法,可以再开启一个线程吗,不可以这么做
//创建Thread类的子类对象 MyThread t1 = new MyThread(); //启动t1线程 t1.start(); t1.start();
如果再次调⽤ start() ⽅法会抛出 IllegalThreadStateException 异常
对一个已经 死亡的线程调用start()方法使他重新启动,会引发 IllegalThreadStateException 异常,表明处于死亡状态的线程无法再次运行。
3、线程状态转换
>新建(NEW):当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
>就绪(Runnable):处于新建状态的线程被start()后,将进入可运行线程池等待CPU时间片,此时它己具备了运行的条件,只是没分配到CPU资源
>运行 (Running):当就绪的线程被调度并获得CPU资源时,便进入运行状态,执行run()方法定义线程的操作和功能
>阻塞 (Blocked):在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
>死亡(TERMINATED):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
1、线程的最终状态只有一个就是死亡。
1、run()或者call()执行完成,线程正常结束 2、线程抛出一个未捕获的Exception或Error 3、直接调用线程的stop方法结束该线程(可能会死锁)
2、在阻塞状态下,即使cpu想执行这个线程也是执行不了的。
1.等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是Object类的方法。 2.同步阻塞:运行的线程在获取对象的的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中 3.其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置于阻塞状态。当sleep状态超时、join等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态、sleep是Thread类的方法
3、阻塞原因:CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,java就会收回CPU的执行权,从而合理应用CPU的资源
4、多线程常用方法
- start():启动当前线程,java虚拟机会调用当前线程的run()
- run():将创建线程要执行的操作声明在此方法
- currentThread():静态方法,返回执行当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():直译为放弃。在这里表示当前线程释放cpu执行权,转入就绪状态
- join() :在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。内部调⽤的是 Object类的wait⽅法实现的;
- stop():已过时,执行此方法,强制结束当前线程
- sleep( Long milltime):静态方法。让当前线程睡眠指定的 milltime毫秒,在指定的 milltime毫秒时间内,当前线程是阻塞状态。
- isAlive():判断当前线程是否存活
静态方法,Thread.yield,线程让位。
yield give way屈服,让步
Thread.yield,让当前线程暂停,回到就绪状态。
public static native void yield();
就算当前线程调⽤了yield() ⽅法,线程调度器再次调度的时候,也还有可能继续运⾏这个线程的
public void run(){ for(int i=1;i<=10000;i++){ //每100的倍数让位一下 if(i%100==0){ Thread.yield();//当前线程暂停一下,让给主线程 } System.out.println(Thread.currentThread().getName()+"----->"+i); } }
yield()方法只会让给优先级相同,或优先级更高的线程执行机会
join,线程合并
join 连接;合并 join方法可以使得线程之间的并行执行变为串行执行
try{ t1.join(); //t1线程合并到当前线程,当前线程受阻塞,t1线程执行直到结束。 }catch(InterruptedException e){ e.printStackTrace(); }
join(long millis):如果在millis毫秒内,被join的线程还没有执行结束,则不再等待
sleep,Thread.sleep(指定毫秒数)
public static native void sleep(long millis) throws InterruptedException;
sleep用于当前线程休眠,休眠结束后退出阻塞。
线程调用sleep()休眠时间段内,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行
5、线程优先级
同优先级就乖乖排队,高优先级的会使用优先调度的抢占式策略,具有更高的概率获得cpu执行权。
1.线程的最高优先级:10 ;最低优先级:1;默认优先级:5
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;
2.涉及到的方法:
getPriority(); //返回线程的优先级 setPriority(int newPriority); //改变线程的优先级
3.每个线程默认的优先级都与创建它的父线程的优先级相同,默认情况下,main线程具有普通优先级。
public static void main(String[] args) { System.out.println(Thread.currentThread().getPriority());//5 }
Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的 线程调度算法决定的。
6、线程组
线程不能独立于线程组存在,每个Thread必然存在于⼀个ThreadGroup中,线程组对线程进⾏批量控制。
如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组。
获取当前的线程组名字
public static void main(String[] args) { System.out.println(Thread.currentThread().getThreadGroup().getName());//main }
方式二:实现Runnable接口
Runnable接口好在哪
Runnable接口源码:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
那么,实现Runnable接口的方式创建线程好在哪了?
- 因为java单继承的限制,继承了Thread类,就没有办法继承别的类了。
- 多个线程有共享数据的情况
Runnable接口[共享数据]
启动多线程的方式只有一个,就是调用Thread类的start方法。
Runnable对象作为Thread对象的target,多个线程可以共享一个target。所以多个线程可以共享一个线程类的实例变量。
public static void main(String[] args) {
//将Runnable实现类对象设置给thread的对象的target属性
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <= 20; i++) {
if (i % 2 == 0) {
//这个线程是用来打印0-20之间的偶数的
System.out.println(Thread.currentThread().getName()+"——>"+i);
}
}
}
});
thread.setName("实现Runnable接口的线程");
//在start方法内,如果target不为空,会自动调用target的run方法
thread.start();
}
结果不言而喻
Runnable接口简写
//实现Runnable接口的线程
new Thread(() -> {
//这个线程业务代码
System.out.println();
},"Runnable-Thread-1").start();
方式三:实现Callable接口
Callable接口
Callable接口源码:
1.实现 Callable<V>接口,重写call方法,把该线程要做的事写在call方法。
2.这个call()方法throws Exception并且 Callable<V>使用到了泛型。
3.call()函数返回的类型就是传递进来的V类型。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable接口使用
那怎么启动这个实现Callable接口的线程呢?
- 将Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象。
- 再把FutureTask对象作为参数传递到Thread类构造器中。
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Callable泛型是返回值类型
FutureTask futureTask = new FutureTask((Callable<Integer>) () -> {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
//遍历输出100以内偶数
System.out.println(i);
sum += i;
}
}
return sum;
});
Thread thread = new Thread(futureTask);
thread.setName("实现callable接口线程");
thread.start();
//get()返回值即为FutureTask构造器参数Callable接口的实现类重写的call方法的返回值
Object sum = futureTask.get();
System.out.println("100以内的偶数和:"+sum);
}
FutureTask的方法
FutureTask的get()方法是依靠其内部类java.util.concurrent.FutureTask.Sync<V>类来实现阻塞。
当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;
当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或抛出异常
FutureTask是什么?得从Future接口说起!
Future接口 提供了三种功能:
- 判断任务是否完成
- 能够中断任务
- 能够获取任务执行结果
Future接口
Future接口和FutureTask的关系是什么?
1.FutureTask实现了RunnableFuture接口
//FutureTask实现了RunnableFuture接口
public class FutureTask<V> implements RunnableFuture<V> {}
2.RunnableFuture继承了Runnable接口和Future接口,java接口支持多继承,所以FutureTask既可以被当做Runnable来执行
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
3.事实上,FutureTask是Future接口的一个唯一实现类。
---------------------
这种实现Callable接口创建线程的方式有什么好处?
有返回值,可以抛出异常,支持泛型。
方式四:线程池方式创建线程
提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池中。
线程池的好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(设置线程池大小,存活时间)
1、Executors快速创建线程池
该方式虽然便捷,但是会有OOM(线程耗尽)隐患
Executors
创建线程池便捷方法列表:方法名功能newFixedThreadPool(int nThreads)创建固定大小的线程池newSingleThreadExecutor()创建只有一个线程的线程池newCachedThreadPool()创建一个可以扩容的线程池
newFixedThreadPool(int nThreads)
public static void main(String[] args) { //5个线程去处理10个顾客的请求 //提供指定数量的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(5); try { for (int i = 0; i < 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 正在处理业务"); }); } } catch (Exception e) { e.printStackTrace(); } finally { //把线程放回线程池 threadPool.shutdown(); } }
控制台打印如下:
newSingleThreadExecutor()
public static void main(String[] args) { //1个线程去处理10个顾客的请求 //创建只有一个线程的线程池 ExecutorService threadPool = Executors.newSingleThreadExecutor(); try { for (int i = 0; i < 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 正在处理业务"); }); } } catch (Exception e) { e.printStackTrace(); } finally { //把线程放回线程池 threadPool.shutdown(); } }
控制台打印如下:
newCachedThreadPool()
public static void main(String[] args) { //可扩容线程去处理10个顾客的请求 //创建一个可以扩容给的线程池 ExecutorService threadPool = Executors.newCachedThreadPool(); try { for (int i = 0; i < 10; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 正在处理业务"); }); } } catch (Exception e) { e.printStackTrace(); } finally { //把线程放回线程池 threadPool.shutdown(); } }
控制台打印如下:
2、ThreadPoolExecutor7个参数
// Java线程池的完整构造函数 public ThreadPoolExecutor( int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。 int maximumPoolSize, // 线程数的上限 long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长, // 超过这个时间,多余的线程会被回收。 BlockingQueue<Runnable> workQueue, // 任务的排队队列 ThreadFactory threadFactory, // 新线程的产生方式 RejectedExecutionHandler handler) // 拒绝策略
ThreadPoolExecutor()7个参数:
核心线程数量(常驻线程数) | corePoolSize |
最大线程数量 | maximumPoolSize |
线程存活时间(回归到常驻过程的时间) | keepAliveTime、unit |
阻塞队列(常驻线程用完之后,再来请求就会先阻塞) | workQueue |
线程工厂 创建线程 | threadFactory |
拒绝策略 (最大线程数量) | handler |
注:存活时间占了2个参数,阻塞队列是多个线程共享的队列,队列里放满了或者取空就会阻塞
为什么用阻塞队列,因为用了之后就不用关心什么时候阻塞线程,什么时候需要唤醒线程,这一切BlockingQueue都给你一手包办了
在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
3、线程池工作流程
分析这张图:
1. 执行线程池的execute()这行代码时,线程才创建
2. 从图中看得出常驻线程数 2 最大线程数5 约定阻塞队列3
3. 第一个、第二个请求用常驻线程
第3个、第4个、第5个用阻塞队列
第6个、第7个、第8个创建新的线程(3/4/5继续等待,第6个优先处理)
4. 第9个请求,会执行拒绝策略
4、线程池任务
可以向线程池提交的任务有两种:Runnable
和Callable
,二者的区别如下:
Callable
是JDK1.5时加入的接口,作为Runnable
的一种补充,允许有返回值,允许抛出异常。
三种提交任务的方式:
提交方式 | 是否关心返回结果 |
| 是 |
| 否 |
| 否,虽然返回Future,但是其get()方法总是返回null |
5、线程拒绝策略
线程池里面的常驻线程满了,队列也满了、也达到了最大的线程数,这个时候再来请求使用拒绝策略
拒绝策略 | 拒绝行为 |
AbortPolicy | 抛出RejectedExecutionException |
DiscardPolicy | 什么也不做,直接忽略,如果允许任务丢失,这是最好的一种策略 |
DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
CallerRunsPolicy | 直接由提交任务者执行这个任务 |
6、ThreadPoolExecutor构造方法创建线程池
实际开发,不允许使用Executors创建线程池。原因如下:
这三货允许的请求队列长度为Integer.Max_Value。要不就是允许创建的线程数量为Integer.Max_Value。
自定义线程池,使用ThreadPoolExecutor,这样是为了更加明确线程池的运行规则,规避资源耗尽的风险
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
测试自定义线程池
1
2
3