java多线程2

3.5 线程中的常见方法

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

3.6 Start与run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test05")
public class Test05 {

public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running......");
}
};
//只是调用了该线程类的普通方法而已,并没有启动t1线程
t1.run();
//启动了t1线程
t1.start();
log.debug("do other things......");
}
}

直接调用run方法并没有启动新的线程,他还是在主线程来调用了run方法。相当于创建一个对象直接调用,没有开启线程。

Thread类中的start方法调用的是native方法,也就是C++方法,由操作系统来new线程。

image-20240406105905327

new表示线程处于一个新建的状态还没有被CPU运行,而在线程启动(t1.start())以后再去打印,打印的是runnable,表示可以被cpu所调度执行。

3.7 Sleep与yield

sleep

  • 调用sleep方法会让线程从running 进入Timed Waiting状态(阻塞)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import lombok.extern.slf4j.Slf4j;
    @Slf4j(topic = "c.Test07")
    public class Test07 {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread("t1"){
    @Override
    public void run() {
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    };
    t1.start();
    //首先打印RUNNABLE的原因是:主线程的代码先运行,先运行以后t1线程再运行,
    //t1线程运行以后他还没有进入休眠状态,这时候主线程的t1.getState()就已经执行了
    log.debug("t1的state是:"+t1.getState());
    //休眠主线程
    Thread.sleep(500);
    //主线程等了500ms之后t1线程进入了休眠状态
    log.debug("t1的state是:"+t1.getState());
    }
    }
  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出interruptedException

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import lombok.extern.slf4j.Slf4j;
    @Slf4j(topic = "c.Test08")
    public class Test08 {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread("t1"){
    @Override
    public void run() {
    log.debug("Enter Sleeping......");
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    log.debug("wake up");
    throw new RuntimeException(e);
    }
    }
    };
    t1.start();

    Thread.sleep(1000);
    log.debug("interrupt...");
    t1.interrupt();

    }
    }
  • 睡眠结束以后的线程未必会立刻得到执行(也许CPU正在执行其他的代码,等任务调度器将新的时间片分给线程以后才能运行)

  • 建议使用TimeUnit的sleep代替Thread的Sleep来获得更好的可读性

sleep案例 - 防止CPU占用100%

Sleep实现

在没有利用CPU来进行计算的时候,不要让while(true)空转来浪费CPU,这时候可以使用yield或者sleep来让出CPU的使用权给其他程序

1
2
3
4
5
6
7
while(true){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.printStackTrace();
}
}
  • 可以利用wait或条件变量达到类似的效果
  • 不同的是后两者都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场所
  • sleep适用于无需锁同步的场景

yield

  • 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果。(有时候会出现想让但是没有让出去的情况)
  • 具体的实现依赖操作系统的任务调度器

yield和sleep的区别

sleep和yield看起来都是让线程暂时先不要运行,将机会让给其他线程。

  • 就绪状态Runnable还是有机会被任务调度器调度的。就绪状态,任务调度器还是会分时间片给就绪状态的
  • 阻塞状态Timed Waiting任务调度器不会将时间片分给阻塞状态。

线程优先级

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

不论是yield还是sleep都不能真正的去控制线程的调度,他们最终还是由操作系统的任务调度器来决定最终哪一个线程分到更多的时间片,他们都是给任务调度器一个提示而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test10 {

public static void main(String[] args) {
Runnable task1 = () ->{
int count = 0;
for (;;){
System.out.println("----->1 " + count++);
}
};
Runnable task2 = () ->{
int count = 0;
for (; ;){
//将CPU时间片分给task1
// Thread.yield();
System.out.println(" ----->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}

3.8 join方法详解

为啥要使用join?

这里先看一段程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
@Slf4j(topic = "c.Test11")
public class Test11 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1()throws InterruptedException{
log.debug("开始");
Thread t1 = new Thread(() ->{
log.debug("开始");
try {
sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("结束");
r =100;
},"t1");
t1.start();
log.debug("结果为:{}",r);
log.debug("结束");
}
}

此程序的结果为

  DEBUG [main] (Test11.java:12) - 开始
  DEBUG [t1] (Test11.java:14) - 开始
  DEBUG [main] (Test11.java:24) - 结果为:0
  DEBUG [main] (Test11.java:25) - 结束
  DEBUG [t1] (Test11.java:20) - 结束
一、主方法执行test1(显然主线程在执行test1,)打印了开始几乎与此同时线程1被启动了,打印了线程1中的开始(从打印的时间或先后顺序上看的)
二、分析先打印主线程的开始(因为主线程开始启动的时候,t1还没开始启动呢,然后t1又休眠了,那主线程结束也肯定比t1结束的快一些),这时候已经来不及获取r的值了,所以R的输出结果为0,在t1线程需要1s之后才能算出r = 10

如何解决获取r = 100的这个值呢?

  • 使用sleep(主线程休眠)可以吗? (行但是不完全行,时间不好把握)
  • 用join,夹在t1.start()之后即可
1
2
3
4
5
DEBUG [main] (Test11.java:23) - 开始
DEBUG [t1] (Test11.java:25) - 开始
DEBUG [t1] (Test11.java:31) - 结束
DEBUG [main] (Test11.java:36) - 结果为:100
DEBUG [main] (Test11.java:37) - 结束

应用之同步(案例1)

从调用的角度上来讲,如果

  • 需要等待结果的返回,才能继续运行就是同步
  • 不需要等待返回的结果,就能继续运行就是异步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
@Slf4j(topic = "c.Test12")
public class Test12 {
static int r1 = 0, r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
public static void test2()throws InterruptedException{
Thread t1 = new Thread(() ->{
try {
sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r1 = 10;
});

Thread t2 = new Thread(() ->{
try {
sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
r2 = 20;
});
t1.start();
t2.start();
//计时
long start = System.currentTimeMillis();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
//这里有一个问题这个时间差到底是多少呢? 2s
log.debug("r1: {} r2: {} cost: {}",r1 , r2,end-start);
}
}

具体的分析如下所示:

  • 第一个join:等待t1时候,t2并没有停止,而是在运行
  • 第二个join:1s后,执行到此,t2也运行了1s,因此也只需要再等待1s

3.9 interrupt方法详解

打断sleep,wait,join的线程

阻塞

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;
@Slf4j(topic = "c.Test14")
public class Test14 {
static int r1 = 0, r2 = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1()throws InterruptedException{
Thread t1 = new Thread(() ->{
log.debug("sleep.......");
try {
sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1");
t1.start();
sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}",t1.isInterrupted());
}
}

输出

1
2
3
DEBUG [t1] (Test14.java:14) - sleep.......
DEBUG [main] (Test14.java:23) - interrupt
DEBUG [main] (Test14.java:25) - 打断标记:false

打断正常运行的线程

打断标记的用途:打断的状态可以用来停止线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test15")
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
//对于正常运行的线程如果被其他线程打断之后,布尔值会变成true
if (interrupted) {
log.debug("被打断了,退出了循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);//主线程睡眠一秒
log.debug("interrupt");
t1.interrupt();
}
}

3.11 主线程与守护线程

默认的情况下,java线程需要等待所有的线程都运行结束,才会结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("结束");
}, "t1");
t1.start();

Thread.sleep(1000);
log.debug("结束");
}
}
1
DEBUG [main] (Test17.java:19) - 结束

有一种特殊的线程叫做守护线程,只要其他的非守护线程结束了,即使守护线程的代码没有执行完毕,也会强制结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("结束");
}, "t1");
//将t1设置为守护线程
t1.setDaemon(true);
t1.start();

Thread.sleep(1000);
log.debug("结束");
}
}
1
DEBUG [main] (Test17.java:19) - 结束

垃圾回收器就是一种守护线程

Tomcat中的Acceptor和poller线程都属于守护线程(Tomcat用来接收请求和分发请求的线程),所以Tomcat接收到shutdown命令后,不会等待他们(Acceptor和poller)处理完当前的请求(强制结束)。

3.1.2 五种状态

从操作系统层面来描述的

graph TB
A[初始状态] --> B[可运行的状态]
B --> C[运行状态]
C --> D[终止状态]
C --> B
E --> B
G((CPU)) --> C
C --> E[阻塞状态]

  • 【初始状态】仅仅是在语言层面创建了线程对象,还未与操作系统相互关联(只是在对象的层面new了一个对象)
  • 【可运行的状态】(就绪状态)指的是该线程已经被创建了(与操作系统相互关联),还可以由CPU调度执行,暂时没有获得CPU的时间片
  • 【运行状态】指的是获取了CPU时间片运行中的状态
  • 当cpu时间片运行完,会从【运行状态】–>【可运行的状态】,会导致线程上下文切换,
  • 【阻塞状态】
    • 如果调用了阻塞的API,如BIO读写文件,这时线程不会实际使用到CPU,会导致线程上下文切换,进入了【阻塞状态】
    • 等到BIO执行完,会由操作系统唤醒阻塞的线程,转换至【可运行的状态】
    • 与可运行状态的区别是,对于阻塞状态的线程来说只要一直不唤醒,调度器就一直不会考虑调度他们
  • 终止状态 :线程已经执行完毕,生命周期结束,不会转化为其他的状态

3.1.3六种状态

这是根据JAVA API来描述的

根据Thread State枚举,分为六种状态

3.1.4 习题

阅读华罗庚《统筹方法》,
给出烧水泡茶的多线程解决方案,提示

  • 参考图二,用两个线程(两个人协作)模拟烧水泡茶过程

    • 文中办法乙、丙都相当于任务串行
    • 而图一相当于启动了4个线程,有点浪费
  • 用sleep(n)模拟洗茶壶、洗水壶等耗费的时间

华罗庚《统筹方法》
附:

  • 统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关
    系复杂的科研项目的组织与管理中,都可以应用。
    怎样应用呢?主要是把工序安排好
    比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗,火已生了,茶叶也有了
    怎么办?

    • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶,等水开
      了,泡茶喝。
    • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了
      泡茶喝。
    • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开,水开了之后,急急忙忙找茶叶,洗茶壶茶杯
      泡茶喝。

    哪一种方法省时间?一眼便可以看得出,第一种方法好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("洗水壶");
try {
Thread.sleep(1000);
log.debug("烧开水");
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"老王");

Thread t2 = new Thread(() ->{
log.debug("洗茶壶");
try {
Thread.sleep(1000);
log.debug("清洗茶叶杯子");
Thread.sleep(2000);
log.debug("拿茶叶");
Thread.sleep(1000);
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("泡茶叶");
},"小王");
t1.start();
t2.start();
}
}

1
2
3
4
5
6
DEBUG [老王] (Test17.java:7) - 洗水壶
DEBUG [小王] (Test17.java:18) - 洗茶壶
DEBUG [老王] (Test17.java:10) - 烧开水
DEBUG [小王] (Test17.java:21) - 清洗茶叶杯子
DEBUG [小王] (Test17.java:23) - 拿茶叶
DEBUG [小王] (Test17.java:29) - 泡茶叶
  • 上述的模拟是小的等老的水烧开了之后,小的泡茶,如果反过来要等老王等小王的茶叶拿来,老王泡茶叶呢?代码最好可以使用两种情况
  • 上面的线程其实就是各执行各的如果要模拟老的将茶壶交给小的泡茶叶,或者模拟小王将茶叶交给老王泡茶叶呢?

java多线程2
http://example.com/2024/04/22/JUC-03-2/
作者
nianjx
发布于
2024年4月22日
许可协议