Java基础篇-多线程

介绍

进程与线程

进程:是一个正在执行中的程序。

说明:每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元

线程:就是进程中的一个独立的控制单元。

说明:线程在控制着进程的执行

细节说明:一个进程中至少有一个线程

Java中的线程与进程

Java VM 在启动的时候会有一个java.exe

该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程

扩展知识:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。所以至少两个线程

创建线程-继承Thread类

java已经提供了对线程这类事物的描述。就是Thread类。

1)创建线程的第一种方式:继承Thread类,并覆盖run方法

2)创建线程的第二种方式:声明实现Runable接口的类,该类然后实现run方法

第一种方式创建线程

覆盖run方法的原因

(1)Thread类用于描述线程。该类定义了一个功能,用于存储线程中要运行的代码。该存储功能就是run方法

(2)Thread类中的run方法用于存储线程中要运行的代码

步骤:

1)定义类继承Thread方法

2)复写Thread类中的run方法

3)调用线程的start方法,启动线程以及调用run()方法

实例:

在这里插入图片描述

细节说明:

(1)发现运行结果每一次的都不同,因为多个线程都在获取cpu的执行权。cpu执行到谁,谁就运行。

(2)cpu在做着快速的切换,以达到看上去同时运行的效果。

(3)我们可以形象的把多线程的运行认为在相互抢夺cpu的执行权。这就是多线程的特性:随机性,谁抢到谁执行,至于执行多长时间,cpu说了算

明确一点:在某一时刻,只能有一个程序运行。(多核除外)

第二种方式创建线程

步骤:

1)定义类实现Runable接口

2)覆盖Runablez接口中的run方法

​ 将线程中要运行的代码存放在该run方法中

3)通过Thread类建立线程对象

4)将Runable接口的子类对象作为实际参数传递给Thread类的构造函数

​ 为什么要将Runable接口子类对象传递给Thread的构造函数?因为,自定义的run方法所属的对象是Runable接口的子类对象。所以要让线程去执行指定对象的run方法,就必须明确该run方法所属的对象

5)调用Thread类中的start方法开启线程并调用Runable接口子类的run方法

实例

在这里插入图片描述

方式1和方式2的区别

1)方式2 避免了单继承的局限性,在定义线程时,建议使用实现方式(方式2)

2)继承Thread:线程代码存放在Thread子类的run方法中,实现Runable接口线程代码存放在接口子类的run方法中

线程的四种状态

在这里插入图片描述

(1)线程在运行的时候会建立线程池,等待的线程都在线程池中。

(2)notify()方法唤醒的通常是线程池中等待的第一个等待线程

(3)它们都是使用在同步中,因为要对持有监视器也就是锁的线程进行操作,所以要使用在同步中。

(4)wait();notify();notifyAll();定义在Object类中的原因是:

1)这些方法在操作同步线程的时候,都必须要对它们所标记到的线程只有的锁,只有同一个锁上的被等待线程,可以被同一个锁上的线程的notify()方法唤醒。不可以对不同锁中的线程进行唤醒。也就是说等待和唤醒都必须是同一锁

2)因为锁(监视器)是任意的对象,所以可以被任意对象调用的方法定义在Object中

线程对象与线程名称

获取默认线程名称

线程都有自己的默认的名称

默认名称:Thread-编号 (编号从0开始)

获取线程名称方法:使用getName()方法。

实例

在这里插入图片描述

自定义线程名称

自定义线程名称有两种方式进行设置:

1)使用setName()方法进行设置

2)构造函数方式进行设置

获取线程对象:

static Thread currentThread();获取当前线程的对象

自定义线程名称步骤如下:

1)创建线程即初始化时,向线程对象传递线程名称

2)线程类中书写构造函数,并使用父类构造方法

实例

自定义线程名称,并在运行时显示自定义线程名称到控制台中

1)通过调用currentThread()方法获取当前线程的对象

2)线程对象调用线程的getName()方法,获取当前线程的名称

在这里插入图片描述

多线程的安全性问题

问题

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另外一个线程参与进来导致了共享数据出现错误

解决办法

对多条操作共享数据的语句,只能让一个线程都执行完才能执行其他线程。在执行的过程中不允许其他线程的参与执行。

(1)Java对于多线程的安全问题提供了专业的解决方式——同步代码块

在这里插入图片描述

注意:对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有锁。

(2)同步函数

同步函数:函数是线程安全的,在函数的定义时在返回值的前面加上synchronized关键字修饰,使得其为同步函数。如下图所示

在这里插入图片描述

注意:同步函数的锁。因为函数是要被一个对象调用的,所以函数都有一个所属对象应用,它的同步锁就是this

(3)静态同步函数

如果同步函数被静态修饰以后,使用的同步锁不是this(为什么?)因为静态函数在类被加载的时候虚拟机就会自动去加载静态修饰的方法或变量部分。所以它的同步锁不会是this。那它的同步锁是什么?因为静态进内存时,内存中没有该类对象在内存中,但是一定有该类对应的字节码文件对象。

锁:静态同步方法所在类的字节码文件。类名.class

同步的前提

(1)必须要有两个或者两个以上的线程

(2)必须是多个线程使用同一个锁

好处和弊端

好处:解决了多线程的安全问题

弊端:多个线程需要判断锁,较为耗费资源

单例设计模式(懒汉式)

回顾

(1)单例设计模式中饿汉式设计模式,代码回顾示意图

在这里插入图片描述

(2)单例设计模式中懒汉设计模式(延迟加载)

在这里插入图片描述

单例设计模式-懒汉式模式带来的线程安全问题的考虑

(1)问题:当多个线程通过单例设计模式(懒汉式),获取对象时,由于没有同步函数或同步代码块。又存在共享的返回数据,于是这样就造成了线程的不安全

(2)解决办法

在这里插入图片描述

注意:采用双判断的方式,提高了程序运行的工作效率。避免在程序运行过程中出现不停进入到同步代码块中进行没必要的判断,并消耗cpu资源

死锁

在程序设计的过程中,出现同步嵌套。如:同步函数中嵌套有同步代码块,当他们的锁不一样时。就会出现挂起的现象。原因是一个锁只能为一个线程解锁。在多个线程出现需要该锁进行解锁的时候就会导致相互抢夺的情况。

JDK1.5新特性

在JDK1.5中提供了多线程升级的解决方案。将同步synchornized替换成Lock操作。将Object中的wait,notify,notifyAll,替换成了Condition对象。该对象可以对Lock锁进行获取。

实例

生产者和消费者(生产一个消费一个)

在这里插入图片描述

注意:该实例中,实现了本方只唤醒对方的操作

总结 1.5版本以后提供了显示的锁机制以及显示的锁对象的等待唤醒机制。同时把锁进行了封装,实现了一个锁对应多个Condition对象。

停止线程

停止线程的方法

(1)stop方法已经过时

(2)只能让run方法结束

开启多线程运行,运行代码通常是循环结构的,只要控制住循环,就可以让run方法结束,也就是线程结束。在循环结构中定义标志位对标志位进行控制实现对线程的控制

特殊情况

当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。

\当没有指定方式让冻结的线程恢复到运行状态时,这时候需要对冻结进行清除。强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。

Tread类中提供录入该方法interrupt();

守护线程

将线程标记为用户线程或守护线程。当正在运行的线程都是守护线程时,java虚拟机自动退出,也就是主线程结束的时候虚拟机也跟着结束。

启动线程前调用setDaemon()方法传入true,将线程标记为守护线程

实例

在这里插入图片描述

join方法

当A线程执行到了B线程的.join方法时,A就会等待。等B线程都执行完,A才会执行。join可以用来临时加入线程执行。

实例:通过调用线程的join方法实现主线程的暂停操作,对调用了join方法的线程执行完毕结束之后,主线程才被重新唤醒

在这里插入图片描述

线程优先级和yield方法

线程优先级

在多线程中,java线程具有的线程优先级越高其获得的执行次数就越多。Java线程中总共有十级的优先级。默认的优先级是5。

设置优先级的办法:调用 setPriority();方法,传入线程优先级参数。

为了程序的阅读性,在Java中将优先级1,5,10定义了静态字段在Thread类中。分别是MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY

实例

在这里插入图片描述

yield方法

减缓线程执行的频率,将线程的执行权交给其他线程进行执行。