多线程之基础

写在前面

本文看下多线程基础相关内容。

多线程之基础_第1张图片

1:线程基础分析

1.1:摩尔定律失效

戈登.摩尔,英特尔公司的创始人之一,其发现了一个计算机的发展规律,即,处理器的性能每24个月就会翻一倍,这就是摩尔定律,开始这个预测是正确的,但是后来处理器性能的提升速度严重放缓,这就是摩尔定律失效,既然单个CPU已经无法满足业务需求,那么就多个CPU,也就是多核,有了多核,自然就有了多线程。有了多核,多线程之后,就可以进行并行计算,提高更高的计算能力。

目前多核CPU有两种架构,共享内存架构,即多个CPU共享同一块内存,如下图:

多线程之基础_第2张图片

多个CPU通过消息总线BUS,访问同一块内存,这种架构方式存在如下问题:

1:多个CPU通过同一个BUS存取数据,导致BUS拥堵
2:多个CPU访问同一块内存获取数据,导致内存资源争抢严重

这个问题可以通过以下这种架构来解决,即分片,多个内存,分配给一个或多个CPU使用,如果某CPU需要访问其他CPU的内存的话则可以通过router路由完成,这种架构叫做非一致性内存访问架构,如下图:

多线程之基础_第3张图片

1.2:线程创建过程分析

线程最终的创建还是需要依赖于底层的操作系统,所以Java中的线程,有三个层面的概念,Java代码层面,jvm层面,操作系统层面,参考下图:

多线程之基础_第4张图片

解释如下:

1:创建完Thread对象后调用其start方法
2,3:JVM创建JVM层面的JavaThread对象,然后通过操作系统创建操作系统层次的OSThread对象
4:在JVM的虚拟机栈上给该线程分配内存
5:给该线程分配TLAB
6:操作系统调度执行线程,自动执行thread的run方法逻辑
7:run方法执行完毕,操作系统结束线程

1.3:一个简单的例子

1.3.1:什么是守护线程

先看一个问题,jvm什么时候会退出,看老外写的原版表述The Java Virtual Machine exits when the only threads running are all daemon threads.,即当jvm中运行的线程都是守护线程时,jvm会退出,如下:

public class NoneDaemonThreadTest {

    public static void main(String[] args) {

        Thread noneDaemonThread = new Thread(() -> {
            while (true) {
                try {
//                    Thread.sleep(1);
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("hi,i am still running...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        noneDaemonThread.start();

        try {
//            Thread.sleep(5);
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main thread exits...");

    }
}

运行:

多线程之基础_第5张图片

如何实现在main函数执行完毕后让jvm退出呢?只需要设置setDaemon(true),就行了,如下:

多线程之基础_第6张图片

运行,如下:

多线程之基础_第7张图片

当然我们也可以添加一个jvm的钩子来确定jvm确实是退出了,如下:

多线程之基础_第8张图片

那么守护进程有什么用呢?如果我们希望执行一些清理,信息收集等类型的工作时,但又不希望因为其存在而影响jvm的正常退出,因为当没有其他的非守护进程存在时,其存在也就没有意义了,最典型的,比如GC线程,就是守护线程,其是为了清理因为非守护线程运行而生成的对象而存在的。如果是工作中有类似的需求,我们也要尽量使用非守护进程,不要使用守护进程,避免因为其影响JVM的正常退出。

1.4:Thread和Runnable什么关系❓

我们创建线程的时候可以给java.lang.Thread一个java.lang.Runnable类一个参数,像这样:

new Thread(new Runnable() {
    @Override
    public void run() {
    }
})

也可以直接继承java.lang.Thread,像这样:

new Thread() {
    @Override
    public void run() {
        super.run();
    }
}

可以这么写的原因是java.lang.Thread类是java.lang.Runnable接口的实现类,如下:

public
class Thread implements Runnable {}

JVM在执行run方法时,会优先执行thread类本身的run方法,如果thread本身没有实现run方法,会执行构造函数中Runnable接口子类的run方法。

2:Thread常用属性和方法

2.1:name属性

线程名称,平时么有用,但是当排查导致问题的线程时会比较有用。

2.2:deamon属性

是否为守护线程,true是,false不是。守护线程本文前面内容已经分析过了。

2.3:Runnable target属性

只能通过Thread构造函数传入。

2.4:start方法

启动线程,之后进入可运行状态,等待操作系统调度执行。

2.5:wait/notify方法

先看个图:

多线程之基础_第9张图片

1:如果线程执行时,被锁阻塞,则进入同步队列
2:如果线程正常获取锁,在同步代码块内部调用对象的wait方法,则该线程进入对象的等待队列
3:如果线程正常获取锁,在同步代码内部调用对象的notify/notifyAll方法,则等待队列线程进入到同步队列,待锁释放后争抢锁
4:如果线程正常获取锁,并且同步代码块正常执行完毕,则同步队列中的线程争抢锁,最终哪个线程获取到锁,不确定

调用某对象wait方法,必须持有该对象的对象锁,调用后当前线程进入waiting状态,并且进入对象的等待队列,等待notify/notifyAll方法唤醒,执行后释放对象锁,进入对象的等待队列,等待notify/notifyAll方法唤醒。另外调用wait时如果线程没有持有该对象的对象锁则会抛出IllegalMonitorException,notify/notifyAll方法如果是没有持有当前对象的对象锁,同样会抛出java.lang.IllegalMonitorException,这点使用的时候要特别小心。

锁对象和线程可参考下图:

多线程之基础_第10张图片

wait方法会让出CPU,并释放锁,不同于sleep,sleep只会让出CPU时间片,但并不会释放锁,所以说其他线程依然无法进入同步代码块

2.6:join方法

底层执行的wait方法,如下:

public final synchronized void join(long millis)
    throws InterruptedException {
        ...
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                ...
                wait(delay);
            }
        }
    }

假定线程A在线程B中执行join方法,即A.join,则线程B会进入到线程A的等待队列,在线程A执行完毕后,底层会自动调用线程A的notifyAll方法,即A.notifyAll,则线程B从线程A的等待队列进入到同步队列,进而获取锁继续执行。参考如下代码。

package dongshi.daddy;

import java.util.concurrent.TimeUnit;

public class Huohuo {
    public static void main(String[] args) throws Exception {
        Thread task = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("runnable content...");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        task.start();
        task.join();
        System.out.println("main run end...");
    }
}

运行,主线程等待task线程执行完毕才继续执行,如下:

runnable content...
main run end...

执行过程分析:

多线程之基础_第11张图片

2.7:本地静态currentThread

获取当前上线文正在执行的线程。

3:线程状态分析

本部分看下线程可能的状态,以及会改变线程状态的操作。

3.1:线程可能的状态

多线程之基础_第12张图片

3.2:会改变线程状态的操作

3.2.1:sleep

如:

void fn() {
    ...
    Thread.sleep(time);
    ...
}

让出CPU,线程从RUNNABLE装填变为TIME_WATING状态,作用是给其他线程使用CPU的机会,当睡眠到时则自动进入RUNNABLE状态,等待操作系统调度。需要注意该操作不会释放对象锁。

3.2.2:yield

如:

void fn() {
    ...
    Thread.yield();
    ...
}

Thread类的静态本地方法,会让出CPU时间片,从RUNNING状态重新进入就绪状态,即会立即开始争抢CPU时间片。

3.2.3:join/join(time)

如:

void fn() {
    ...
    Thread t = new Thread();
    t.join();
    ...
}

在当前线程调用某些线程t的join方法,则当前线程会等待线程t执行完毕之后,由底层调用notify方法唤醒,重新进入到可运行状态。不会释放当前线程的对象锁,但是会释放线程t的对象锁,因为底层调用的其实是t.wait();

3.2.4:wait/wait(time)

java.lang.Object类的本地方法,当还行obj.wait后,线程会释放obj的对象锁,并进入obj的等待队列。wait到时或者是有notify/notifyAll方法唤醒,重新变为就绪状态,开始争抢CPU时间片。

3.2.5:notify/notifyAll

唤醒在对象的等待队列中处于WAITING,TIMED_WAITING状态的线程,notify唤醒随机的一个,notifyAll唤醒所有的线程,进入就绪状态。

4:线程的中断异常处理

如果我们想要中断一个线程的执行,该怎么做?详细你首先会想到Thread的实例方法interrupt,但该方法真的可以吗?如下代码:

public class Huohuo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println();
            }
        });
        t1.start();
        t1.interrupt();
    }
}

运行后你会发现,jvm进程依然在运行,也就是t1.interrupt();程序的执行对于t1线程毫无作用,其实该方法只有当执行了Object.wait,Thread.sleep(),Thread.join而进入到WATIING,TIMED_WAITING状态后,才会有效果,并且在线程的run方法内会抛出java.lang.InterruptedException,这是一个非运行时异常,需要try住,修改如下:

public class Huohuo {
    static Object lock1 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println();
                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        System.out.println("中断线程t1");
        t1.interrupt();
    }
}

运行,如下:

中断线程t1

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at dongshi.daddy.Huohuo.lambda$main$0(Huohuo.java:10)
	at java.lang.Thread.run(Thread.java:748)

也就是说想要中断线程,必须先让想要中断的线程处于WAITING,TIMED_WAITING状态。具体可参考下图:

多线程之基础_第13张图片

写在后面

参考文章列表

面试官: 谈谈什么是守护线程以及作用 ? 。

Java等待唤醒机制wait/notify深入解析 。

你可能感兴趣的:(Java高级开发进阶教程,jvm,java,算法)