Java多线程-线程核心基础

1.线程和进程的区别

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2.实现多线程的方法

这是Thread类中的run()方法

1
2
3
4
5
public void run() {
if (this.target != null) {
this.target.run();
}
}

下面两个创建线程的方法都是

2.1 实现Runnable接口

实现Runnbale接口,必须重写run()方法,在run方法中定义需要执行的任务。然后创建一个Thread类,将实现类传进去

1
2
3
4
5
6
7
8
9
10
11
public class RunnableStyle implements Runnable {
@Override
public void run() {
System.out.println("【用Runnable,线程被创建....】");
}

public static void main(String[] args) {
Thread thread = new Thread(new RunnableStyle());
thread.start();
}
}

输出结果是:【用Runnable,线程被创建….】

Thread.start()方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public synchronized void start() {
if (this.threadStatus != 0) {
throw new IllegalThreadStateException();
} else {
this.group.add(this);
boolean started = false;

try {
this.start0();
started = true;
} finally {
try {
if (!started) {
this.group.threadStartFailed(this);
}
} catch (Throwable var8) {
}
}
}
}

this.start0();可以看出来是这句话真正创建了线程。

1
private native void start0();

可是深入进去这个方法是一个本地方法,是由c/c++编写的,供JVM调用。所以Thread.start()将线程启动后,在底层代码中,JVM将会自动回调Thread.run()方法。由于实现Runnable接口后,将实现类的实例传到Thread的构造器中,Runnable target; 这个target就是这个实例,因此在Thread.run() 中实际上又调用了我们重写的run()方法,因此会有这样的输出结果。

Thread.run() 源码:

1
2
3
4
5
public void run() {
if (this.target != null) {
this.target.run();
}
}

2.2 继承Thread类

直接继承Thread类来重写run()方法,最终这个重写的run()方法体的内容将会直接覆盖父类run()方法中的代码。

1
2
3
4
5
6
7
8
9
10
11
public class ThreadStyle extends Thread {
@Override
public void run(){
System.out.println("继承thread类,线程被创建...");
}

public static void main(String[] args) {
Thread threadStyle = new ThreadStyle();
threadStyle.start();
}
}

对比

实现Runnable接口创建线程是优于继承Thread创建线程。

  • java不支持多继承,一个类只能继承一个父类,如果已经继承了Thread类的话,就不能再继承其他类了,大大限制了可扩展性。
  • 实现Runnable接口比较节约资源。——有待考证,不太理解
  • 从代码架构的角度,使用接口有利于降低程序间的耦合度,线程具体要执行的任务与线程的整个生命周期相关的代码应该解耦,因为线程的生命周期相关的方法是start()、线程阻塞,线程终止等,这是由Thread类所管理的,他们目的不一样,所以应该解耦。

2.3 创建线程的错误观点

以下这些,仅仅是在代码层面的千变万化,但其本质是万变不离其宗的。

1.线程池创建线程

本质是

1
Thread t = new Thread(this.group, r, this.namePrefix + this.threadNumber.getAndIncrement(), 0L);

2.通过Callable和FutureTask创建线程

3.定时器创建线程

本质是Thread thread = new Thread(…..)

4.匿名类部类,lambda表达式

2.4 有多少种实现线程的方法

从不同的角度看,会有不同的答案。

1.两种本质实现方法

实现Runnable接口和继承Thread类,比较他们的优缺点,通过看源代码,发现其实他们的本质是一样的,都是通过Thread的构造方法来创建一个新线程

2.几种代码实现方法

其他的外在表现形式的本质还是通过thread类创建线程

3.启动线程

只有start()方法是启动线程的方法,会经历线程的各个生命周期

在当前线程(一般是主线程)中创建新线程对象,主线程执行start()方法,start()方法内的本地方法start0()会通知JVM在有空闲的情况下去运行这个线程,不过至于这个线程何时去运行,由线程调度器决定,所以这个线程可能稍后或者很久都不运行。

不能重复执行start()方法,因为第一次调用过后,线程状态会被改变,在第二次再执行start()后,线程状态就不是初始的创建的状态了。

如果直接调用run()方法的话,它就是一个普通的方法,最终的线程还是主线程,并没有做通知虚拟机等相关操作,并不能执行新线程。所以必须由虚拟机回调这个方法才可以

4.停止线程

如果没有外界干涉的情况下,线程通常的停止过程:

  1. run()方法执行完毕
  2. 有一点异常出现,但没有被捕获

在有外界干涉的情况下,正确停止线程的方法是使用interrupt,好处:数据安全,并且我们要把主动权交给被中断线程。

原理:使用interrupt来通知,而不是强制

4.1停止线程的几种情况

  1. 当线程处于正在运行时(不处于阻塞状态),使用interrupt停止线程,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RigthWayToStopThread implements Runnable{

@Override
public void run() {
for(int i = 0; i < 1000000; i++){
//当前线程是否响应中断取决于线程内的代码逻辑是否响应这个中断,如果不响应就可以继续运行。
//本例中,当接收到中断信号时,就会响应中断,这是人为编写的
if(Thread.currentThread().interrupted()){
break;
}
if(i % 100 == 0){
System.out.println(i + " "+Thread.currentThread().getName());
}
}
System.out.println("线程停止了");
}
public static void main(String[] args) throws InterruptedException {
RigthWayToStopThread r = new RigthWayToStopThread();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}

可以发现当这个线程可以响应到停止信号,就会停止运行。

  1. 当线程处于阻塞时,使用interrupt停止线程:
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
public class RigthWayToStopThread implements Runnable{

@Override
public void run() {
try {
for(int i = 0; i < 100000; i++){
if(Thread.currentThread().interrupted()){
break;
}
if(i % 10 == 0){
System.out.println(i + " "+Thread.currentThread().getName());
}
}
System.out.println("线程停止");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
RigthWayToStopThread r = new RigthWayToStopThread();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(50);
// Thread.sleep(500);
thread.interrupt();
}
}

程序执行结果:

这种情况就和第一点的一样了,线程正在运行的时候被停止

1
2
3
4
//打印i的值和线程名称...省略
线程停止

Process finished with exit code 0

第二种情况是由于使用sleep语句时需要try catch将这个异常捕捉,所以当使用Interrupt停止线程时,此时正处于sleep阻塞状态,这个interrupt信号会被catch住

1
2
3
4
5
6
7
//打印i的值和线程名称...省略
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at juc.thread.RigthWayToStopThread.run(RigthWayToStopThread.java:20)
at java.base/java.lang.Thread.run(Thread.java:834)

Process finished with exit code 0
  1. 每次迭代的过程都阻塞

把sleep语句放到for循环内,(阻塞时间大于每次的运行时间)每次阻塞的时候,如果有interrupt信号,都会被catch捕捉到异常。也不需要 **if(Thread.currentThread().interrupted()){ break; } **语句了

sleep的try catch 一旦响应中断,便会把中断标志位清除,回到没有被中断的状态。

4.2 处理中断的方法

  1. 优先选择:传递中断。catch了InterruptedExcetion之后,在方法签名中抛出异常,在底层方法中不能把异常吞了!!

    当run()方法调用了一个方法a(),而a中有try catch相关的内容捕获停止信号,当一个线程想要停止run()对应的线程,a()方法感知到了interrupt信号,会捕获异常,但是会把interrupt标志位清除,所以run()方法还是可以正常运行,a方法的catch打印了中断信息也不会被发现。

    正确的方法是a方法在方法签名中抛出这个异常,让run处理这个异常,可以是保存日志等等….

  2. 无法或不想传递中断:恢复中断

    在catch子句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便在后序的执行中,依然能够检查到刚才的中断。

  3. 不应屏蔽中断

4.3 哪些方法可以响应中断

响应中断指当中断信号发出时,这些方法可以感知到这个中断,并捕获。

wait() sleep() join() 等等

4.4 错误的停止方法

  1. stop,suspend,resume方法

    **stop()**来停止线程会导致线程运行一半突然停止,没办法完成一个基本单位的操作,会造成脏数据。 stop是不安全的。

  2. volatile设置boolean标记位

    无法处理长时间阻塞的情况

5.线程的生命周期

补充:当线程刚刚被唤醒时,由于唤醒它的线程持有这个锁,因此线程不能立刻获得到锁,因此会进入BLOCKED阶段,如果在wait阶段被中断,会直接进入终止状态。

5.1 NEW

初始状态,线程被构建,但是还没有调用start()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadSixStatus implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i+" "+Thread.currentThread().getName());
}
}

public static void main(String[] args) {
ThreadSixStatus threadSixStatus = new ThreadSixStatus();
Thread thread = new Thread(threadSixStatus);
System.out.println(thread.getState());
}
}

程序的输出结果是:

1
NEW

可见刚刚创建的线程的状态是NEW,没有start()启动线程,所以不会运行Runnable接口实现类的run()方法。

5.2 RUNNABLE(可运行的/运行中的)

运行状态,java线程将操作系统中的就绪运行两种状态笼统的称为“运行中”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadSixStatus implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i+" "+Thread.currentThread().getName());
}
}

public static void main(String[] args) {
ThreadSixStatus threadSixStatus = new ThreadSixStatus();
Thread thread = new Thread(threadSixStatus);
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getState());
}
}
1
2
3
4
5
6
7
8
9
10
NEW
RUNNABLE
0 Thread-0
1 Thread-0
2 Thread-0
......
707 Thread-0
RUNNABLE
......
999 Thread-0

thread.start();之后,线程就处于RUNNABLE状态,这时如果被分到了CPU,就运行,没有被分到CPU,就处于就绪状态,等会,当开始运行时,jvm会调用run()方法,输出上面的内容。

5.3 BLOCKED

阻塞状态,表示线程阻塞于锁。

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
public class ThreadSixStatus implements Runnable{
@Override
public void run() {
syn();
}

public static void main(String[] args) {
ThreadSixStatus threadSixStatus = new ThreadSixStatus();
Thread thread1 = new Thread(threadSixStatus);
thread1.start();
Thread thread2 = new Thread(threadSixStatus);
thread2.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1的状态:"+thread1.getState());
System.out.println("thread2的状态:"+thread2.getState());
}

public synchronized void syn(){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出的结果为:

1
2
thread1的状态:TIMED_WAITING
thread2的状态:BLOCKED

这是由于线程1先被启动,然后进入syn()方法中,就会处于睡眠10s的状态,由于main函数中获取线程1状态前睡眠10ms,所以当获取线程1的状态时,线程1一定处于睡眠,所以是TIMED_WAITING,而此时线程1拿着syn()方法的锁,线程2无法运行syn()方法,只能阻塞自身等待锁,所以是BLOCKED

5.4 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package juc.thread;

/**
* @Author qq
* @Date 2022/3/12
*/
public class ThreadSixStatus implements Runnable{

@Override
public void run() {
syn();
}

public static void main(String[] args) {
ThreadSixStatus threadSixStatus = new ThreadSixStatus();
Thread thread1 = new Thread(threadSixStatus);
thread1.start();
Thread thread2 = new Thread(threadSixStatus);
thread2.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1的状态:"+thread1.getState());
System.out.println("thread2的状态:"+thread2.getState());
System.out.println("-------------");
try {
Thread.sleep(1100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1的状态:"+thread1.getState());
System.out.println("thread2的状态:"+thread2.getState());
}

public synchronized void syn(){
try {
Thread.sleep(1000);
System.out.println("线程进入到wait等待");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

执行结果为:

1
2
3
4
5
6
7
thread1的状态:TIMED_WAITING
thread2的状态:BLOCKED
-------------
线程进入到wait等待
thread1的状态:WAITING
thread2的状态:TIMED_WAITING
线程进入到wait等待

这说明线程1先进入WAITING状态,然后会释放这个锁,线程2拿到锁后进入sleep,休眠时间过了之后也会进入WAITING状态,此时控制台显示程序并没有停止运行,注意框住的部分。

5.5 TIMED_WAITING

超时 / 计时等待状态,该状态不同于WAITING,它是可以在指定时间自行返回的。

看BLOCKED中的代码。

5.6 TERMINATED

终止状态,表示当前线程已经执行完毕。线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadSixStatus implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i+" "+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadSixStatus threadSixStatus = new ThreadSixStatus();
Thread thread = new Thread(threadSixStatus);
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getState());
}
}

1s钟程序一定执行完毕了,所以打印线程状态是TERMINATED

1
2
3
  ......
999 Thread-0
TERMINATED

阻塞状态:一般把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态。

6.Thread类的重要方法详解

6.1 sleep()

线程sleep的时候不释放lock以及synchronized的monitor锁,等sleep时间到了以后,线程正常结束后才释放锁。

sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间再执行,休眠期间如果被中断,会抛出异常并清除中断状态。

TimeUnit.SECONDS.sleep(1);Thread.sleep()更优雅

sleep()不需要放在同步代码块中。

6.2.join()

当有新的子线程加入时,子线程调用jion(),主线程会等子线程执行完毕后再往下运行,如果子线程不调用这个方法,当它还没执行完的时候,主线程就会继续执行。

当join()的时候主线程的状态是WAIT,一个线程去等待另一个线程

下面的代码就说明了主线程不一定会等子线程执行完再执行,因为start()方法启动线程,线程不一定立刻就获得到CPU资源。如果在子线程中slepp,那主线程应该是第一个执行完,然后才是子线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class join {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行");
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行");
}
}).start();
System.out.println(Thread.currentThread().getName()+"执行");
}
}

输出结果:(这个顺序具有随机性)

1
2
3
Thread-0开始执行
main执行
Thread-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
41
42
//如果加入了join()


public class join {
public static void main(String[] args) {
Thread t1 =new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"开始执行");
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"开始执行");
}
});

t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()+"执行");
}
}

输出结果:(一定是这个顺序)

1
2
3
Thread-1开始执行
Thread-0开始执行
main执行

当join()期间遇到中断是什么情况?

是主线程被中断,抛出异常。最好将异常抛出给子线程。

6.3 yield()

yield()的作用是让步,会让线程释放CPU时间片,但是线程的状态还是Runnable。

它能够让当前线程从“运行状态”进入到“就绪状态”,从而让其他等待线程获取执行权,但是不能保证在当前线程调用yield()之后,其他线程就一定能获得执行权,也有可能是当前线程又回到“运行状态”继续运行

7.Object类的重要方法详解

Object类是所有对象的父类,所有对象都可以调用以下的方法。

wait()和notify()必须放在synchronized代码块中,如果不通过同步块包住的话,JVM会抛出IllegalMonitorStateException异常。

7.1 wait()

某个线程必须拥有对象的monitor锁才可以调用wait()方法,并且这个线程调用后会进入WAITING阻塞状态。

一旦调用了wait方法会释放这个对象锁,该线程会进入休息室WaitSet,等待有人唤醒

什么情况下会被唤醒?

  1. 另一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程。
  2. 另一个线程调用这个对象的notifyAll()方法
  3. 过了wait(long timeout)规定的超时时间,但是如果传入0就是永久等待
  4. 线程遇到中断,即调用了interrupt(),发生中断

7.2 notify()

notify()只会唤醒单个正在阻塞的线程,这个线程会重新转为就绪状态,等待虚拟机分配CPU,如果有多个线程在等待,它只会选取其中一个去唤醒,选取规则由虚拟机去实现

7.3 notifyAll()

会唤醒所有阻塞的线程。


wait()和notify()相互作用

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
41
42
43
44
public class waitAndNotify{
private static Object object = new Object();

static class waitAndNotify1 implements Runnable{
@Override
public void run() {
synchronized (object){
System.out.println("线程" + Thread.currentThread().getName() + "获得了锁");
try {
System.out.println("线程" + Thread.currentThread().getName() + "将要阻塞,会释放锁");
object.wait();
System.out.println("线程" + Thread.currentThread().getName() + "再次获得了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

static class waitAndNotify2 implements Runnable{
@Override
public void run() {
synchronized (object){
System.out.println("线程" + Thread.currentThread().getName() + "得到了锁");
object.notify();
System.out.println("线程" + Thread.currentThread().getName() + "已经唤醒了一个阻塞线程,它可能一会就获得锁");
}
}
}

public static void main(String[] args) {
waitAndNotify1 w1 = new waitAndNotify1();
Thread thread1 = new Thread(w1);
thread1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitAndNotify2 w2 = new waitAndNotify2();
Thread thread2 = new Thread(w2);
thread2.start();
}
}

输出结果为:

1
2
3
4
5
线程Thread-0获得了锁
线程Thread-0将要阻塞,会释放锁
线程Thread-1得到了锁
线程Thread-1已经唤醒了一个阻塞线程,它可能一会就获得锁
线程Thread-0再次获得了锁

notify()和notifyAll()的区别

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
41
42
43
44
45
46
public class waitAndNotify{
private static Object object1 = new Object();
static class waitAndNotify1 implements Runnable{
@Override
public void run() {
synchronized (object1){
System.out.println("线程" + Thread.currentThread().getName() + "获得了object1锁");
try {
System.out.println("线程" + Thread.currentThread().getName() + "将要阻塞,会释放object1锁");
object1.wait();
System.out.println("线程" + Thread.currentThread().getName() + "再次获得了object1锁,将要运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

static class waitAndNotify2 implements Runnable{
@Override
public void run() {
synchronized (object1){
System.out.println("线程" + Thread.currentThread().getName() + "获得了object1锁");
object1.notifyAll();
System.out.println("线程" + Thread.currentThread().getName() + "已经唤醒object1对象锁的全部阻塞线程");
}
}
}

public static void main(String[] args) {
waitAndNotify1 w1 = new waitAndNotify1();
waitAndNotify1 w3 = new waitAndNotify1();
Thread thread1 = new Thread(w1);
thread1.start();
Thread thread3 = new Thread(w3);
thread3.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitAndNotify2 w2 = new waitAndNotify2();
Thread thread2 = new Thread(w2);
thread2.start();
}
}

输出结果:

1
2
3
4
5
6
7
8
线程Thread-0获得了object1锁
线程Thread-0将要阻塞,会释放object1锁
线程Thread-1获得了object1锁
线程Thread-1将要阻塞,会释放object1锁
线程Thread-2获得了object1锁
线程Thread-2已经唤醒object1对象锁的全部阻塞线程
线程Thread-0再次获得了object1锁,将要运行结束
线程Thread-1再次获得了object1锁,将要运行结束

entry set:入口集

wait set:等待集

7.4 使用wait()和notify()模拟生产者消费者模式

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class ProducerAndCustomer {
public static void main(String[] args) {
BlockedQueue queue = new BlockedQueue();
Producer p = new Producer(queue);
Customer c = new Customer(queue);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}

class Producer implements Runnable{

BlockedQueue queue;
public Producer(BlockedQueue queue){
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
queue.in();
}
}
}

class Customer implements Runnable{
BlockedQueue queue;
public Customer(BlockedQueue queue){
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
queue.take();
}
}
}

class BlockedQueue{
private int maxSize;
private List<String> list;
public BlockedQueue(){
maxSize = 10;
list = new ArrayList<>();
}
public synchronized void take(){
while(list.size() == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者拿到【"+list.get(0)+"】,仓库还剩"+(list.size() - 1)+"件");
list.remove(0);
this.notify();
}
public synchronized void in(){
while(list.size() == maxSize){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String num = String.valueOf(System.currentTimeMillis());
list.add(num);
System.out.println("生产者生产了【"+num+"】仓库里有"+list.size()+"件东西");
this.notify();
}
}

输出结果为:

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
生产者生产了【1647305874352】仓库里有1件东西
生产者生产了【1647305874352】仓库里有2件东西
生产者生产了【1647305874352】仓库里有3件东西
生产者生产了【1647305874352】仓库里有4件东西
生产者生产了【1647305874352】仓库里有5件东西
生产者生产了【1647305874352】仓库里有6件东西
生产者生产了【1647305874352】仓库里有7件东西
生产者生产了【1647305874352】仓库里有8件东西
生产者生产了【1647305874352】仓库里有9件东西
生产者生产了【1647305874352】仓库里有10件东西
仓库满啦..生产者等待
消费者拿到【1647305874352】,仓库还剩9
消费者拿到【1647305874352】,仓库还剩8
消费者拿到【1647305874352】,仓库还剩7
消费者拿到【1647305874352】,仓库还剩6
生产者生产了【1647305874352】仓库里有7件东西
生产者生产了【1647305874352】仓库里有8件东西
生产者生产了【1647305874352】仓库里有9件东西
生产者生产了【1647305874352】仓库里有10件东西
仓库满啦..生产者等待
.........
仓库空啦..消费者等待
生产者生产了【1647305874362】仓库里有1件东西
生产者生产了【1647305874362】仓库里有2件东西
生产者生产了【1647305874362】仓库里有3件东西
生产者生产了【1647305874362】仓库里有4件东西
消费者拿到【1647305874362】,仓库还剩3
消费者拿到【1647305874362】,仓库还剩2
消费者拿到【1647305874362】,仓库还剩1
消费者拿到【1647305874362】,仓库还剩0
仓库空啦..消费者等待
....

8.wait()、notify()、sleep()的异同

  • 相同点:阻塞、响应中断、清除中断标志位
  • 不同点:是否被包括在同步方法中、是否释放锁、是否必须指定时间、所属类是哪个

9.线程的各个属性

  1. 线程id的那个属性初始值是0,但是获取前会被加1,所以第一个线程也就是主线程main的id是1,后面的子线程依次增加。
  2. 线程名字可以通过Thread类中的setName方法来设置,也可以不设置,使用默认的Thread-number,number从0开始,每次加1
  3. 守护线程:服务于用户线程的,不影响jvm的退出。
  4. 线程优先级:java虚拟机中,有10个级别,默认是5。一般是将java虚拟机中的优先级映射到操作系统上的优先级,不同的操作系统优先级范围级别可能不同,所以我们不要随意改变优先级,否则导致映射后的优先级不符合我们的预期。优先级也可能会被操作系统改变

10.线程安全问题

10.1 共享的成员变量和静态变量

根据它们的状态是否能够改变,分两种情况

  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

10.2 死锁、活锁、饥饿

死锁:每个线程都不放弃自己的锁,同时也期望得到对方的锁。

死锁可以通过线程按照相同的顺序获取锁来解决,但是这样会产生饥饿。

死锁的例子,注意一下,object1和object2必须是static的变量。

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
41
42
43
44
45
46
47
48
49
50
51
public class dead implements Runnable {
int flag = 1;
//如果有数据需要被共享给所有对象使用时,那么就可以使用static修饰。
//因为静态成员变量是存储方法 区内存中,而且只会存在一份数据。
//非静态的成员变量是存储在堆内存中,有n个对象就有n份数据。
static Object object1 = new Object();
static Object object2 = new Object();
@Override
public void run() {
System.out.println(flag);
if(flag == 1){
synchronized (object1){
System.out.println(Thread.currentThread().getName() + "拿到了object1");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2){
System.out.println(Thread.currentThread().getName() + "拿到了object2");
}
}
}

if(flag == 0){
synchronized (object2){
try {
System.out.println(Thread.currentThread().getName() + "拿到了object2");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1){
System.out.println(Thread.currentThread().getName() + "拿到了object1");
}
}
}
}

public static void main(String[] args) {
dead d1 = new dead();
dead d2 = new dead();
d1.flag = 0;
d2.flag = 1;
Thread thread1 = new Thread(d1);
Thread thread2 = new Thread(d2);
thread1.start();
thread2.start();
}
}

死锁的例子:哲学家就餐问题

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Philosopher extends Thread {

chopsticks left;
chopsticks right;
String name;
public Philosopher(String name,chopsticks left,chopsticks right){
this.name = name;
this.right = right;
this.left = left;
}
@Override
public void run() {
while(true){
synchronized (left){
synchronized (right){
eat();
}
}
}
}
public void eat(){
System.out.println(name + "拿上筷子" + left.name + "," + right.name + "开始吃饭..");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "放下筷子" + left.name + "," + right.name + "开始思考..");
}

public static void main(String[] args) {
chopsticks c1 = new chopsticks("c1");
chopsticks c2 = new chopsticks("c2");
chopsticks c3 = new chopsticks("c3");
chopsticks c4 = new chopsticks("c4");
chopsticks c5 = new chopsticks("c5");
Philosopher p1 = new Philosopher("哲学家1",c1,c2);
Philosopher p2 = new Philosopher("哲学家2",c2,c3);
Philosopher p3 = new Philosopher("哲学家3",c3,c4);
Philosopher p4 = new Philosopher("哲学家4",c4,c5);
Philosopher p5 = new Philosopher("哲学家5",c5,c1);
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
}
}
class chopsticks{
String name;
public chopsticks(String name){
this.name = name;
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
哲学家3拿上筷子c3,c4开始吃饭..
哲学家1拿上筷子c1,c2开始吃饭..
哲学家3放下筷子c3,c4开始思考..
哲学家3拿上筷子c3,c4开始吃饭..
哲学家1放下筷子c1,c2开始思考..
哲学家5拿上筷子c5,c1开始吃饭..
哲学家5放下筷子c5,c1开始思考..
哲学家5拿上筷子c5,c1开始吃饭..
哲学家3放下筷子c3,c4开始思考..
哲学家5放下筷子c5,c1开始思考..

此时已经发生了死锁,线程处于BLOCKED状态。

10.3 活锁

活锁:两个线程互相改变对方的结束条件,最终谁也无法结束。都处于Runnable状态。

比如当两个线程同时改变一个共享变量,它的初始值是10,并且线程1结束运行的条件是这个变量大于0,线程2运行的结束条件是这个变量小于20,

10.4 饥饿

饥饿:一个线程由于优先级太低,始终得不到CPU调度执行,也不能结束。

10.5 对象发布和初始化

发布是让其他类可以访问到这个变量,就是把这个变量发布。

逸出是不让其他类可见的变量被其他类访问,或者改变它的值。


Java多线程-线程核心基础
https://vickkkyz.fun/2022/03/24/Java/JUC/线程八大核心基础/
作者
Vickkkyz
发布于
2022年3月24日
许可协议