Java多线程

1. 进程、线程、管程

直观感受:windows任务管理器界面的exe程序就是一个个进程。
进程是受操作系统管理的基本运行单元。在windows中启动一个JVM虚拟机就相当于创建一个进程。线程可以理解为在进程中独立运行的子任务。进程负责向操作系统申请资源,在一个进程中,多个线程可以共享进程中相同的内存或文件资源。
管程:Monitor,更常见的是直接将它称为“锁”。Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程来实现的。

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

当系统中出现了阻塞,可以考虑利用多线程来提高程序运行效率。但不要为了使用多线程而使用多线程,要根据实际场景决定。

2. 并行与并发

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

3. 同步与异步

如果数据将在线程间共享,则必须进行同步存取。例如正在写的数据可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步往往更有效率。

同步会阻塞,异步非阻塞。使用多线程其实就是在使用异步。但是如果多个线程同时操作共享数据,此时需要加锁来保证线程安全,则是同步。

4.实现多线程的四种方式

4.1继承Thread类

继承Thread类重写run方法

public class ThreadA extends Thread {

    @Override
    public void run(){
        System.out.println("ThreadA多线程启动了");
    }
}
public class TestA {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

ThreadA多线程启动了

Thread类实现了Runnable接口,使用继承Thread实现多线程的最大局限在于java不支持多继承。

4.2 实现Runnable接口

实现Runnable接口并重写run方法。
在Thread的构造方法中,形参有的是Runnable。可以通过构造Thread对象时传入Runnable,然后调用start方法来启动多线程。

public class ThreadB implements Runnable {
    @Override
    public void run() {
        System.out.println("ThreadB多线程启动了");
    }
}
public class TestB {
    public static void main(String[] args) {
        ThreadB threadB = new ThreadB();
        new Thread(threadB).start();
    }
}

通过实现Runnable接口的方式,间接地实现了多继承。因为可以在继承一个类的同时实现多个接口。

4.3 实现Callable接口
4.4 线程池

5.启动一个线程是用run()还是start()?

启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。直接调用run()方法就是普通的对象调用方法的执行,并不是使用开启新线程来执行的。

执行start()的顺序并不代表执行run()的顺序,调用start()方法仅仅表示此线程已准备就绪,随时可以调用此线程的run()方法。故线程执行具体的任务具有随机顺序执行的结果。

一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。

start方法源码分析
start方法底层调用的是 start0()方法,而start0()是一个native方法。Java语言本身底层就是C++语言。
1)openjdk8\jdk\src\share\native\java\lang thread.c
2)openjdk8\hotspot\src\share\vm\prims jvm.cpp
3)openjdk8\hotspot\src\share\vm\runtime thread.cpp
java线程是通过start的方法启动执行的,主要内容在native方法startO中,openjdk的写JNI()一般是一一对应的,Thread.java对应的就是Thread.cstart0其实就是JVM_StartThread。此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。

SUN公司发布的Java 本地接口(JNI)提供了将Java与C/C++、汇编等本地代码集成的方案,该规范使得在 Java 虚拟机内运行的 Java 代码能够与其它编程语言互相操作,包括创建本地方法、更新Java对象、调用Java方法,引用 Java类,捕捉和抛出异常等,也允许 Java代码调用 C/C++或汇编语言编写的程序和库。作为一个标准程序接口,它没有对底层 Java虚拟机的实现施加任何限制。

6.什么情况下才会存在线程不安全的问题?

多个线程同时对同一个对象的同一个实例变量进行操作

三个条件,缺一不可:多线程、同一对象的同一个实例变量、操作

此处的操作指的是写操作,多个线程同时读并不会造成线程不安全问题。

7.sleep() 和 wait() 有什么区别?

1)、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
2)、关于锁的释放 wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!
3)、使用的范围是不同的 :sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

8.多线程程序的优点:

提高应用程序的响应。对图形化界面更有意义,可增强用户体验

提高计算机系统CPU的利用率

改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

9.什么时候用多线程?

程序需要同时执行两个或多个任务

程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等

需要一些后台运行的程序时

9.Thread类的常用方法

long getId(): 获取线程唯一标识

void start(): 启动线程,并执行对象的run()方法

run(): 线程在被调度时执行的操作

String getName(): 返回线程的名称

void setName(String name):设置该线程名称

static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。可得到代码段正在被哪个线程调用。

static void yield(): 线程让步暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程若队列中没有同优先级的线程,忽略此方法

**join():**当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止 低优先级的线程也可以获得执行

static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。抛出InterruptedException异常。

stop(): 强制线程生命期结束,不推荐使用

**boolean isAlive():**返回boolean,判断线程是否还活着。线程已经启动且尚未终止的状态即为活动状态。如果线程处于正在运行或准备就绪的状态,就认为线程是存活的。

**void interrupt()😗*中断线程,仅仅是在当前线程中做了一个停止的标记,并不是真正停止线程。

**boolean interrupted()😗*测试currentThread是否已经中断(执行后清除状态位置为false)

**boolean isInterrupted()😗*测试currentThread是否已经中断(不清楚状态位)

10.中断线程

单单凭interrupt无法立刻终止线程,需要以下两种组合拳才能终止线程。

10.1 interrupt+状态位+return 来终止
public class ThreadA extends Thread {

    @Override
    public void run(){
        System.out.println("ThreadA多线程启动了");
        for (int i = 1; i < 500000; i++) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("ThreadA中断:"+i);
                return;
            }
            System.out.println(i);
        }
    }
}
public class TestA {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        threadA.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.interrupt();
    }
}

10.2 interrupt+状态位+异常 来终止
public class ThreadB implements Runnable {
    @Override
    public void run() {
        System.out.println("ThreadB多线程启动了");
        try {
            for (int i = 1; i < 500000; i++) {
                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException("ThreadB中断");
                }
                System.out.println(i);
            }
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
}
public class TestB {
    public static void main(String[] args) {
        ThreadB threadB = new ThreadB();
        Thread thread = new Thread(threadB);
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

虽然使用return较抛异常在代码结构上可以更加简洁,但还是建议使用抛异常法,因为在catch块中可以对异常进行统一处理。

interrupt和sleep方法碰到一起就会出现异常,sleep状态执行interrupt或先执行interrupt再sleep.

10.3 interrupt+标志位 来终止
public class ThreadC implements Runnable {

    private volatile boolean flag = true;

    @Override
    public void run() {
        System.out.println("ThreadB多线程启动了");
        int i = 0;
        while (flag && !Thread.currentThread().isInterrupted()) {
            i++;
        }
        System.out.println(i);
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
public class TestC {
    public static void main(String[] args) {
        ThreadC threadC = new ThreadC();
        Thread thread = new Thread(threadC);
        thread.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
        threadC.setFlag(false);
    }
}

11. 线程的优先级等级

操作系统会分出一个个时间片,线程会分到若干时间片,当时间片用完就会发生线程的调度,并等着下次的分配。线程优先级就是决定线程需要多或少分配一些处理器资源的线程属性。

线程优先级不能作为程序正确执行的保证。因为操作系统完全可以不用理会Java对线程优先级的设定。

故而线程的优先级属实鸡肋,了解即可。

取值范围:1-10,值越大,线程级别越高
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5

涉及的方法

**getPriority():**返回线程优先值

**setPriority(int newPriority):**改变线程的优先级

线程创建时继承父线程的优先级(可继承)

低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

12. 用户线程、守护线程

Java有两种线程,一种是用户线程,一种是守护线程。一般情况下不做特别说明配置,默认都是用户线程。它是系统的工作线程,它会完成这个程序需要完成的业务操作。

**thread.setDaemon(true)**可以把一个用户线程变成一个守护线程。

守护线程主要是用作程序后台调度以及支持性工作。为其它线程服务的,在后台默默地完成一些系统性的服务,比如垃圾回收线程。
守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的
业务操作已经结束了,系统可以退出了。所以假如当系统只剩下守护线程的时候,java虚拟机会自动退出。

Daemon([ˈdiːmən])属性需要在启动线程之前设置,不能在启动线程之后设置。在构建Daemon线程时,不能依靠finally块中的代码来确保清理资源的逻辑,因为当java虚拟机关闭时,守护线程中的finally块中的代码并不一定会执行。

Java垃圾回收就是一个典型的守护线程。

若JVM中都是守护线程,当前JVM将退出。

形象理解:坦克护鹰。守护守护,谁守护谁?坦克守护鹰,则坦克是守护线程,鹰是用户线程,当鹰挂了,坦克也立刻挂了,但坦克挂了,鹰立刻不会挂

13. 过期的suspend()、resume()、stop()

suspend()、resume()、stop()方法完成了线程的暂停、恢复、终止,但是这些方法都被标记为过时,所以不推荐使用。

过期原因:
stop()属于暴力停止,在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程释放资源的机会,导致程序处于一种不确定状态下。就像是直接枪毙犯人,也没有给犯人交代临终遗言的机会。

在调用suspend()方法后,线程不会释放已经占有的资源(比如锁),而是占着资源进入失眠状态,这种容易引发死锁问题。resume比suspend先执行,也会发生死锁。

14. 线程的六大状态

新建(NEW): 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态(未调用start()方法前)

**运行中(RUNNABLE):**处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。在任意时刻,一个就绪的线程可能在运行也可能未运行。Java线程将操作系统的就绪和运行中两种状态笼统地成为运行中。

**等待(WAITING)😗*等待另一个线程通知调度器一个条件时,则进入等待状态。Object.wait();Condition.await();Thread.join()方法

**计时等待(TIMED_WAITING)😗*这一状态一直保持到超时或者收到适当的通知。Thread.sleep(),Object.wait();Lock.tryLock();Condition.await();

**阻塞(BLOCKED):**当一个线程试图获取另一个线程的锁的时候,则进入阻塞状态。阻塞状态是线程阻塞在进入synchronized同步方法或同步代码块获取锁时的状态。但是阻塞在concurrent包中的Lock接口的线程状态却是等待状态,因为Lock接口对于阻塞的实现使用了LockSupport类中的方法。

**死亡(TERMINATED):**线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
在这里插入图片描述Java线程状态转换图,图片来源于方腾飞老师的《Java并发编程的艺术》。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值