多线程

本文详细介绍了并发与并行的概念,深入解析了线程与进程的区别及联系,涵盖了线程调度、线程池的使用等内容,并探讨了线程安全问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程

并发与并行

  • 并发:指的是两个或者多个事件(任务) 在***同一时间段***内发生的。
  • 并行:指的是两个或者多个事件(任务)在***同一时刻***发生(同时发生)。
    在这里插入图片描述

线程与进程

  • 进程:是指一个内存中运行的的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个线程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序就是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的运行,一个程序中至少有一个进程。一个进程可以有多个线程,这个应用程序也可以称之为多线程程序。

简而言之,一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

备注:单核处理器的计算机肯定不能并行的处理多个任务的,只能是多个任务在单个CPU上并发的执行。同理,线程也是一样的,从宏观角度上理解线程是一种并行运行的,但是从微观上分析并行运行不可能,即需要一个一个线程的去执行,当系统只有一个CPU的时候,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程调度:

  • 分时调度:所有的线程轮流使用CPU的使用权,平均分配给每个线程占用CPU的时间

  • 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机一个线程执行,Java使用的就是抢占式调度方式来运行线程程序。

    • 设置线程的优先级
    对象名.setPriority(int n) // 1~10, 默认值5
    

创建线程类

​ java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或者Thread类的子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流,java使用线程执行体来代表这段程序流。

java中通过继承Thread类来创建并启动多线程,步骤如下:

  1. 创建一个Thread类的子类
  2. 在Thread类的子类当中重写Thread类的run方法,设置线程任务(开启线程需要你做什么事情?)
  3. 创建Thread类的子类对象
  4. 调用Thread类中的方法start方法,开启新线程,执行run方法

​ void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

​ 结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。

​ 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

实例代码:

多线程的原理

多线程执行时序图,多线程的执行流程。
在这里插入图片描述
​ 程序启动运行main时候,java虚拟机启动一个进程,主线程main在main调用的时候被创建。随着调用oneThread对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程环境下运行着。

​ 通过上面一张图可以发现多线程在内存当中的执行流程。

​ 多个线程执行时,在栈内存当中,其实每一个线程都有一片属于自己的栈内存空间,进行方法的压栈和弹栈。

​ 当执行线程的任务结束了,线程自动在栈内存当中释放了。当所有的执行线程都结束了,那么进程也就结束了。

Thread类

构造方法
方法
publicThread()
分配一个新的线程对象
publicThread(String name)
分配一个指定名字的新的线程对象
publicThread(Runnable target)
分配一个带有指定目标新的线程对象
publicThread(Runnable target,String name)
分配一个带有指定目标的新的线程对象并且带有指定名字的。
常用方法
返回值方法
pubicStringgetName()
获取当前线程的名称
pubicvoidsetName(String name)
改变线程名称
publicvoidstart()
让此线程开始执行,Java虚拟机会调用此线程的run方法
publicvoidrun()
此线程要执行的任务在此方法内定义。
public staticvoidsleep(long millis)
使当前正在执行的线程以指定的毫秒数暂停(临时性暂停线程的执行)
public staticThreadcurrentThread()
获取当前正在执行的线程对象的引用。
public finalvoidsetPriority(int priority)
更改线程的优先级
public finalvoidjoin(long millisec()
等待该线程终止的时间最长为 millis 毫秒。

创建线程的方法

方式一:继承Thread类

采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。

代码示例:

public class MyThread extends Thread {
	@Override
	public void run() {
		//循环20次,打印循环的次数
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + "--" + i);
		}
	}
}
public class Demo01GetThreadName {
	public static void main(String[] args) {
		//创建Thread类的子类对象
		MyThread myThread = new MyThread();
		//调用start方法,开启新线程
		myThread.start();
	}
}
方式二:实现Runnable接口

实现步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。

    1. 创建Runnable接口实现类的实例,并以此实例作为Thread类的构造方法的target来创建Thread类的对象,该Thread类的对象才是真正的线程对象。
      3. 调用线程对象的start()方法来启动新线程。

示例代码

public class Demo02RunnbaleImpl implements Runnable {
	//在实现类重写Runnable接口当中的run方法,设置线程任务
	@Override
	public void run() {
		//循环20次,打印循环的次数
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + "--" + i);
		}
	}
}
public class Demo01Runnbale {
	public static void main(String[] args) {
		//创建Runnable接口实现类的对象
		Runnable runnbale = new Demo02RunnbaleImpl();
		//构建Thread类的对象,在构造方法中传递Runable接口的实现类对象
		Thread thread = new Thread(runnbale);
		//调用Thread类中的start方法,开启新线程执行run方法
		thread.start();
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+ "---" + i);
		}	
	}
}

​ 通过实现Runnable接口,使得该类有了多线程类的特征,run方法是多线程程序的一个执行目标,所有的多线程代码都写在run()方法中,Thread类实际上也是实现了Runnable接口的实现类

​ 在启动多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)构建线程对象,然后调用Thread类对象的start方法来运行多线程程序。

备注:Runnable对象仅仅作为Thread类对象的target,Runnable实现类里包含了run方法作为线程的执行体。而实际的线程对象依然是Thread类的实例。

使用匿名内部类的方式

使用线程的匿名内部类方式,可以很方便的实现每个线程执行不同的线程任务操作。、

使用匿名内部类方式实现Runnable接口的run方法。

示例代码:

public static void main(String[] args) {
	// 线程的接口Runnable
	Runnable run = new Runnable() {
		// 重写run方法
		@Override
		public void run() {
		// 循环20次,打印循环的次数
			for (int i = 0; i < 20; i++) {
				System.out.println(Thread.currentThread().getName() + "--->" + i);
			}
		}
    };
	new Thread(run).start();
	// 简化接口的方式
	new Thread(new Runnable() {
		// 重写run方法
		@Override
		public void run() {
			// 循环20次,打印循环的次数
			for (int i = 0; i < 20; i++) {
				System.out.println(Thread.currentThread().getName() + "--->" + i);
			}
		}
	}).start();
}

Thread类和Runnable接口的区别

如果一个类继承了Thread类,则不适合资源的共享。但是如果实现了Runnable接口的话,则很容易实现资源共享。

实现Runnable接口比继承Thread类的所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源
  2. 可以避免java中单继承的局限性
  3. 增加了程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程可以实现分离。
  4. 线程池只能放入实现Runnable或者Callable类的线程,不能直接放入继承Thread的类。

备注:在java中,每次程序运行至少启动两个线程,一个是main线程,一个垃圾收集线程。因为每当使用java命令去执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实都是在操作系统中启动了一个进程。

线程安全

​ 如果有多个线程在同时的运行,而这些线程可能同时在运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的值是一样的,就是线程安全的。

备注:线程安全问题一般都是由全局变量或者静态变量引起的,若每个线程中对全局变量、静态变量只有读操作,而无写的操作,这样的话,这个全局变量就是线程安全的;若有多个线程同时执行写操作,一般就需要考虑线程的同步,否则的话就很可能会引发线程的安全问题。

线程的同步

​ 当我们使用多线程访问同一资源的时候,且这多个线程中对资源有的写的操作,就容易出现线程安全问题。

​ 要解决多线程并发访问一个资源的安全问题,java中提供了同步机制synchronized来解决。

三种方式实现同步机制:
  1. 同步代码块
  2. 同步方法
  3. 锁机制
同步代码块
  • 同步代码块:synchronized关键字可以用于方法中的某个代码块中,表示只对这个代码块的资源实行互斥访问。

格式:

synchronized(同步锁) {
    // 需要同步操作的代码。
}

同步锁

同步锁是一个对象,是一个抽象的概念,可以想象成在对象上标记了一个锁。

  1. 锁对象可以是任意类型的。Object
  2. 多个线程对象,要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁谁就拥有资格进入代码块中,其他线程只能在外面等待着。(Blocked阻塞状态)

代码示例:

public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}
class Counter {
    public static final Object lock = new Object();
    public static int count = 0;
}
class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            //synchronized(Counter.lock) {
                Counter.count += 1;
            //}
        }
    }
}
class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            //synchronized(Counter.lock) {
                Counter.count -= 1;
            //}
        }
    }
}
//若不加synchronized得到的值不为0,加synchronized后给代码加锁保证原子性,结果为0
同步方法
  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面等待着,排队。

  • 格式:

    public synchronized void method() {
        // 可能会产生线程安全问题的代码
    }
    

备注:同步锁是谁?

​ 对于非static方法,同步锁就是this(对象本身)

​ 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)

同步方法代码示例如下:

@Override
public void run() {
	while(true){
		saleTicket();
	}
}
/*
 * 静态的同步方法
 * 锁对象,不能是this
 * this是创建对象之后产生的,静态方法优先于对象的创建
 * 静态同步方法中的锁对象是本类的class属性--->class文件对象(反射)
 */
public synchronized void saleTicket() {
	if (ticket > 0) {
		// 提高卖票的体验感 ,让程序睡眠下
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		// 票存在,卖出第ticket张票
		System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");
		ticket--;
    }
}
锁机制

Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized同步方法更加广泛的锁操作,

同步代码块/同步方法具有的功能,Lock都有,除此之外更强大,更能体现出面向对象特征。

Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:

publicvoidlock():加同步锁
publicvoidunlock():释放同步锁。

备注:锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。

示例代码如下:

public class RunnableImpl implements Runnable {
	// 定义一个多线程共享的资源 票
	private int ticket = 100;	
	// 1. 在成员的位置创建一个ReentrankLock对象
	Lock Lock = new ReentrantLock();
	// 设置线程的任务:卖票  此时窗口--->线程
	@Override
	public void run() {
		// 先判断票是否存在
		while(true){
			// 2. 在可能会引发线程安全问题的代码前调用Lock接口中的lock方法获取锁
			Lock.lock();
			if (ticket > 0) {
				try {
					Thread.sleep(10);
					// 票存在,卖出第ticket张票
					System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");
					ticket--;
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					// 无论程序出现异常,此时都会把锁释放掉
					// 在finally语句块中一般用于资源的释放,关闭IO流,释放lock锁,关闭数据库连接等等
					// 3.在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
					Lock.unlock();
				}
			}
		}
	}
}

线程状态

线程状态概述

当线程被创建并启动之后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态,

在JavaAPI帮助文档中java.lang.Thread.State这个枚举给出了线程的6种状态。

线程状态导致状态发生条件
New(新建)线程刚被创建,但是还没有启动,还没有调用start()方法
Runnable(可运行)线程可以在Java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决于操作系统处理器
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程就进入到Runnable状态
Waiting(无限等待)一个线程在等待另一个线程执行一个动作(新建)时,该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify()或者notifyAll()方法才能够唤醒
Timed_Waiting(计时等待)同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知。带有超时参数的常用方法有Thread.sleep()Object.wait()
Terminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。或对某个线程的Thread实例调用stop()方法强行终止(不建议)

六种状态切换描述:

在这里插入图片描述

Timed Waiting(计时等待)

Timed Waiting在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态(到时间后会自动醒)

其实当我们调用了sleep()方法之后,当前正在执行的线程就进入到了计时等待状态。

练习:实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1;i <= 100 ; i ++) {
            if (i % 10 == 0) {
                System.out.println("------------------>" + i);
            }
            System.out.println(i);
            // 在每个数字之间暂停1秒
            try{
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }       
    } 
    // 准备一个main函数
    public static void main(String[] args) {
        new MyThread().start();
    }
}

备注:

  1. 进入到Timed Waiting状态的一种常见的操作是调用sleep()方法,单独的线程也可以调用,不一定非要有协作关系

  2. 为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠

  3. sleep()与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行。
    在这里插入图片描述

Blocked锁阻塞状态

​ Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态,反之线程B就进入到Blocked锁阻塞状态。
在这里插入图片描述

Waiting无限等待状态

​ Waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

​ 一个调用了某个对象的Object.wait()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法

​ 其实waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

等待唤醒机制

线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却又不相同。

​ 比如说,线程A用来生产一个产品,线程B用来消费产品,产品可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

图例如下:
在这里插入图片描述

为什么要处理线程之间的通信:

​ 多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮助我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

​ 多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。

而这种手段就是----->等待唤醒机制

等待唤醒机制

什么是等待唤醒机制呢?

这是多个线程间的一种协作机制。

就是一个线程进行了规定操作后,就进入到了等待状态(wait()),等待其他线程执行完他们的指定代码后,再将其唤醒(notify());

在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。

wait()和notify()就是线程间的一种协作机制。

等待唤醒中的方法:

等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法有三个如下:

  • wait():线程不再活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不再去竞争锁,这时的线程状态就是WAITING。他还要等着别的线程执行一个特别的动作,就是**唤醒通知(notify)**在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中。

  • notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等候就餐最久的顾客最先入座。

  • notifyAll():释放所通知对象的wait set中的全部线程。

备注:

​ 哪怕只通知了一个等待线程,被通知的线程也不会立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁了,所以它需要再次尝试着取获取锁(很可能面临着其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。

​ 总结如下,

​ 如果能获取到锁,线程就从Waiting状态转变成Runnable状态,否则,从wait set 中又进入 entry set 中,线程就从Waiting状态转变成Blocked状态。

调用wait和notify方法的注意细节
  1. wait方法和notify方法必须是同一个锁对象调用。因为,对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
  2. wait方法与notify方法是属于Object类的方法,因此,锁对象可以使任意对象 ,而任意对象的所属类都是继承了Object类的
  3. wait方法与notify方法必须要在同步代码块或者同步方法中使用。因此,必须通过锁对象调用这两个方法来实现等待与唤醒。

线程池

线程池的概念

线程池:是一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁的创建线程的操作,无需反复创建线程二消耗过多的系统资源。

​ 由于线程池中有很多操作都是与优化系统资源有关的,先介绍一下线程池的工作原理
在这里插入图片描述
在这里插入图片描述

合理利用线程池能够带来什么样的好处
  • 降低了资源消耗,减少了线程的创建和销毁的次数,每个工作线程都可以被反复利用,可以执行多个任务
  • 提高了响应速度,当任务到达时,任务可以不需要等到线程的创建就能立即执行
  • 提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止以为消耗过多的内存,而导致服务器的宕机(每个线程需要大约1MB内存,县城开的越多,消耗的内存越大,死机的风险越高 )
线程池的使用

Java里面的线程池的顶级接口是java.util.concurrent.Executor,但是严格意义讲,Executor它并不是一个线程池,它只是执行线程的一个工具,真正的线程池接口是java.util.concurrent.ExecutorService

因此在java.util.concurrent.Executors线程工程类提供了一些静态工厂,生成一些常用的线程池,官方建议使用Executors来创建线程池对象。

Executors有创建线程池的方法如下:

public static ExecutorService  newFixedThreadPool(int nThreads)  :返回的就是线程池对象(创建的是有界的线程池,也就是池中的线程个数可以指定最大数量)

获取到一个线程池的ExecutorService对象,在该类中定义了一个使用线程池对象的方法如下:

public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行。 

Future接口,用来记录线程任务执行完毕后产生的结果。线程的创建与使用。

使用线程池中线程对象的使用步骤

  1. 创建线程池,使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定的数量的线程池
  2. 创建Runnable接口子类对象。(task) 定义一个类,实现Runnable接口,重写run方法,设置线程任务,new Runnable接口实现类对象
  3. 提交Runnable接口的子类对象,(take task) 调用ExecutorService中的方法submit,传递线程任务(实现类对象,)开启线程池,执行
  4. 关闭线程池(一般不做) 调用ExecutorService类中方法shutdown销毁线程池(一般不建议使用)
public class Demo01ThreadPool {
    public static void main(String[] args) {
		//1.创建线程池,使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定的数量的线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(10);
		//2.new Runnable接口实现类对象
		RunnableImpl task = new RunnableImpl();
		//3.提交Runnable接口的子类对象,(take task) 调用ExecutorService中的方法submit,传递线程任务(实现类对象,)开启线程池,
		threadPool.submit(task);
		threadPool.submit(new RunnableImpl());
		threadPool.submit(new RunnableImpl());
		/*
		 * 注意:submit方法结束后,程序并不会终止,是因为线程池控制了线程的关闭
		 * 将使用完的线程又归还到了线程池
		 */
		//4.关闭线程池(一般不做) 调用ExecutorService类中方法shutdown销毁线程池(一般不建议使用)
		threadPool.shutdown();	
		threadPool.submit(new RunnableImpl());//抛异常,线程池没有了,就不能获取到线程	
	}	
}
public class RunnableImpl implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " --> Hello Word");
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值