二、Java线程
2.1 创建和运行线程
方法一:Thread创建线程方式:
- 继承Thread类
- 匿名内部类方式
public class CreateThread01 {
public static void main(String[] args) {
// 匿名内部类方式
Thread t1 = new Thread("t1"){
// run 方法内实现了要执行的任务
public void run() {
System.out.println("匿名内部类方式....."+ Thread.currentThread().getName());
}
};
t1.start();
// 继承Thread类
MyThread t2 = new MyThread();
t2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("创建类继承Thread接口....." + Thread.currentThread().getName());
}
}
方法二:实现Runnable配合Thread
- 实现Runnale接口
- 匿名内部类方式
public class CreateThread02 {
public static void main(String[] args) {
// 实现Runnale接口
MyThread1 myThread1 = new MyThread1();
Thread t1 = new Thread(myThread1);
t1.start();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类的方式....."+Thread.currentThread().getName());
}
};
Thread t2 = new Thread(runnable);
t2.start();
}
}
// 实现Runnale接口
class MyThread1 implements Runnable{
@Override
public void run() {
System.out.println("使用Runnable接口创建线程......"+ Thread.currentThread().getName());
}
}
方式一和方式二的比较:
- 开发中优先选择实现Runnable接口的方式
- 原因:
(1)实现的方式没有类的单继承性的局限性
(2)实现的方式更适合来处理多个线程有共享数据的情况 - 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
方法三:实现Callable接口
实现Callable接口:
- 定义一个线程任务类实现Callable接口,申明线程执行结果类型
- 重写Callable接口的call方法,这个方法可以直接返回执行结果
- 创建一个Callable的线程任务对象
- 把Callable的线程任务对象包装成一个未来任务对象
- 把未来任务对象包装成线程对象
- 调用线程的start()方法启动线程
-
public FutureTask(Callable<V> callable)
:未来任务对象,在线程执行完后得到线程的执行结果。 -
FutureTask就是Runnable对象,因为Thread类只能执行Runnable实例的任务对象,所以把Callable包装成未来任务对象
-
public V get()
:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步- get()线程会阻塞等待任务完成
- run()执行完后会把结果设置到FutureTask的一个成员变量,get()线程可以获取到该变量的值
public class CreateThread03 {
public static void main(String[] args) {
// 第一种将实现Callable接口类传入FutureTask
FutureTask<String> task = new FutureTask<String>(new MyCallable());
new Thread(task,"t3").start();
// 获取call方法返回的结果(正常/异常结果)
String result = null;
try {
result = task.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
// 第二种内部类实现
FutureTask<String> task1 = new FutureTask<String>(() -> {
return Thread.currentThread().getName() + "->" +"FutureTask";
});
new Thread(task1,"t4").start();
try {
result = task1.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String> {
@Override //重写线程任务类方法
public String call() throws Exception {
return Thread.currentThread().getName() + "->" +"实现Callable接口";
}
}
优点:
-
可以定义返回值
-
可以抛出异常
方法四:利用线程池创建
使用 ThreadPoolExecutor 创建线程池,并从线程池中获取线程用于执行任务。在 JUC 中,Executor 框架已经实现了几种线程池,以 Executor 的 newFixedThreadPool 来作为 Demo 的展示。
public class CreateThread4 {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
Date date1 = new Date();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable1(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable1 implements Callable<Object> {
private String taskNum;
MyCallable1(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
-
ExecutorService、Callable、Future 实际上都是属于 Executor 框架。
-
线程池支持有返回结果和无返回结果的任务,有返回值的任务必须实现Callable接口,无返回值的任务必须实现Runnable接口。
-
对于有结果的任务,执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到Callable任务返回的 Object 了,
-
但需要注意的是:get方法是阻塞的,如果线程未返回结果,那么 get() 方法会一直等待,直到有结果返回或者超时。
2.2 查看进程线程的方法
windows
- 任务管理器可以查看线程和进程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
ps -fe 查看所有进程
ps -fT -p PID 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写H切换是否显示线程
top -H -p PID 查看某个进程(PID)的所有线程
Java
jps 查看所有java进程
jstack pid 查看某个java进程(Pid)的所有进程状态
jconsole 查看某个java进程中线程运行情况(图形界面)
jconsole 远程监控配置
-
需要以如下方式运行你的 java 类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 - Dcom.sun.management.jmxremote.authenticate=是否认证 java类
-
修改/etc/hosts文件将127.0.0.1映射至主机名
-
如果要认证
- 复制 jmxremote.password 文件
- 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
- 连接时填入 controlRole(用户名),R&D(密码)
2.3原理之线程运行
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
JVM由堆、栈、方法去所组成,每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换
导致 cpu 不再执行当前的线程,转而执行另一个线程的代码原因:
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、join、wait、park、synchronized、lock等方法
当上下文切换(Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。
- 状态包括程序计数器、虚拟机中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- 线程上下文切换(Context Switch)频繁发生会影响性能
2.4 常用api以及详解
方法 | 说明 |
---|---|
start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法 |
run() | 线程启动后调用该方法 |
join() | 等待线程运行结束 |
join(long n) | 等待线程运行结束,最多等n毫秒 |
getId() | 获得线程长整型id id是唯一的 |
getName() | 获得线程名 |
getName(String name) | 修改线程名 |
getPriority() | 获得线程优先级 |
setPriority(int n) | 修改线程优先级,java中规定线程优先级是1-10的整数,较大的优先级能提高该线程被CPU调度的几率,注意:这不是一定按照优先级的大小来进行调度的 |
getState() | 获取线程的状态,Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
isInterrupted() | 判断是否被打断,不会清楚打断标记 |
isAlive() | 线程是否存活,即是否运行完毕 |
interrupt() | 打断线程,如果打断线程正在sleep、wait、join中会导致被打断的线程抛出出 InterruptedException,并清除打断标记;如果被打断的是正在运行的线程,则会设置打断标记,则会设置打断标记;park线程被打断,也会设置打断标记 |
isInterrupt() | 判断当前线程是否被打断,会清楚打断标记 |
currentThread() | 获取当前正在执行的线程 |
sleep(long n) | 让当前线程休眠n毫秒,休眠时让出cpu时间片给其他线程,睡眠结束后的线程未必会立刻得到执行 |
yield() | 提示线程调度器让出当前线程对CPU的使用 |
start与run
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
sleep与yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
- 具体实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但仅仅是一个提示,调度器可以忽略。
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没有作用
join方法
@Slf4j
public class ThreadJoinTest {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1(){
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
sleep(1);
log.debug("结束");
r = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
log.debug("结束:{}",r);
log.debug("结束");
}
}
//注意: sleep用的是工具类
public class Sleeper {
public static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sleep(double i) {
try {
TimeUnit.MILLISECONDS.sleep((int) (i * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印结果为什么为0,而不是打印r=10 ?
-
这个问题是因为主线程和t1线程是并行的,t1线程需要1秒才能算出r=10
-
而主线程直接就打印出了r的值,所以只能打印出r=0
解决方法:
- 我们能不能让主线程等待t1线程运行完,在打印出值。这就用到join()
@Slf4j
public class ThreadJoinTest {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
sleep(1);
log.debug("结束");
r = 10;
} catch (Exception e) {
e.printStackTrace();
}
});
t1.start();
// 在主线程打印r值的前面加一个join,作用是等待t1线程运行完,在继续往下运行
t1.join();
log.debug("结束:{}",r);
log.debug("结束");
}
}
还有一个有时效的join方法
没等够时间
@Slf4j
public class ThreadJoinTest {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
sleep(2); // 睡2s
log.debug("结束");
r = 10;
} catch (Exception e) {
e.printStackTrace();
}
});
t1.start();
// 等1s
t1.join(1000);
log.debug("结束:{}",r);
log.debug("结束");
}
}
等够时间
@Slf4j
public class ThreadJoinTest {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
sleep(2); // 睡2s
log.debug("结束");
r = 10;
} catch (Exception e) {
e.printStackTrace();
}
});
t1.start();
// 等3s
t1.join(3000);
log.debug("结束:{}",r);
log.debug("结束");
}
}
interrupt方法
打断sleep线程
打断 sleep,wait,join 的线程
会让这几个线程都进入阻塞状态
打断sleep的线程,会清空打断标记
@Slf4j
public class InterruptTest01 {
public static void main(String[] args) {
Runnable r1 = (() -> {
// sleep 导入工具类 现在是以秒为单位
sleep(1);
});
Thread t1 = new Thread(r1,"t1");
t1.start();
sleep(0.5);
t1.interrupt();
log.debug("打断状态:{}",t1.isInterrupted());
}
}
- 打断sleep的线程会抛出InterruptedException异常
- 并且清空打断标记
打断正常线程
打断正常运行的线程,不会清空打断标记
@Slf4j
public class InterruptTest02 {
public static void main(String[] args) {
Runnable r1 = (() -> {
while(true){
Thread currentThread = Thread.currentThread();
boolean interrupted = currentThread.isInterrupted();
if (interrupted){
log.debug("打断状态:{}",interrupted);
break;
}
}
});
Thread t1 = new Thread(r1,"t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
}
注意:
- 并不是说我打断这个正常运行的线程,他就停止了,如果光打断正常运行的线程,不判断打断状态的话,并不会停止,只是打断标记为true而已。
打断park线程
park: 暂停当前线程,处于 WAIT 状态
unpark: 既可以在 park 之前调用或之后调用,都是用来恢复某个线程的运行,简单的说,调用 unpark 后再调用 park 线程依然不会暂停,类似提前“解毒”。
打断 park 线程, 不会清空打断状态
@Slf4j
public class InterruptTest03 {
public static void main(String[] args) {
Runnable r1 = (() -> {
log.debug("park....");
LockSupport.park();
log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
});
Thread t1 = new Thread(r1,"t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
}
注意:并不会停止运行,只是将打断标记设置为true
但是如果打断标记已经是true,则park将会失去作用
@Slf4j
public class InterruptTest03 {
public static void main(String[] args) {
Runnable r1 = (() -> {
for (int i =0; i < 5; i++){
log.debug("park....");
LockSupport.park();
log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
}
});
Thread t1 = new Thread(r1,"t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
}
模式之两阶段终止
在一个线程t1中如何终止线程t2,并且能让t2在终止前,能有一个操作的机会。
利用isInterrupt()方法
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行。
@Slf4j
public class InterrruptApplyTest {
public static void main(String[] args) throws InterruptedException {
TPTInterrupt t1 = new TPTInterrupt();
t1.start();
Thread.sleep(3500);
log.debug("调用封装的stop");
t1.stop();
}
}
@Slf4j
class TPTInterrupt{
private Thread thread;
public void start(){
thread = new Thread(()-> {
while(true){
Thread current = Thread.currentThread();
if (current.isInterrupted()){
log.debug("最后的操作!");
break;
}
try {
// 这里的sleep并不是封装的 因为我们想让他抛出异常才能进行我们的下一步操作
sleep(1000);
log.debug("业务操作!");
} catch (InterruptedException e) {
// 防止在睡的时候被主线程打断,我们在这里再次打断就可以继续运行
current.interrupt();
}
}
},"工作线程");
thread.start();
}
public void stop(){
thread.interrupt();
}
}
利用停止标记
volatile:保证了可见性、有序性
- 保证此变量对所有线程的可见性,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存中,每次使用前从主内存中取值
- 禁止指令重排序优化,这个操作相当于一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置,在懒汉式的优化中也经常这样使用。
@Slf4j
public class InterrruptApplyTest {
public static void main(String[] args) throws InterruptedException {
TPTVolatile t1 = new TPTVolatile();
t1.start();
Thread.sleep(3500);
log.debug("调用封装的stop");
t1.stop();
}
}
@Slf4j
class TPTVolatile{
private Thread thread;
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见
// 主线程把它改成true,对t1线程可见
private volatile boolean stop = false;
public void start(){
thread = new Thread(()-> {
while(true){
Thread current = Thread.currentThread();
if (stop){
log.debug("最后的操作!");
break;
}
try {
// 这里的sleep并不是封装的 因为我们想让他抛出异常才能进行我们的下一步操作
sleep(1000);
log.debug("业务操作!");
} catch (InterruptedException e) {
// 防止在睡的时候被主线程打断,我们在这里再次打断就可以继续运行
current.interrupt();
}
}
},"工作线程");
thread.start();
}
public void stop(){
stop = true;
}
}
加粗样式
2.5 主线程和守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程里的代码没执行完,也会强制结束。
@Slf4j
public class GuardThreadTest {
public static void main(String[] args) {
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行....");
// 2s后
sleep(2);
log.debug("运行结束....");
},"daemon");
// 设置该线程是守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
}
}
主线程结束后,守护线程并没有执行 运行结束
- 垃圾回收器线程就是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它门处理完当前请求
2.6五种状态
操作系统层面
- 初始状态 仅仅是在语言层面创建了线程对象,还未与操作系统的线程关联
- 可运行状态 (就绪状态) 指线程已经被创建(与操作系统的线程关联),可以由CPU调度执行
- 运行状态 指已获取了CPU时间片运行中的状态
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
- 等BIO操作完,会由操作系统唤醒阻塞的线程,转换至 可运行状态
- 与 可运行状态的区别是,对 阻塞状态的线程来说只要一直不唤醒,调度器就一直不会考虑调度它们
- 终止状态表示线程已经执行完毕,生命周期已经结束,不会再转换为奇它状态
2.7六种状态
Java层面 根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没调用start()方法
- RUNNABLE 当调用了start()方法之后,在Java API层面的 RUNNABLE状态涵盖了操作系统层面的 可运行状态、运行状态和阻塞状态(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)
- BLOCKED、WAITING、TIME_WAITING都是Java API层面对 阻塞状态的细分
- TERMINATED当线程代码运行结束