写点什么

Java 高级特性之多线程

作者:Java高工P7
  • 2021 年 11 月 11 日
  • 本文字数:8914 字

    阅读完需:约 29 分钟


1 基本概念


=========================================================================


1.1 程序、进程与线程




  • 程序(program):程序是为完成特定任务、用某种语言编写的一 组指令的集合 。即指一段静态的代码,静态对象。

  • 进程(process):进程是程序的一次执行过程,或是正在运行的一个程序 。是 一个动态的过程 :有它自身的产生、存在和消亡的过程,即生命周期。进程是作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域。

  • 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 若 一个进程同一时间并行执行多个线程,就是支持多线程的;

  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器 ( pc),线程切换的开销小;

  • 一个进程中的多个线程共享相同的内存单元/内存地址空间→它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。


1.2 内存结构中进程与线程的关系




其中每个线程拥有自己独立的栈和程序计数器,其中多个线程共享同一个进程中的结构:方法区、堆。



1.3 线程的生命周期





1.4 并行与并发




【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码




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

  • 并发:一个 CPU( 采用时间片)同时执行多个任务。


1.5 多线程的使用场景




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

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

  • 需要一些后台运行的程序。


一个 Java 应用程序其实至少三个线程:main()主线程、gc()垃圾回收线程和异常处理线程。


2 创建多线程的四种方法


===============================================================================


创建多线程的方式有四种,分别是:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池。


四种方式的差异如下:


| 创建多线程的方式 | 特征 |


| --- | --- |


| 继承 Thread 类 | 使用时每次都要创建 Thread 类的子类的对象 |


| 实现 Runnable 接口 | 使用时创建实现类的对象,然后将此对象作为参数传递到 Thread 类的构造器中,从而创建 Thread 类的对象。相比于继承 Thread 类,1)实现的方式没有类的单继承性的局限性;2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 |


| 实现 Callable 接口 | 相比于实现 Runnable 接口,1)call()可以有返回值的;2)call()可以抛出异常,被外面的操作捕获,获取异常的信息。而之前的方法只能是 try catch 捕获异常;3)Callable 是支持泛型的。 |


| 使用线程池 | 用从学校到西湖举例,前三种方法创建多线程就是每次去都要造一辆自行车,骑到西湖后就把自行车销毁,而线程池的方法就是搭乘公共交通去西湖。因此,这一种是最常用的方法,好处是:1)提高响应速度(减少了创建新线程的时间);2)降低资源消耗(重复利用线程池中线程,不需要每次都创建);3)便于线程管理。 |


下面用代码实例来讲解每一种方法,这样更容易理解。


3 实现 Runnable 接口


=================================================================================


3.1 利用 Runnable 实现基础实现(无线程同步)




/**


  • 需求:多线程的创建的方式二:实现 Runnable 接口

  • 创建一个实现了 Runnable 接口的类

  • 实现类去实现 Runnable 中的抽象方法:run()

  • 创建实现类的对象

  • 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象

  • 通过 Thread 类的对象调用 start()

  • 比较创建线程的两种方式。

  • 开发中:优先选择:实现 Runnable 接口的方式

  • 原因:1. 实现的方式没有类的单继承性的局限性

  • 联系:public class Thread implements Runnable

  • 相同点:两种方式都需要重写 run(),将线程要执行的逻辑声明在 run()中。


*/


public class BasicRunnable {


public static void main(String[] args) {


//3. 创建实现类的对象


Myhread myhread = new Myhread();


//4. 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象


Thread t1 = new Thread(myhread);


t1.setName("线程 1");


//5. 通过 Thread 类的对象调用 start():① 启动线程 ②调用当前线程的 run()-->调用了 Runnable 类型的 target 的 run()


t1.start();


//再启动一个线程,遍历 100 以内的偶数


Thread t2 = new Thread(myhread);


t2.setName("线程 2");


t2.start();


//如下操作仍然是在 main 线程中执行的。


for (int i = 0; i < 100; i++) {


if(i % 2 == 0){


System.out.println(Thread.currentThread().getName() + ":" + i );


}


}


}


}


//1. 创建一个实现了 Runnable 接口的类


class Myhread implements Runnable{


//2. 实现类去实现 Runnable 中的抽象方法:run()


@Override


public void run() {


for (int i = 0; i < 100; i++) {


if(i % 2 == 0){


System.out.println(Thread.currentThread().getName() + ":" + i);


}


}


}


}


该程序运行后可以发现,子线程和主线程的运行顺序是随机的,这非常不好,因为当多个线程同时对一个共享数据进行操作时,会导致操作的不完整性,会破坏数据,从而使得程序不稳定。


如以下这个实例 BadTicket,模拟火车站售票程序,开启三个窗口售票。会发现 ticket 会有重复,而且还可能会负值,这是因为 t1、t2、t3 都进入了 if 判断语句,而没有走到 ticket- -。


class Window1 implements Runnable{


private int ticket = 100;


@Override


public void run() {


while(true){


if(ticket > 0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


ticket--;


}else{


break;


}


}


}


}


public class BadTicket {


public static void main(String[] args) {


Window1 w = new Window1();


Thread t1 = new Thread(w);


Thread t2 = new Thread(w);


Thread t3 = new Thread(w);


t1.setName("窗口 1");


t2.setName("窗口 2");


t3.setName("窗口 3");


t1.start();


t2.start();


t3.start();


}


}


所以一方面我们会通过 synchronized 来同步线程,但其中有两种方式,一种是同步代码块,另一种是同步方法,这两种方式有些区别,我将分两段代码来介绍。另一方面,我们会使用 Lock 锁来解决这个问题。


3.2 利用 Runnable 实现线程同步(synchronized 同步代码块)




内容见注释,在代码中学习是最好的。


/**


  • 需求:创建三个窗口卖票,总票数为 100 张。使用同步代码块解决实现 Runnable 接口的方式的线程安全问题

  • 1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题

  • 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

  • 3.如何解决:当一个线程 a 在操作 ticket(共享数据)的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他

  • 4.在 Java 中,我们通过同步机制,来解决线程的安全问题。

  • 5.同步的方式,解决了线程的安全问题。---好处

  • 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 ---局限性

  • JDK5.0 之前

  • 方式一:同步代码块

  • synchronized(同步监视器){

  • }

  • 说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。

  • 方式二:同步方法。


*/


public class SynchroRunnable {


public static void main(String[] args) {


Window1 w = new Window1();


Thread t1 = new Thread(w);


Thread t2 = new Thread(w);


Thread t3 = new Thread(w);


t1.setName("窗口 1");


t2.setName("窗口 2");


t3.setName("窗口 3");


t1.start();


t2.start();


t3.start();


}


}


class Window1 implements Runnable{


private int ticket = 100;


// Object obj = new Object();只需放在 run()方法的外面即可


@Override


public void run() {


// Object obj = new Object(); 放在这个地方就不安全了,要放在 run()方法的外面,因为每个线程都有自己的一把锁


while(true){


//此时的 this:唯一的 Window1 的对象或者使用一个 Object 对象的方法


synchronized (this){


if (ticket > 0) {


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


ticket--;


} else {


break;


}


}


}


}


}


3.3 利用 Runnable 实现线程同步(synchronized 同步方法)




/**


  • 利用同步方法的方式来解决线程安全


*/


public class SynchroMethodRunnable {


public static void main(String[] args) {


Window w = new Window();


Thread t1 = new Thread(w);


Thread t2 = new Thread(w);


Thread t3 = new Thread(w);


t1.setName("窗口 1");


t2.setName("窗口 2");


t3.setName("窗口 3");


t1.start();


t2.start();


t3.start();


}


}


class Window implements Runnable {


private int ticket = 100;


@Override


public void run() {


while (true) {


show();


}


}


//默认同步监视器:this


private synchronized void show(){


if (ticket > 0) {


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


ticket--;


}


}


}


3.4 利用 Runnable 实现线程同步(Lock 锁)



3.4.1 Lock 锁定义

  • 从 JDK 5.0 开始,Java 提供了更强大的线程同步机制 通过显式定义同步锁对象来实现同步,同步锁使用 Lock 对象充当 。

  • java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象 。

  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在 实现线程安全的控制中,比较常用的是 ReentrantLock 可以显式加锁、释放锁。

3.4.2 代码实例

import java.util.concurrent.locks.ReentrantLock;


/**


  • 解决线程安全问题的方式三:Lock 锁 --- JDK5.0 新增

  • 面试题:synchronized 与 Lock 的异同?

  • 相同:二者都可以解决线程安全问题

  • 不同:synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器

  • 2.优先使用顺序:

  • Lock ? 同步代码块(已经进入了方法体,分配了相应资源) ? 同步方法(在方法体之外)


*/


public class Lock {


public static void main(String[] args) {


Window3 w = new Window3();


Thread t1 = new Thread(w);


Thread t2 = new Thread(w);


Thread t3 = new Thread(w);


t1.setName("窗口 1");


t2.setName("窗口 2");


t3.setName("窗口 3");


t1.start();


t2.start();


t3.start();


}


}


class Window3 implements Runnable{


private int ticket = 100;


//1.实例化 ReentrantLock


private ReentrantLock lock = new ReentrantLock();


@Override


public void run() {


while(true){


try{//2.调用锁定方法 lock()


lock.lock();


if(ticket > 0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);


ticket--;


}else{


break;


}


}finally {


//3.调用解锁方法:unlock()


lock.unlock();


}


}


}


}


3.5 synchronized 同步与 Lock 锁的区别




相同点:二者都可以解决线程安全问题。


不同点


  • synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock 是显式锁(需要手动开启和关闭锁), synchronized 是隐式锁,出了作用域自动释放;

  • Lock 只有代码块锁, synchronized 有代码块锁和方法锁;

  • 使用 Lock 锁, JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。


使用的优先顺序:Lock 锁、synchronized 同步代码块、synchronized 同步方法。


同步虽然解决了线程的安全问题,但其实操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。


3.6 死锁问题



3.6.1 定义

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。


  • 出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续

  • 我们使用同步时,要避免出现死锁。

3.6.2 解决办法

  1. 专门的算法、原则

  2. 尽量减少同步资源的定义

  3. 尽量避免嵌套同步

3.6.3 死锁的实例

//死锁的演示;死锁的增强版


class A {


public synchronized void foo(B b) { //同步监视器:A 类的对象:a


System.out.println("当前线程名: " + Thread.currentThread().getName()


  • " 进入了 A 实例的 foo 方法"); //


System.out.println("当前线程名: " + Thread.currentThread().getName()


  • " 企图调用 B 实例的 last 方法"); // ③


b.last();


}


public synchronized void last() {//同步监视器:A 类的对象:a


System.out.println("进入了 A 类的 last 方法内部");


}


}


class B {


public synchronized void bar(A a) {//同步监视器:b


System.out.println("当前线程名: " + Thread.currentThread().getName()


  • " 进入了 B 实例的 bar 方法"); // ②


System.out.println("当前线程名: " + Thread.currentThread().getName()


  • " 企图调用 A 实例的 last 方法"); // ④


a.last();


}


public synchronized void last() {//同步监视器:b


System.out.println("进入了 B 类的 last 方法内部");


}


}


public class DeadLock implements Runnable {


A a = new A();


B b = new B();


public void init() {


Thread.currentThread().setName("主线程");


// 调用 a 对象的 foo 方法


a.foo(b);


System.out.println("进入了主线程之后");


}


@Override


public void run() {


Thread.currentThread().setName("副线程");


// 调用 b 对象的 bar 方法


b.bar(a);


System.out.println("进入了副线程之后");


}


public static void main(String[] args) {


DeadLock dl = new DeadLock();


new Thread(dl).start();


dl.init();


}


}


4 继承 Thread 类


==============================================================================


4.1 Thread 的方法




/**


  • 测试 Thread 中的常用方法:

  • start():启动当前线程;调用当前线程的 run()

  • run(): 通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中

  • currentThread():静态方法,返回执行当前代码的线程

  • getName():获取当前线程的名字

  • setName():设置当前线程的名字

  • yield():释放当前 cpu 的执行权

  • join():在线程 a 中调用线程 b 的 join(),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完以后,线程 a 才

  • stop():已过时。当执行此方法时,强制结束当前线程。

  • sleep(long millitime):让当前线程“睡眠”指定的 millitime 毫秒。在指定的 millitime 毫秒时间内,当前

  • isAlive():判断当前线程是否存活

  • 线程的优先级:

  • MAX_PRIORITY:10

  • MIN _PRIORITY:1

  • NORM_PRIORITY:5 -->默认优先级

  • 2.如何获取和设置当前线程的优先级:

  • getPriority():获取线程的优先级

  • setPriority(int p):设置线程的优先级

  • 说明:高优先级的线程要抢占低优先级线程 cpu 的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下

  • 被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。


*/


public class ThreadMethod {


public static void main(String[] args) {


HelloThread h1 = new HelloThread("Thread:1");


h1.setName("线程一");


//设置分线程的优先级


h1.setPriority(Thread.MAX_PRIORITY);


h1.start();


//给主线程命名


Thread.currentThread().setName("主线程");


Thread.currentThread().setPriority(Thread.MIN_PRIORITY);


for (int i = 0; i < 100; i++) {


if(i % 2 == 0){


System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);


}


}


System.out.println(h1.isAlive());


}


}


class HelloThread extends Thread{


@Override


public void run() {


for (int i = 0; i < 100; i++) {


if(i % 2 == 0){


System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);


}


}


}


public HelloThread(String name){


super(name);


}


}


4.2 利用 Thread 实现基础实现(无线程同步)




/**


  • 需求:多线程的创建的方式一:继承于 Thread 类

  • 步骤:

  • 创建一个继承于 Thread 类的子类

  • 重写 Thread 类的 run() --> 将此线程执行的操作声明在 run()中

  • 创建 Thread 类的子类的对象

  • 通过此对象调用 start()

  • 说明各个子线程和主线程的顺序是随机的★


*/


public class BasicThread {


public static void main(String[] args) {


//3. 创建 Thread 类的子类的对象


MyThread t1 = new MyThread();


//4.通过此对象调用 start():①启动当前线程 ② 调用当前线程的 run()


t1.start();


MyThread t2 = new MyThread();


t2.start();


//如下操作仍然是在 main 线程中执行的。


for (int i = 0; i < 100; i++) {


if(i % 2 == 0){


System.out.println(Thread.currentThread().getName() + ":" + i );


}


}


}


}


//1. 创建一个继承于 Thread 类的子类


class MyThread extends Thread {


//2. 重写 Thread 类的 run()


@Override


public void run() {


for (int i = 0; i < 100; i++) {


if(i % 2 == 0){


System.out.println(Thread.currentThread().getName() + ":" + i);


}


}


}


}


4.3 利用 Thread 实现线程同步(synchronized 同步代码块)




/**


  • 需求:创建三个窗口卖票,总票数为 100 张。使用同步代码块解决实现 Runnable 接口的方式的线程安全问题

  • 1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题

  • 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

  • 3.如何解决:当一个线程 a 在操作 ticket(共享数据)的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他

  • 4.在 Java 中,我们通过同步机制,来解决线程的安全问题。

  • 5.同步的方式,解决了线程的安全问题。---好处

  • 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 ---局限性

  • JDK5.0 之前

  • 方式一:同步代码块

  • synchronized(同步监视器){

  • }

  • 说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。

  • 方式二:同步方法。


*/


public class SynchroThread {


public static void main(String[] args) {


Window w = new Window();


Thread t1 = new Thread(w);


Thread t2 = new Thread(w);


Thread t3 = new Thread(w);


t1.setName("窗口 1");


t2.setName("窗口 2");


t3.setName("窗口 3");


t1.start();


t2.start();


t3.start();


}


}


class Window implements Runnable{


private static int ticket = 100;


//一定要加 static 才可以


private static Object obj = new Object();


@Override


public void run() {


while(true){


//正确的 synchronized (obj)。在反射的时候会说,Window2.class 只会加载一次


synchronized (Window.class){


if(ticket > 0){


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


ticket--;


}else{


break;


}


}


}


}


}


4.4 利用 Thread 实现线程同步(synchronized 同步方法)




/**


  • 利用同步方法的方式来解决线程安全


*/


public class SynchroMethodThread {


public static void main(String[] args) {


Window2 w = new Window2();


Thread t1 = new Thread(w);


Thread t2 = new Thread(w);


Thread t3 = new Thread(w);


t1.setName("窗口 1");


t2.setName("窗口 2");


t3.setName("窗口 3");


t1.start();


t2.start();


t3.start();


}


}


class Window2 implements Runnable {


private static int ticket = 100;


@Override


public void run() {


while (true) {


show();


}


}


//必须使用静态方法,这样默认的同步监视器是 Window.class


private static synchronized void show(){


if (ticket > 0) {


try {


Thread.sleep(100);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


ticket--;


}


}


}


5 线程通讯


=========================================================================


5.1 释放锁与不会释放锁的方法:






5.2 代码实例




/**


  • 需求:利用线程通信打印 1-100。关键点:线程 1, 线程 2 交替打印

  • 涉及到的三个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

  • notify():一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。

  • notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。

  • 说明:

  • 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。

  • 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

  • 否则,会出现 IllegalMonitorStateException 异常

  • 3.wait(),notify(),notifyAll()三个方法是定义在 java.lang.Object 类中。

  • 面试题:sleep() 和 wait()的异同?

  • 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

  • 2.不同点:1)两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait()


*/


public class CommunicationRunnable {


public static void main(String[] args) {


Number number = new Number();


Thread t1 = new Thread(number);


Thread t2 = new Thread(number);


t1.setName("线程 1");


t2.setName("线程 2");


t1.start();


t2.start();


}


}


class Number implements Runnable{


private int number = 1;


private Object obj = new Object();


@Override


public void run() {


while(true){


synchronized (obj) {


obj.notify();


if(number <= 100){


try {


Thread.sleep(10);


} catch (InterruptedException e) {


e.printStackTrace();


}


System.out.println(Thread.currentThread().getName() + ":" + number);


number++;


try {


//使得调用如下 wait()方法的线程进入阻塞状态


obj.wait();


} catch (InterruptedException e) {


e.printStackTrace();


}


}else{


break;


}


}

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
Java高级特性之多线程