Java的线程

131 阅读9分钟

Java线程

创建和运行线程

直接使用Thread

Thread t = new Thread(){
    public void run(){
        //要执行的任务
    }
};
//启动线程
t.start();

//指定名称
t.setName("t1");

使用Runnable配合Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread代表线程
  • Runnable可运行的任务(线程要执行的代码)
@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                //要执行的任务
                log.debug("running");
            }
        };
        //创建线程对象
        Thread t = new Thread(r,"t2");
        //开启线程
        t.start();
    }
}

java8以后可以使用lambda精简代码

@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        Runnable r = () -> log.debug("running");
        Thread t = new Thread(r,"t2");
        t.start();
    }
}

FutureTask

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况

@Slf4j(topic = "c.Test2")
public class Test2 {
	public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建FutureTask对象
        //在其中创建Callable对象,对象中传入执行方法,并有返回值
		FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				log.debug("running...");
				Thread.sleep(1000);
				return 100;
			}
		});
        //创建新线程
		Thread t = new Thread(task,"t1");
		//开始执行
        t.start();
		log.debug("返回值为:{}",task.get());
	}
}

Thread与Runnable的关系

  • 方法一是把线程和任务合并在了一起,方法2是把线程和任务分开了
  • 用Runnable更容易与线程池等高级API配合
  • 用Runnable让任务类脱离了Thread继承体系,更灵活

查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写H切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令可以查看所有Java进程
  • jstack 查看某个Java进程(PID)的所有线程状态
  • jconsole 来查看某个Java进程中线程的运行情况(图形界面)

线程运行的原理

栈与栈帧

JVM Stacks(Java虚拟机栈)

JVM中有堆、栈、方法区所组成,其中栈内存是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程的上下文切换

因为以下一些原因大致cpu不再执行当前的线程,转而执行另一个线程的代码

  • 线程的cpu时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法

当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用是记住下一条jvm指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量表、操作数栈、返回地址等
  • Context Switch频繁发生会影响性能

常见方法

方法名静态方法功能说明注意
start()启动一个新线程,在新的线程运行run方法中的代码start方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用多次会抛出异常
run()新线程启动后会调用的方法如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待n毫秒
getId()获取线程长整型的id,id唯一
getName()获取线程名
setName()修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级Java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的几率
getState()获取线程状态Java中线程状态是用6个enum表示,分别为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在sleep,wait,join会导致被打断的线程抛出异常,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时让出cpu的时间片给其他线程
yield()static提示线程调度器让出当前线程对cpu的使用主要是为了测试使用

start与run

直接调用run方法,会认为是在当前线程中调用了Thread类中的一个方法,不会新创建一个线程

调用start方法则会在新创建的线程中执行run方法,不会在调用start方法的线程中执行

sleep与yield

sleep

  1. 调用sleep会让当前线程从Running进入Timed Waiting状态
  2. 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性

yield

  1. 调用yield会让当前线程从Running进入Runnable状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
  2. 具体的实现依赖于操作系统的任务调度器

线程的优先级

  • 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用

join

join方法调用如下


public class Main {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                //要执行的任务
                log.debug("running");
            }
        };
        //创建线程对象
        Thread t = new Thread(r,"t2");
        t.join();
        //开启线程
        t.start();
    }
}

join方法会使调用该方法的线程等待被调用的线程执行完毕后执行,此方法可以控制线程间的执行顺序

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

打断sleep、wait、join的线程

阻塞

打断sleep的线程,会清空打断状态,以sleep为例

public static void main(String[] args) {
	Thread t1 = new Thread(() -> {
		log.debug("sleep---");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	});
	t1.start();
	log.debug("interrupt");
	t1.interrupt();
    log.debug("打断标记{}",t1.isInterrupted());
}

打断正常运行的线程

打断正常运行的线程,不会清空打断状态

@Slf4j(topic = "c.Test1")
public class Test1 {
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			log.debug("sleep---");
			while (true){
				
			}
		});
		t1.start();
		Thread.sleep(1000);
		log.debug("interrupt");
		t1.interrupt();
		log.debug("打断标记{}",t1.isInterrupted());
	}
}

两阶段终止模式

Two Phase Termination

在一个线程T1中如何优雅终止T2?这里的【优雅】指的是给T2一个料理后事的机会

错误思路

  • 使用线程对象的stop()方法停止线程

    stop方法会真正的杀死线程,会释放所持有的锁对象,造成数据不一致

  • 使用System.exit(int)方法停止线程

    目的仅是停止一个线程,但这种做法会让整个程序都停止

正确思路

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
	private Thread monitor;
	//启动监控线程
	public void start() {
		monitor = new Thread(() ->{
			while (true){
				Thread current = Thread.currentThread();
				if (current.isInterrupted()){
					log.debug("料理后事");
					break;
				}
				try {
					Thread.sleep(1000);//情况1
					log.debug("执行监控记录");
				} catch (InterruptedException e) {
					e.printStackTrace();
					Thread thread = Thread.currentThread();
					thread.interrupt();
				}
			}
		});
		monitor.start();
	}
	//停止监控线程
	public void stop(){
		monitor.interrupt();
	}
}

打断park线程

打断park线程,不会清空打断状态

@Slf4j(topic = "c.Test4")
public class Test4 {


	public static void main(String[] args) throws InterruptedException {
		test3();
	}

	public static void test3() throws InterruptedException {
		Thread t1 = new Thread(()->{
			log.debug("park...");
			LockSupport.park();
			log.debug("unpark...");
			log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
		},"t1");
		t1.start();
		
		Thread.sleep(1000);
		t1.interrupt();
	}
}

主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束

例:

log.debug("开始运行");
Thread t1 = new Thread(() -> {
    log.debug("开始运行...");
    sleep(2);
    log.debug("运行结束");
},"daemo");
//设置该线程为守护线程
t1.setDaemon(true);
t1.start();

sleep(1);
log.debug("运行结束...");

结果

06:37:11 [main] c.Test4 - 开始运行
06:37:11 [daemo] c.Test4 - 开始运行...
06:37:11 [main] c.Test4 - 运行结束...

注意

  • 垃圾回收器线程就是一种守护线程
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求

五种状态

对于操作系统层面来说

  1. 初始状态:仅是在语言层面上创建了线程对象,还未与操作系统关联

  2. 可运行状态:指线程已经被创建(与操作系统线程关联),可以由CPU调度执行

  3. 运行状态:指获取了CPU时间片运行中的状态

    当CPU的时间片用完,会从运行状态转换至可运行状态,会导致线程的上下文切换

  4. 阻塞状态

    如果调用了阻塞API,例如BIO读写文件,这时线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态

    等待BIO操作完毕,会由操作系统唤醒阻塞线程,转换至可运行状态

    与可运行状态的区别是,阻塞状态只要不被唤醒,永远不会被调度

  5. 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态

六种状态

对于Java层面来说

  1. NEW:线程刚被创建,但是没有调用start()方法
  2. RUNNABLE:当调用了start()方法之后,进入RUNNABLE状态,其将操作系统层面的【可运行状态】【运行状态】【阻塞状态】
  3. BLOCKED:对【阻塞状态】的细分,例如在竞争锁失败时进入
  4. WAITING:对【阻塞状态】的细分,没有时间的等待
  5. TIMED_WAITING:对【阻塞状态】的细分,有时间的等待,例如Thread.sleep(1000)
  6. TERMINATED:终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态