目录
Java如何进行多线程编程?
线程是操作系统的概念, 操作系统提供了一些 API 可以操作线程, Java针对上述系统 API 进行封装, 我们需要掌握这套 API 即可
Thread 类创建线程(5种方法)
注意: Thread是Java标准库内置的类, 平时写程序用到一个类, 需要import, 此处 Thread 不需要 import 也能直接使用, 这是因为 Thread 在 Java.lang 包下
1> 创建一个类继承 Thread
class MyThread extends Thread {
@Override
public void run() {
//线程的入口方法
while(true) {
System.out.println("hello thread");
}
}
}
public class Test {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while(true) {
System.out.println("hello main");
}
}
}
每个线程都是一个独立的执行流, 每个线程都可以执行一系列的代码, 一个线程跑起来, 从入口方法开始执行
想启动线程时需要使用t.start(), 而不是 t.run(), 前者是让系统调用API创建出了一个线程, 这个线程再调用 run() 方法, 而后者只是调用了 run() 方法. 这样只会让 run() 方法执行, 打印 hello thread, 而不会打印 hello main; 二者不同看下运行结果.
前者: 两个线程都打印
后者: 只打印run()方法
这两个线程的循环转的太快, 想让它慢一点. 使用sleep 休眠功能
在 Java 种 sleep 是 Thread 的静态方法, static 静态的, 在Java 种 static 作用个 " 静态 " 没有什么关系, Java 中 static 修饰 类方法, 类属性, 对应的的是 实例方法 和 实例属性, 前者 拿着类名直接使用, 后者创建实例化对象才能调用.
sleep 是一个受查异常, 需要显示处理, 此处需要 try catch , 不能throws, 因为此处 run() 重写父类的, 父类没有 throws
代码例子:
class MyThread extends Thread {
@Override
public void run() {
//线程的入口方法
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
打印结果如图
在俩线程休眠1000ms后这俩线程谁先执行不一定, 可以视为 " 随机 " 的, 但概率不均等
2>实现Runnable 重写run
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread t= new Thread(runnable);
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
实现Runnable 后把任务交给线程即可, 两个关键操作即 明确线程执行的任务 和 调用系统 api 创建出线程.
3> 创建一个类继承Thread 使用内部类
让Thread 实现匿名内部类
public class Demo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4>实现 Runnable , 重写run, 使用匿名内部类
让Runnable实现匿名内部类
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5>基于 lambda 表达式 [推荐]
优点: 更加简洁, 本质上是一个匿名函数(没有名字, 使用一次就完了), 主要用来实现" 回调函数 " 的效果.
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Thread 类的其他使用方式
1> Thread() 创建线程对象
2> Thread(Runnable target) 使用Runnable 对象创建线程
3> Thread(String name) 创建线程并命名. 就是给线程起个名字, 在调试的时候更方便
Thread 类的属性
属性 : ID 获取方法 : getId() 含义: 线程的身份标识, 表示一个进程中的唯一一个线程, 它是Java分配给线程的, 不是 api 分配的.
属性: 名称 获取方法: getName() 含义: 获取线程名字
属性: 状态 获取方法: getState() 含义: 就绪状态, 阻塞状态....
属性: 优先级 获取方法: getPrionrity() 含义: 获取优先级, 应用程序的角度很难察觉
属性: 是否是后台线程 获取方法: isDaemon() 含义: 一个Java进程中有后台线程, 也有前台线程, 如果前台进程不结束, 整个进程不结束, 前台进程不影响整个进程的结束.
举个例子 : 就是正式场合喝酒, 酒就是前台进程, 菜就是后台进程, 如果不喝完酒就不散场, 喝完的话不管菜吃完不吃完都结束了.
一个线程默认情况是前台进程
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "这是新线程");
//设置 t 为后台线程
t.setDaemon(true);
t.start();
}
}
改成后台线程之后, 主线程瞬间执行完了, 没有其他前台线程了, 进程结束, t 线程还来不及执行.
属性 是否存活 获取方法: isAlive 含义: Thread 对象的生命周期, 比系统内核中的线程更长一些, 会出现Thread 对象还在, 内核中的线程已经销毁了.
举个例子:
public class Demo6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("线程开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束");
});
t.start();
System.out.println(t.isAlive());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.isAlive());
}
}
运行结果:
线程启动
start 方法, start 方法内部使用系统 api, 在该系统内核中创建出线程
run 方法, 单纯描述了该线程执行啥内容( 会在 start 创建好线程中自动调用)
中断一个线程
终止/打断(interrupt)
让一个线程停止运行(销毁)
Java中 此操作的做法 就是让 run 方法尽快执行结束
public class Demo7 {
public static boolean isQuit = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
//想让这个线程执行完毕, 就得跳出这个循环, 所以增加一个判断条件
while (!isQuit) {
//打印换成任意逻辑表示实际工作内容都行
System.out.println("线程工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程工作完毕");
});
t.start();
try {
Thread.sleep(5000);
isQuit = true;
System.out.println("设置 isQuit 为 true");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码正常运行
注意: lambda 中以一个规则: 变量捕获, lambda 表达式里面的代码可以自动捕获到上层作用域中涉及到的局部变量的 ~~ 但是不能改变这个局部变量的值. 改的话会报错. 在上述代码中 isQuit 改为了成员变量, 此时lambda 访问这个成员就不再是变量捕获的语法了, 而是" 内部类访问外部类的属性 ", 此时就没有final之类的限制了, 下面的代码我们将全局变量改为局部变量来看一下.
public class Demo {
public static void main(String[] args) {
boolean isQuit = false;
Thread t = new Thread(() -> {
System.out.println("线程开始");
while(!isQuit) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束");
});
t.start();
try {
Thread.sleep(5000);
isQuit = true;
System.out.println("设置 isQuit 为 true");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码直接报错, 无法改变 isQuit 的值. 讲这一点主要是了解 lambda 的变量捕获的规则, 就是这个变量不能修改它的值, 也可以将它设置为 final 属性.
上述方案的缺点:
1> 需要手动创建变量
2> 当线程内部在 sleep 的时候, 主线程修改变量, 新线程内部不能及时响应
我们使用另一种方法(Thread 类内部有一个现成的标志位, 判定当前循环是否要结束)
public class Demo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
//其中Thread.currentThread()可以获取到当前线程的实例, 就是 t
//哪个线程调用这个方法, 就会返回那个线程的对象, 但是不能直接写 t ,还没实例化呢
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("让 t 线程终止");
//这个操作把Thread 对象内部的标志位设置位true了, 即使线程内部出现阻塞(sleep), 仍然可以用这个方法唤醒
t.interrupt();
}
}
运行的结果如图, 我们会发现线程停止后又重新开始了, 这是因为抛出异常后, 系统会自动清除刚才设置的标志位, 这样就是 "设置标志位" 的效果好象没有生效一样
这让我们确定在线程收到":要中断" 的信号时, 能够自动决定接下来怎么处理
三种方式:
1> 直接结束
2>不理会
3>完成接下来的工作在结束
public class Demo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
//其中Thread.currentThread()可以获取到当前线程的实例, 就是 t
//哪个线程调用这个方法, 就会返回那个线程的对象, 但是不能直接写 t ,还没实例化呢
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//1 不理会
//2 break;
//3 加入工作
//3 break;
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("让 t 线程终止");
//这个操作把Thread 对象内部的标志位设置位true了, 即使线程内部出现阻塞(sleep), 仍然可以用这个方法唤醒
t.interrupt();
}
}
上述说法只是在有异常的情况下说明的, 如果没有异常就会直接停止
线程等待
让线程等待另一个线程执行结束再继续执行, 本质上就是控制线程结束的顺序
join 实现线程等待效果
主线程中, 调用t.join(), 此时就是主线程等待t线程结束
例子:
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++){
System.out.println("t 线程工作中!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//让主线程来等待 t 线程执行结束
//一旦调用join, 主线程就会触发阻塞, 此时 t 线程就可以趁机完成后续的工作
//一旦阻塞到 t 线程执行完毕了, join 才会解除阻塞, 才能继续执行
System.out.println(" join 等待开始");
t.join();
System.out.println(" join 等待结束");
}
}
执行结果如图
t.join 工作过程
1> 如果 t 线程正在运行中, 此时调用 join 的线程就会阻塞, 一直阻塞到 t 线程执行结束为止
2> 如果 t 线程已经执行结束了, 此时调用 join 线程, 就直接返回了
一般在实际情况下可能会出现死等的情况, 设计开发中最好带有一个超时时间
获取当前线程引用
(public static Thread currentThread())
返回当前线程对象的引用
休眠当前线程
(public static void sleep(long mills) throws interruptedException)
public class Test {
public static void main(String[] args) throws InterruptedException {
long beg = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println(end - beg);
}
}
休眠当前线程 mills 毫秒
输出结果为:
系统由阻塞状态 变成 就绪状态, 被唤醒之后不能立刻到 CPU 上运行, 有一个调度开销, 所以会有误差.
线程的状态
进程的状态最核心的一个是 就绪状态, 一个是阻塞状态 (对于线程同样适用)
Java中有给线程赋予了一些其他的状态
NEW : Thread 对象已经有了. start 方法还没调用
TERMINATED: Thread 对象改在, 内核中的线程已经没了
RUNNABLE: 就绪状态 (线程在cpu上执行了/线程正在排队等待上 CPU 执行)
TIMED_WAITING: 阻塞, 由于 sleep 这种固定时间的方式产生的阻塞
WAITING: 阻塞, 由于 wait 这种不固定时间的方式产生的阻塞
BLOCKED: 阻塞, 由于锁竞争导致的阻塞
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
});
// 在调用 start 之前获取状态, 此时就是 NEW 状态
System.out.println(t.getState());
t.start();
for (int i = 0; i < 5; i++){
System.out.println(t.getState());
Thread.sleep(1000);
}
t.join();
// 在线程执行结束之后, 获取线程的状态, 此时是 TERMINATED 状态
System.out.println(t.getState());
}
}
在运行开始前 NEW 状态, 开始后 RUNNABLE 状态, 如果在 t 线程中增加sleep会怎样呢?
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 在调用 start 之前获取状态, 此时就是 NEW 状态
System.out.println(t.getState());
t.start();
for (int i = 0; i < 5; i++){
System.out.println(t.getState());
Thread.sleep(1000);
}
t.join();
// 在线程执行结束之后, 获取线程的状态, 此时是 TERMINATED 状态
System.out.println(t.getState());
}
}
在sleep()中会变成阻塞状态
小结:
线程的学习需要理解, 认真思考