1. 线程、进程和管程
进程:是程序的一次执行,是系统进行资源分配和调度的独立单位,每一个进程都有自己的内存空间和系统资源。
**线程:**在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程。⼀个进程会有1个或多个线程。
管程:Monitor(监视器),也就是平时所说的锁。
什么是Monitor?
Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,
Object o = new Object(); new Thread(new Runnable() { @Override public void run() { synchronized (o) { } } },"t2");
Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
2. 用户线程和守护线程
Java线程分为用户线程和守护线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t 开始运行," +
(Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
}
}, "t1");
t1.setDaemon(true);
t1.start();
Thread.currentThread().isDaemon()
,线程的daemon属性为true表示是守护线程,false表示是用户线程。
t1.setDaemon(true);
可以设置线程类型。
**守护线程:**是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。
**用户线程:**是系统的工作线程,它会完成这个程序需要完成的业务操作。
我们一般创建或使用的线程都是用户线程,除非用
t1.setDaemon(true);
设置。
重点:
- 当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出
如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。
- 设置守护线程,需要在
start()
方法之前进行。
代码1:t1为用户线程:
public class DaemonDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t 开始运行," +
(Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
while (true) {
}
}
}, "t1");
// t1.setDaemon(true);
t1.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t ---- end主线程");
}
}
![截屏2022-07-13 16.14.35](https://i-blog.csdnimg.cn/blog_migrate/b0ac7ef6e5d750b9eabd11c5bd19d552.png)
此时程序没有结束运行,因为t1为用户进程。
代码2:t1为守护线程:
只需将上述t1.setDaemon(true);
注释打开即可。
![截屏2022-07-13 16.16.47](https://i-blog.csdnimg.cn/blog_migrate/37247b90811582d93eea3f0bfb1127c4.png)
此时主线程结束后,代码结束运行。
3. 线程的创建方式
首先明确一点,Java无法自己开启线程。
为什么?
让我们看一下Thread类的start方法:
public synchronized void start() {
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
...
}
}
这里主要调用了start0()
,而start0()
则是一个本地方法:
private native void start0();
所以创建线程的方式有以下四个方法:
- 继承Thread
- 实现Runnable接口
- 实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
- 线程池
3.1 继承Thread
//注意:打印出来的结果会交替执行,但交替顺序是随机的。
public class ThreadDemo{
public static void main(String[] args) {
//4.创建Thread类的子类对象
MyThread myThread=new MyThread();
//5.调用start()方法开启线程
//[ 会自动调用run方法这是JVM做的事情,源码看不到 ]
myThread.start();
for (int i = 0; i < 100; i++) {
System.out.println("我是主线程"+i);
}
}
}
class MyThread extends Thread{
//2.重写run方法
public void run(){
//3.将要执行的代码写在run方法中
for(int i=0;i<100;i++){
System.out.println("我是线程"+i);
}
}
}
3.2 实现Runnable接口
public class RunnableDemo {
public static void main(String[] args) {
//4.创建Runnable的子类对象
MyRunnale mr=new MyRunnale();
//5.将子类对象当做参数传递给Thread的构造函数,并开启线程
//Runnale taget=mr; //多态,传入target也可以。
new Thread(mr).start();
for (int i = 0; i < 100; i++) {
System.out.println("我是主线程"+i);
}
}
}
//1.定义一个类实现Runnable
class MyRunnale implements Runnable{
//2.重写run方法
@Override
public void run() {
//3.将要执行的代码写在run方法中
for (int i = 0; i < 100; i++) {
System.out.println("我是线程"+i);
}
}
}
实现Runnable接口原理:
调用start方法的时候,会自动调用run方法这是JVM做的事情,源码看不到。
继承Thread和实现Runnable接口方式的区别
(1).查看源码
a.继承Thread:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run()方法
b.实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法
(2).继承Thread
a.好处是:可以直接使用Thread类中的方法,代码简单
b.弊端是:如果已经有了父类,就不能用这种方法
(3).实现Runnable接口
a.好处是:即使自己定义的线程类有了父类也没有关系,因为有了父类可以实现接口,而且接口可以多现实的
b.弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂
3.3 Callable+FutureTask
3.3.1 Callable接口的call方法
Callable接口中的call方法和Runnable接口中的run方法的区别如下:
- 是否有返回值(Runnable接口没有返回值 Callable接口有返回值)
- 是否抛异常(Runnable接口不会抛出异常 Callable接口会抛出异常)
- 落地方法不一样,一个是call() ,一个是run()
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/853d3260ecf20790df21caf89e3049c6.png)
3.3.2 Future接口
- FutureTask是Future接口的唯一的实现类
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Futrue得到Callable的返回值
![截屏2022-07-13 17.20.00](https://i-blog.csdnimg.cn/blog_migrate/65eb047cc36cd898bf71f534d5c43043.png)
有了Runnable,为什么还要有Callable接口?
我们假设一共有四个程序需要执行,第三个程序时间很长。
Runnable接口会按照顺序去执行,会依次从上到下去执行,会等第三个程序执行完毕,才去执行第四个。
Callable接口会把时间长的第三个程序单独开启一个线程去执行,第1、2、4 线程执行不受影响。
3.3.3 创建线程代码
/*
创建线程的方式三: 实现callable接口 ---JDK 5.0 新增
1.创建一个实现Callable接口的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建callable接口实现类的对象
4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star
6.获取callable接口中call方法的返回值
* */
public class CallableDemo {
public static void main(String[] args) {
//3.创建callable接口实现类的对象
NumThead m=new NumThead();
//4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(m);
//5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
//FutureTask类继承了Runnable接口
//new Runnable = futrueTask;
new Thread(futureTask).start();
//6.获取callable接口中call方法的返回值
try {
//get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
Object sum = futureTask.get();
System.out.println("总和是:"+sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.创建一个实现Callable接口的实现类
class NumThead implements Callable {
// class NumThead implements Callable<Integer>{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
//public Integer call() throws Exception {
int sum=0;
for(int i=1;i<=100;i++){
System.out.println(i);
sum+=i;
}
return sum;
}
}
3.3.4 注意事项
- get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞)
- 一个FutureTask,多个线程调用call( )方法只会调用一次
- 如果需要调用call方法多次,则需要多个FutureTask
- 如果直接new FutureTask,不创建线程,则不会调用Callable的call方法
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread t1 = new Thread(futureTask, "t1");
Thread t2 = new Thread(futureTask, "t2");
t1.start();
t2.start();
System.out.println("结果是:"+futureTask.get());
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("---call方法---");
return 666;
}
}
isDone()
轮询(后面我们会用CompletableFuture
来解决get()
阻塞的问题)
- 轮询的方式会消耗无畏的CPU资源,而且也不见得能及时地得到计算的结果
- 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞
public class FutureTaskTest {
public static void main(String[] args) throws Exception{
FutureTask futureTask = new FutureTask(()->{
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t"+"coming......");
return 1024;
});
new Thread(futureTask).start();
//1.果futureTask.get()放到main线程前面,会导致main线程阻塞
//Object o = futureTask.get();
/*Object o = futureTask.get();//不见不散,只要出现了get()方法就会阻塞
System.out.println("不见不散,只要出现了get()方法就会阻塞,获取到的值为:"+o);*/
//2.过时不候
// System.out.println(Thread.currentThread().getName()+"\t"+"线程来了.....");
// Object o2 = futureTask.get(2L, TimeUnit.SECONDS);
//3.使用轮询
while(true){
if(futureTask.isDone()){
System.out.println("使用轮询来解决,值为:"+futureTask.get());
break;
}else{
System.out.println("阻塞中**********");
}
}
}
}
3.4 线程池
线程池比较重要且内容较多,将会单独做一份笔记。
如下是线程池的内容目录。
4. 线程优先级
线程有两种调度模型:
- 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]。
Thread类中设置和获取线程优先级的方法
public final void setPriority(int newPriority):
更改此线程的优先级public final int getPriority():
返回此线程的优先级- 线程默认优先级是5;线程优先级范围是:1-10;
- 线程优先级高仅仅表示线程获取的CPU时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
//除了构造器,还有setName方法可以设置名字
tp1.setName("高铁");
tp2.setName("飞机");
tp3.setName("汽车");
//设置正确的优先级
tp1.setPriority(5);
tp2.setPriority(10);
tp3.setPriority(1);
tp1.start();
tp2.start();
tp3.start();
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/1adb9d549cb5fa0cc4d2b5691f0bc4e3.png)
5. 线程状态
5.1 操作系统线程状态-5种
-
初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
-
可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行 【运行状态】指获取了 CPU 时间片运行中的状态。
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们 -
终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态。
线程的转换具体行为会在 3_线程安全 章节展开
5.2 Java API线程状态-6种
根据 Thread.State 枚举,分为六种状态
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
-
NEW 线程刚被创建,但是还没有调用 start() 方法
-
RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
-
TERMINATED 当线程代码运行结束.
如下代码展示了六种不同的线程状态:
public class TestState {
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
}
6. 线程控制
6.1 Start、Run
public class StartAndRun {
@Test
public void testStart() {
MyThread thread1 = new MyThread();
thread1.setName("thread1");
thread1.start();
System.out.println("---"+Thread.currentThread().getName());
}//输出thread1和主线程main
@Test
public void testRun() {
MyThread thread1 = new MyThread();
thread1.setName("thread1");
thread1.run();
System.out.println("---"+Thread.currentThread().getName());
}//输出全都是主线程main
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
上述testStart()
和testRun()
区别仅在于调用的方法不同,
-
run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行
-
start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码
说明:线程控制资源类
run() 方法中的异常不能抛出,只能 try/catch
- 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
- 异常不能跨线程传播回 main() 中,因此必须在本地进行处理
6.2 sleep、yield
sleep:
- 调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞) - sleep() 方法的过程中,线程不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("t1") {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (Exception e) {
throw new RuntimeException();
}
}
};
thread.start();
System.out.println(thread.getName()+"---"+thread.getState());
Thread.sleep(500);
System.out.println(thread.getName()+"---"+thread.getState());
}
输出:说明sleep会使线程由Running到TIMED_WAITING状态。
t1---RUNNABLE
t1---TIMED_WAITING
yield:
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
- 会放弃 CPU 资源,锁资源不会释放
yield和sleep的区别:
- sleep有休眠时间
- 调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞),休眠时无法获得时间片。 - 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,仍有可能获得时间片。
6.3 join
public final void join():
等待这个线程结束
原理:调用者轮询检查线程 alive 状态,t1.join()
等价于:
public final synchronized void join(long millis) throws InterruptedException {
// 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
while (isAlive()) {
wait(0);
}
}
-
join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁
-
当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕
测试join方法代码:
public class testJoin {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
try {
sleep(2000);
i = 10;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
t1.start();
t1.join();
System.out.println(Thread.currentThread().getName()+"--i结果是:"+i);
}
}
输出结果是:main--i结果是:10
6.4 interrupt
打断线程
public void interrupt()
:打断这个线程,异常处理机制
public static boolean interrupted()
:判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted()
:判断当前线程是否被打断,不清除打断标记
打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
-
sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)
@Test public void test01() throws InterruptedException { Thread thread = new Thread("t1") { @Override public void run() { try { sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; thread.start(); System.out.println(thread.getState()); sleep(500); thread.interrupt(); System.out.println(" 打断状态:" + thread.isInterrupted()); }
-
interrupt不会打断正常运行的线程,但是会改变
isInterrupted()
的布尔值,所以我们需要用这特点自主结束运行。public static void main(String[] args) throws Exception { Thread t2 = new Thread(()->{ while(true) { Thread current = Thread.currentThread(); boolean interrupted = current.isInterrupted(); if(interrupted) { System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true break; } } }, "t2"); t2.start(); Thread.sleep(500); t2.interrupt(); }
打断 park
park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
System.out.println("park...");
LockSupport.park();
System.out.println("unpark...");
System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true
}, "t1");
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
如果打断标记已经是 true, 则 park 会失效
LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不会阻塞
System.out.println("unpark...");//和上一个unpark同时执行
可以修改获取打断状态方法,使用 Thread.interrupted()
,清除打断标记
LockSupport 类在 同步 → park-un 详解