文章目录
多线程基本概念
并发与并行
- 并发: 指两个或多个事件在同一个时间段内发生。
- 并行: 指两个或多个事件在同-时刻发生(同时发生)。
线程与进程
基本概念
首先要理解进程(Processor)和线程(Thread)的区别
- 进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间, -一个应用程序可以同时运行多个进程;进程也是程序的一-次执行过程,是系统运行程序的基本单位;系统运行一一个程序即是-个进程从创建、运行到消亡的过程。如启动一个QQ.exe就叫一个进程。 接着又启动一个WeiXin.exe,这叫两个进程。
- 线程: 线程是进程中的一个执行单元,负责当前进程中程序的执行, 一个进程中至少有一个线程。 一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。线程是在进程内部同时做的事情,比如在迅雷里,有很多资源要同时下载
简而言之:一个程序运行后至少有一一个进程 , -一个进程中可以包含多个线程
线程调度
分时调度 :所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性) ,Java使用的为抢占式调度。
Thread类
构造方法
Thread()
分配一个新的Thread
对象。Thread(String name)
分配一个指定名称的新的Thread
对象。Thread(Runnable target)
分配一个带有指定目标新的Thread
对象。Thread(Runnable target, String name)
分配一个带有指定目标新的线程对象,并指定名字。
常用方法
- String
getName()
返回此线程的名称。 - void
setName(String name)
将此线程的名称更改为等于参数name
。 - void
start()
导致此线程开始执行,Java虚拟机调用此线程的run方法。 - void
run()
如果这个线程使用单独的Runnable
运行对象构造,则调用该Runnable
对象的run
方法; 否则,此方法不执行任何操作并返回。 - static void
sleep(long millis)
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行), - static Thread
currentThread()
返回对当前正在执行的线程对象的引用。
创建线程类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务。实际上就是执行一-段程 序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
Java中通过继承Thread类来创建并启动多线程的步骤如下:
方法1:将一个类声明为Thread的子类
- 定义Thread类的子类,并重写该类的
run()
方法,该run()
方法的方法体就代表了线程需要完成的任务,因此把run()
方法称为线程执行体。 - 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的
start()
方法来启动该线程
public static void main(String[] args){
// 创建自定义线程对象
MyThread mt = new MyThread("新的线程");
// 调用start()方法,开启新线程,自动执行run()方法
mt.start(); // 如果调用的是mt.run(),就不是多线程了,就是平常的一般方法调用。
// 在主方法中执行for循环
for(int i = 0; i < 10 ; i++){
String name = Thread.currentThread().getName(); // 获取当前线程的名称
System.out.println(name + "线程-No" + i);
}
}
// MyThread类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// String name = getName(); // 获取该线程的名称,方法1
// 获取线程名,方法2
Thread thread = Thread.currentThread();
String name = thread.getName();
System.out.println(name + "线程-No" + i);
}
}
}
方法2:创建一个线程是声明实现类Runnable接口
第一种方法会有一个缺点,因为在Java中类是单继承的,如果继承了Thread类,就无法再继承其他的类,所以可以通过实现Runnable接口进行。
java.lang.Runnable:接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run
的无参数方法。
实现步骤:
- 创建一个
Runnable
接口的实现类。 - 在实现类中重写
Runnable
接口的run()
方法,设置线程任务 - 创建一个
Runnable
接口的实现类对象 - 创建
Thread
类对象,构造方法中传递Runnable
接口的实现类对象 - 调用
Thread
类中start()
方法,会自动开启新的线程执行run()
方法
public static void main(String[] args) {
// 3 创建一个`Runnable`接口的实现类对象
RunnableImpl rni = new RunnableImpl();
// 4 创建`Thread`类对象,构造方法中传递`Runnable`接口的实现类对象
Thread thread = new Thread(rni);
// 5 调用`Thread`类中`start()` 方法,会自动开启新的线程执行`run()`方法
thread.start();
}
// RunnableImpl.java
// 1 创建一个`Runnable`接口的实现类
public class RunnableImpl implements Runnable{
// 2 在实现类中重写`Runnable`接口的`run()`方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
实现Runnable接口创建多线程的好处
-
适合多个相同的程序代码的线程去共享同一个资源。
-
可以避免Java中的单继承的局限性。一个类只能继承一个类,继承了Thread就不能继承其他的类了。
-
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
实现Runnable接口的方式,把设置线程任务和开启线程任务进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象调用start方法用来开启新线程
-
线程池只能放入实现Runnable或Collable类线程,不能直接放入继承Thread的类。
匿名内部类方式创建线程
匿名:没有名字
内部类:写在其他类内部的类
匿名内部类作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合一步完成
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类实现类对象,而这个类没有名字
格式:
new 父类/接口(){
重复父类/接口中的方法
};
public static void main(String[] args) {
// 方法一:线程父类是Thread
new Thread(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("法1:" + Thread.currentThread().getName() + "--->" + i);
}
}
}.start();
// 方法二:线程是接口Runnable的实现类
Runnable rn = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("法2:" + Thread.currentThread().getName() + "--->" + i);
}
}
};
new Thread(rn).start();
// 方法二简化版
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("法2简化版:" + Thread.currentThread().getName() + "--->" + i);
}
}
}).start();
}
线程安全
问题的出现
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,说明线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是“菊花侠大战桃花怪”,本次电影的座位共100个。有两个窗口正在卖同一张第21号座的票。
代码示例:
// RunnableImpl.java
public class RunnableImpl implements Runnable{
// 定义一个多个线程共享的票源
private int ticket = 100;
@Override
public void run() {
// 目的是把票卖完。
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "--> 正在卖票第" + ticket + "张票");
ticket -- ;
}
}
}
}
public static void main(String[] args) {
RunnableImpl rni = new RunnableImpl();
Thread t1 = new Thread(rni); // 卖票窗口1
Thread t2 = new Thread(rni); // 卖票窗口1
Thread t3 = new Thread(rni); // 卖票窗口1
// 三个窗口同时开始卖票
t1.start();
t2.start();
t3.start();
}
结果可能会出现以下问题:
- 有多个窗口正在卖同一张票
- 出现不存在的票号,如0,-1等。
问题的解决-线程同步
法1:同步代码块
解决线程安全问题的一种方案:使用同步代码块
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(如访问了共享数据的代码)
}
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
卖票案例的代码示例:
// 采用同步代码块 对RunnableImpl.java代码修改如下
public class RunnableImpl implements Runnable{
private int ticket = 100;
// 创建一个锁对象,比如用Object对象
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "--> 正在卖票第" + ticket + "张票");
ticket -- ;
}
}
}
}
}
法2:同步方法
同步方法:使用synchronized
修饰的方法,叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等候。
格式:
修饰符 synchronized 返回值类型 myMethod(){
可能会出现线程安全问题的代码(如访问了共享数据的代码)
}
使用步骤:
- 把访问了共享数据的代码抽取出来,放到一个方法中
- 在方法声明时添加
synchronized
修饰符
// 采用同步方法 对RunnableImpl.java代码修改如下
public class RunnableImpl implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
payTicket();
}
}
// 同步方法解决线程安全问题
public synchronized void payTicket(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "--> 正在卖票第" + ticket + "张票");
ticket -- ;
}
}
}
法3:Lock锁
java.util.concurrent.locks.Lock
:机制提供了比synchronized
代码块和synchronized方法
更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
使用步骤:
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口的
lock()
获取锁方法 - 在可能会出现安全问题的代码后调用Lock接口的
unlock()
释放锁方法
使用如下:
// 采用Lock方法 对RunnableImpl.java代码修改如下
public class RunnableImpl implements Runnable{
private int ticket = 100;
// 1 在成员位置创建一个ReentrantLock对象
Lock myLock = new ReentrantLock();
@Override
public void run() {
while(true){
// 2 在可能会出现安全问题的代码前调用Lock接口的`lock()`获取锁方法
myLock.lock();
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "--> 正在卖票第" + ticket + "张票");
ticket -- ;
}
// 3 在可能会出现安全问题的代码后调用Lock接口的`unlock()`释放锁方法
myLock.unlock();
}
}
}
最好加上try…catch…finally代码块,能保证无论是否出现异常,都会把Lock释放掉。示例如下
while(true){
// 2 在可能会出现安全问题的代码前调用Lock接口的`lock()`获取锁方法
myLock.lock();
if(ticket > 0){
try {
System.out.println(Thread.currentThread().getName() + "--> 正在卖票第" + ticket + "张票");
ticket -- ;
}catch (Exception e){
e.printStackTrace();
}finally {
// 3 在可能会出现安全问题的代码后调用Lock接口的`unlock()`释放锁方法
myLock.unlock();
}
}
}
线程状态
概述
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析
NEW
:至今尚未启动的线程处于这种状态。RUNNABLE
:正在 Java 虚拟机中执行的线程处于这种状态。BLOCKED
:受阻塞并等待某个监视器锁的线程处于这种状态。WAITING
:无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。TIMED_WAITING
:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERMINATED
:已退出的线程处于这种状态。
在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。
等待与唤醒案例
说明:顾客和老板线程,顾客下单后,老板开始做,做好了通知顾客
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();
// 创建一个消费者线程
new Thread(){
@Override
public void run() {
while(true){
synchronized (obj){
System.out.println("告知老板要的奶茶种类和数量!");
try {
// 调用wait方法,放弃CPU进入WAITING状态,
// wait()方法里面可以有参数,参数是最长等待时间毫秒,如`obj.wait(3000);` 指3秒后若还未被唤醒,则自动唤醒,【若无参数的话,只有被唤醒时才会醒。】
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("奶茶做好了,开喝。");
System.out.println("-------------------------");
}
}
}
}.start();
// 创建一个老板类
new Thread(){
@Override
public void run() {
while (true){
try {
Thread.sleep(3000); // 花3秒钟做奶茶
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("老板3秒钟做好了奶茶,告知顾客可以喝了");
obj.notify();// 调用notify方法,可随机唤醒一个WAITING状态的线程
// 还有`obj.notifyAll();`方法,用于唤醒所有WAITING状态的线程
}
}
}
}.start();
}
等待唤醒机制
线程间通信
概念:多个线程在处理同一资源,但处理的动作(线程的任务)却不相同。
比如:线程A用来做奶茶,线程B用来喝奶茶,奶茶可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A和线程B之间就存在线程通信问题
为什么要处理线程键通信?
多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即为等待唤醒机制。
等待幻想机制
什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争( race) ,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态( wait()
),等待其他线程执行完他们的指定代码过后再将其唤醒( notify()
) ;在有多个线程进行等待时,如果需要,可以使用notifyAll()
来唤醒所有的等待线程。
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
-
wait:线程不再活动,不再参与调度,进入waitset中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程状态即是WAITING。它还要等着别的线程执行一个特别的动作,也即是"通知( notify )”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列( ready queue )中
-
notify:则选取所通知对象的wait set中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
-
notifyAll:则释放所通知对象的wait set上的全部线程。
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同-一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了0bject类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
生产者与消费者问题
线程池
思想作用概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束 了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用 ,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。
线程池概念
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多螯述。我们通过一-张图来了解线程池的工作原理:
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
线程池:JDK1.5之后提供的。java.util.concurrent.Executors
:线程池的工厂类,用来生成线程池。
Executors
类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池
参数:
int nThreads
:创建线程池中包含的线程数量
返回值:
ExecutorService
接口,返回的是ExecutorService
接口的实现类对象,我们可以使用ExecutorService
接口接收(面向接口编程)
java.util.concurrent.ExecutorService
:线程池接口
- 用来从线程池中获取线程,调用
start
方法,执行线程任务
submit(Runnable task)
提交一个Runnable
任务用于执行
关闭/销毁线程池的方法
void shutdown()
线程池的使用步骤:- 使用线程池的工厂类
Executors
里边提供的静态方法newFixedThreadPool
生产一个指定线程数量的线程池 - 创建一个类,实现
Runnable
接口,重写run
方法,设置线程任务 - 调用
ExecutorService
中的方法submit
,传递线程任务(实现类),开启线程,执行run
方法 - 调用
ExecutorService
中的方法shutdown
销毁线程池(不建议执行)
- 使用线程池的工厂类
代码示例
public static void main(String[] args) {
// 1 使用线程池的工厂类`Executors`里边提供的静态方法`newFixedThreadPool`生产一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(2); // 该线程池只能放两个线程
// 3 调用`ExecutorService`中的方法`submit`,传递线程任务(实现类),开启线程,执行`run`方法
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
// 4 调用`ExecutorService`中的方法`shutdown`销毁线程池(不建议执行)
es.shutdown();
}
// RunnableImpl.java
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "创建了一个新的线程执行");
}
}
执行结果:
pool-1-thread-1创建了一个新的线程执行
pool-1-thread-2创建了一个新的线程执行
pool-1-thread-1创建了一个新的线程执行