1 概述
程序当中一条独立的执行线索。Java中的主线程是一个JVM进程中的主方法。-
程序:指保存在物理介质中的代码片段。
-
进程:一旦程序运行起来,就变成操作系统当中的一个进程。操作系统中的一个任务就是一个进程。
比如图中操作系统的任务管理器,每一个应用和后台进程都是一个进程。 -
一个CPU,无论几个核,一个时间点只能处理一件事。 但是为了看起来像是“并发”,操作系统分时地将CPU的时间化成长短基本相同的“时间片”。操作系统调度各个任务依次轮流使用这些时间片,造成一种“宏观并行,微观串行”的现象。
上面一段说的“时间片”适用于现在市面上大多操作系统(多任务分时操作系统),CPU用来处理所有程序的操作,操作系统在其中扮演调度这些程序的角色。 -
进程的上下文切换: 切换程序时,CPU切换到另外一个进程,并保存切出进程的状态(持久保存在CPU的进程控制块/切换帧(PCB),直到下次被使用),并且恢复切进来进程的状态。切进来的进程由就绪态转为运行态,切出的进程由运行态转为就绪态(或者删除、挂起态,取决于中断事由和执行进度)。
上下文切换这样对于进程的操作较耗费内存资源成本,效率低。如果将进程这一个“任务”内部细化为多个“子任务”,就会减少这样过多的耗费。 -
线程是进程内部的“子任务”,程序当中一条独立的执行线索。 如很多下载器软件的多线程下载功能,下载文件时将一个文件分为好几段到多个线程上同时下载,达到提速的目的;word编辑文字时还可以统计字数。也就是一个进程内部,同时做很多事。
-
进程是资源分配的基本单元,线程作为执行的基本单元,同一进程多个线程之间共享资源。 好比进程在外与其他进程争抢到了一个时间片资源,其子任务线程再来争抢时间片来执行。进程和线程都有生命周期。
-
把上面的描述类比到Java上:Java程序运行在JVM上,每一个JVM都是一个进程,所有资源分配依据每一个JVM,而资源分配到了某一个JVM,真正执行是依据线程,此时这个JVM内的线程之间可以共享获得的资源,并发执行。(Java是第一款在语言级别直接支持多线程的语言)
-
线程的线程体(继承Thread类或实现Runnable接口的类的run()方法体),是线程核心的逻辑,是当线程抢到时间片之后要执行的操作。
2 生命周期(五状态图)
- start(): 一个新创建的线程不会自动开始运行,要执行线程,必须调用线程的start()。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。
不要直接使用对象引用调用类中的run(),这样的意思是让主线程来调用run()方法,将军是用来指挥局面的,而不是用来亲力亲为冲锋陷阵的。主线程调用某个类中的run(),相当于把run()放在主线程(主方法)中执行,这样就又变成一个单线程程序了。
start()方法后,线程就处于就绪状态。此时处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争时间片,只有获得时间片(CPU时间)才可以运行线程(执行run())。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由JVM的线程调度程序来调度的。
从阻塞态到运行态是不可能的。 阻塞此进程运行的事件解除后即满足了就绪的条件,但系统中处于就绪态的进程可能有多个,因此只能由系统根据某种调度算法从就绪队列中选择一个进程占用CPU。
从就绪态到阻塞态势也是不可能的。 一个就绪程序不可能会做出任何的事情来产生阻塞,只有运行的进程才会被阻塞。
从阻塞态到就绪态是可能的。 使用interrupt()方法。
- 运行态转换到阻塞态,可以使用了join()和sleep()。
-
方法只要涉及到由运行态转换为阻塞态,必需要异常处理。
-
如果一个线程无法在一个时间片内运行完成,它将会保存现场转为就绪,继续与其它线程争抢时间片;如果执行得完就消亡这个线程。时间片CPU时间任何时间耗尽都有可能。
-
先到就绪的线程不一定先执行,要与其它线程在下次时间片耗尽后争抢时间片。首先被start()的那个类对象大概率先被执行的原因是主线程一定先执行,第一个时间片耗尽时JVM中可能只加载进来第一个类对象的线程,此时只有主线程和这个线程争夺下个时间片。
-
程序出异常时,虚拟机杀死的只是出异常的线程,被杀死的线程中,后面的语句都无法执行。
-
线程阻塞的原因:
-
线程通过调用sleep方法进入睡眠状态;
-
线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
-
线程试图得到一个锁,而该锁正被其他线程持有(现在阻塞的线程在锁池中);
-
线程在等待某个触发条件;
-
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
3 如何定义一个线程
-
extends Thread
定义线程的常用方法,继承Thead类并重写其中void run()方法。new出类对象后使用类对象引用调用start()即可运行线程。 -
implements Runnable
如果类需要继承其他更重要的父类,此时定义线程只能实现Runnable接口。需要实现其中的void run()方法。这个需要new出Thread类对象并在参数括号中传入new的类对象引用之后,使用Thread类的类对象引用调用start()即可运行线程。
public class TestThread{
public static void main(String[] args){
//直接将这个对象映射为对于操作系统来说中的一个线程
ThreadOne t1 = new ThreadOne();
t1.start();
ThreadTwo t2 = new ThreadTwo();
//通过传入实现了Runnable接口的类,将对象映射为一个线程
Thread t = new Thread(t2);
t.start();
while(true){
System.out.println("姚明");
}
}
}
class ThreadTwo implements Runnable{
@Override
public void run(){
while(true){
System.out.println("艾佛森");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
while(true){
System.out.println("科比");
}
}
}
-
implements Callable< E >
在concurrent包下,还可以实现Callable接口的call方法来实现一个线程,但一般需要配合ExecutorService类(线程池)来使用。此时的call方法的返回值返回类型为E,且可以throws异常。- 线程池
假如一个线程的总共执行时间为 T
T = t1 + t2 + t3
t1: 创建一个线程所消耗的时间(把java中的一个对象映射成一个线程所需要的时间)
t2: 线程核心逻辑执行的时间,也就是执行run()的时间
t3: 销毁一个线程所消耗的时间
而如果run()当中的代码非常简单,则t2所占T的比例就会很小,就好比做一顿饭(t1),吃饭(t2),然后洗碗(t3)。如果吃饭时间太短,而做饭t1、t3时间太长,就有点得不偿失,不如去饭馆(资源池),有做好的饭(提前预留活跃的资源,不需要t1),也不需要洗碗(t3)。
线程池是一种标准的资源池,所谓的资源池都是为了提前预留活跃资源,在用户出现的第一时间就能使用而不需要创建和销毁。
- 如何实现线程池:
- ExecutorService es = Executors.newFixedThreadPool(int n);
资源可重用的线程池,意思是线程池中预先定义了固定的n个线程,线程类submit()了之后可以直接使用这些线程,但如果submit()进来的线程类超过了n这个数值,之后submit()进来的新线程类将会等待其他线程类线程使用完线程池中线程资源,将资源归还之后再执行。n越大,这个建立线程池时的t1时长越长。 - ExecutorService es1 = Executors.newCachedThreadPool();
带有缓存机制的线程池,线程池中的线程可按需无限增加,它比较适合处理执行时间比较小的任务,应对偶然的高并发。当其中线程空闲时间超过60s,会自动被杀死。 - ExecutorService es2 = Executors.newSingleThreadExecutor();
单一实例的线程池,只会创建一条工作线程处理任务,同一时间只能允许一个线程类使用活跃资源。
- ExecutorService es = Executors.newFixedThreadPool(int n);
import java.util.concurrent.*;//并发包
public class TestThreadPool{
public static void main(String[] args)throws Exception{
//执行器服务(不需要自己炒菜,直接告诉服务员要点菜就可以了)
ExecutorService es = Executors.newFixedThreadPool(2); //可重用的
//Executors.newCachedThreadPool(); //缓存机制的
//Executors.newSingleThreadExecutor(); //单一实例的
ThreadOne t1 = new ThreadOne();
es.submit(t1);//点菜
ThreadTwo t2 = new ThreadTwo();
es.submit(t2);//点菜
ThreadThree t3 = new ThreadThree();
Future<String> f = es.submit(t3);
//System.out.println(f.get());//阻塞主线程
es.shutdownNow();//shutdown();
}
}
class ThreadThree implements Callable<String>{
@Override
public String call()throws Exception{
for(int i = 0; i < 10000; i++){
System.out.println("我是创建线程的第三种方式");
}
return "ThreadThree";
}
}
class ThreadTwo implements Runnable{
@Override
public void run(){
for(int i = 0; i < 10000; i++){
System.out.println("我是创建线程的第二种方式");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
for(int i = 0; i < 10000; i++){
System.out.println("我是创建线程的第一种方式");
}
}
}
-
使用线程池时,执行器服务对象.submit(new 继承了线程的线程类或者实现runnable的类))而不是在自己再去接(继承了线程的线程类或者实现runnable的类对象.start())。
-
需要注意的是,submit()递交上去未必一定立马执行,若此时是fixed线程池而现在池中的活跃线程资源都被占用,submit()递交上去的线程类会排队等待执行。
-
此时若不关闭ExecutorService的es方法,它将一直执行。关闭时有两种方法,shutDown()和shutDownNow()。shutDown()的意思是停止submit()新的线程类到线程池中,shutDownNow(),更绝一些,阻止等待的线程类映射为线程资源启动并试图停止当前正在执行的线程资源,shutDown()中也包含shutDownNow()方法。
-
ExecutorService对象结束的状态是线程资源没有执行,也没有线程类在等待执行,且此时无法提交新任务。应关闭不再使用的ExecutorService的对象以允许JVM回收其资源。
-
Future< E >是一个接口,未来模式(23种设计模式之外的一种模式,使用Future模式,获取数据的时候无法立即得到需要的数据。而是先拿到一个契约,你可以再将来需要的时候再用这个契约去获取需要的数据),E为需要接收的Callable实现类的传入的泛型E。主要作用有判断任务是否完成、中断任务、能够获取任务执行结果。上面的代码中使用的是最后一个作用,用来接收call方法的返回值。但是返回值在接收时,因为返回值需要线程体执行完后才可以获取到,所以.get()方法需要一直使所在线程阻塞直到任务执行完毕才返回call方法的返回值,此时需要对get()进行异常处理。
4 控制线程运行的方法
线程章节所有的静态方法,不要关注是谁调用方法。这个调用出现在谁的线程体(方法体)内,这就代表操作谁。
线程章节所有的涉及主动进入阻塞状态的方法(sleep()、join()),都必须进行异常处理。但要注意在run()中如果需要处理异常,必须要使用try-catch。因为重写的run()方法,父类本身就没有抛出异常,子类也不能抛出异常。
-
start()
见2 生命周期(五状态图)中的start()介绍。 -
setPriority(int)
设置线程优先级别,可选范围1-10,默认为5。优先级越高代表抢到时间片的概率越高。是有概率,而不代表一定。所以我们不可能依靠概率完成任何需求,这样的需求不靠谱。 -
join()
一个线程邀请另一个线程加入并且优先执行,在被邀请的线程执行结束之前邀请别人的线程一直处于阻塞状态,不再继续执行。调用这个方法的类对象将作为被邀请优先执行的线程,join()在邀请执行的线程内被调用。这涉及到两个进程,这个方法与线程同步有关。 由运行态到阻塞态,需要进行异常处理。
public class TestJoin{
public static void main(String[] args)throws Exception{
ThreadOne to = new ThreadOne();
to.start();
//主线程邀请to线程优先执行,to线程执行结束之前主线程一直处于阻塞态,不再继续执行
to.join();
for(int i = 0; i < 2000; i++){
System.out.println("岂因祸福避趋之");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
for(int i = 0; i < 2000; i++){
System.out.println("苟利国家生死以");
}
}
}
- setDaemon(true)
设置线程成为守护线程,守护线程是给其他核心线程提供服务的线程。当程序当中只有守护线程时,守护线程会自行结束。(Java当中最著名的守护线程:gc)
守护线程可以晚于其他线程启动。
守护线程为防止过早消亡,需无限循环或存活时间尽量长。
守护线程必须的设置(这个方法)必须早于启动。
守护线程优先级必须低(需要设置setPriority()),防止与其他线程抢时间片。
- interrupt()
某线程(在这个线程(方法)中定义)中断另一线程(调用interrup的这个类对象)的阻塞状态,使后者回到就绪态。注意,线程不能自己中断自己。
public class TestInterrupt{
public static void main(String[] args)throws Exception{
ThreadOne to = new ThreadOne();
//开局就阻塞
to.start();
Thread.sleep(5000);
//主线程在睡了5秒之后,中断to线程阻塞状态
to.interrupt();
}
}
class ThreadOne extends Thread{
@Override
public void run(){
try{
sleep(99999999999999999L);
}catch(Exception e){
e.printStackTrace();
}
System.out.println("闹钟响了,起来关。");
}
}
所有的非静态方法,如TestInterrupt类的对象在主线程调用了start()、interrupt()方法,意思是主线程中定义了这个类的对象,对象调用了这两个方法,在主线程中用start()使对象的run方法执行(这个方法是一个新的线程(run方法为线程体),与主线程争抢时间片)。调用interrupt()的意思是主线程中用这个方法使这个这个新的线程从阻塞到就绪态。
-
static activeCount()
得到程序当中所有活跃线程的总数。活跃线程 = 就绪 + 运行 + 阻塞
activeCount()的值应至少为1,因为运行的程序至少要有一个主线程。 -
static sleep(long)
让当前线程休眠指定的毫秒数,运行态到阻塞态,需要进行异常处理。 -
static yield()
让当前线程直接放弃时间片直接返回就绪,一定会放弃时间片,让时间片切换得更加频繁,但操作系统不一定会将时间片交给优先级更高或者同级的线程执行。 这个方法有可能出现“自投自抢”,因为让步的线程还有可能被线程调度程序再次选中,再转到就绪状态。 -
static currentThread() ★
得到当前正在处于运行状态的线程对象。- 若出现在主方法当中,是用来得到主线程对象
- 若出现在run()调用的其他方法中,是用于得到线程对象
- 如果直接出现在run()方法内,相当于this,用于调用自己类中的成员
public class TestCurrentThread1{
public static void main(String[] args){
ThreadOne to = new ThreadOne();
to.start();
//获取主线程对象,将主线程优先级设置为10
Thread main = Thread.currentThread();
main.setPriority(10);
while(true){
System.out.println("奥尼尔");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
while(true){
System.out.println("姚明");
}
}
}
import java.util.*;
import java.util.concurrent.*;
class TestCurrentThread2{
public static void main(String[] args)throws Exception{
Student s1 = new Student("s1");
Student s2 = new Student("s2");
s1.start();
s2.start();
/*
//直接使用Map的遍历方法比较快
//如果不加.join()会让主线程第一次时间片时集合遍历就早于这两个Student线程执行完毕了
//所以需要先将两个同学提问的问题解决完毕之后(.join())再遍历问题记录本
s1.join();
s2.join();
Teacher tea = Teacher.getOnly();
for(String s : tea.map.keySet()){
System.out.println(s + ":" + tea.map.get(s));
}
*/
//lambda底层效率低
//所以可以有概率在主线程第一次时间片结束后让两个Student线程先执行完主线程再抢到时间片将集合遍历
Teacher tea = Teacher.getOnly();
tea.map.forEach((x, y) -> System.out.println(x + ":" + y));
}
}
class Teacher{
//防止出现并发修改错误
Map<String, Integer> map = new ConcurrentHashMap<>();
private Teacher(){}
private static Teacher only = new Teacher();
public static Teacher getOnly(){
return only;
}
//老师的回答问题方法
public void hdwt(){
//获取哪个线程调用的这个方法,得到这个线程对象(谁问的问题)
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println("解决" + name +"的问题。");
//记录已经回答这个同学几个问题
if(map.containsKey(name)){
map.put(name, map.get(name) + 1);
}else{
map.put(name, 1);
}
}
}
class Student extends Thread{
String name;
public Student(String name){
setName(name);
}
@Override
public void run(){
//随机模拟这个学生遇到了几个问题
int i = (int)(Math.random() * 5) + 1;
System.out.println(getName() + "决心好好学习,但是遇到了" + i + "个问题~");
for(int j = 0; j < i; j++){
Teacher t = Teacher.getOnly();
t.hdwt();
}
}
}
某一次的执行结果:
- setName()/getName()
当继承Thread类时,这两个方法用于设置和得到线程的名字。对于需要定义名字的类,这两个方法可以省去再定义name属性和其setter/getter,直接调用父类的使用就可以了,线程名可以重复命名。