本章节对应知识库
多线程 (yuque.com)
>手机app应用的图片加载
>迅雷等下载软件
>Tomcat服务器上的web应用,多个客户端发起请求,Tomcat针对多个请求开辟多个线程处理
stop():一旦执行,线程就结束了,导致run()有未执行结束的代码。stop()会导致释放同步监视器,导致线程安全问题
suspend():与resume()搭配使用,会造成死锁。
同步一定会阻塞,阻塞不一定会同步
因为调用者必须时同步监视器
饿汉式自身创建了单例,所以时安全的,懒汉式不安全;内部类
三个常量【1,10】
synchronized
同步监视器不同。静态:当前类本身 非静态:this
进程作为操作系统调度和分配资源的最小单位
线程最为CPU调度和执行的最小单位
一组指令的集合
。即指一段静态的代码
,静态对象。操作系统调度和分配资源的最小单位
(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。一条执行路径
。一个进程中至少有一个线程。注意:
不同的进程之间是不共享内存的。
进程之间的数据交换和通信的成本很高。
所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。
让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
在操作系统中,启动了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单核 CPU 系统中,每一时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多核 CPU 系统中,则这些可以并发执行的程序便可以分配到多个CPU上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。
在Java种创建线程一共两种方式,继承Thread类,和实现Runnable接口
//自定义线程类
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
public class TestMyThread {
public static void main(String[] args) {
//创建自定义线程对象1
MyThread mt1 = new MyThread("子线程1");
//开启子线程1
mt1.start();
//创建自定义线程对象2
MyThread mt2 = new MyThread("子线程2");
//开启子线程2
mt2.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class TestMyRunnable {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "长江");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("黄河 " + i);
}
}
}
与之前的方式对比:
好处:
call()可以有返回值,更灵活
call()可以使用throws的方式处理异常,更灵活
callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活
缺点:
如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态
/*
* 创建多线程的方式三:实现Callable (jdk5.0新增的)
*/
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
// 接收返回值
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
好处
提高了程序执行的效率(因为线程已经提前创建好了)
提高了资源的复用率(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
可以设置相关的参数,对线程池中的线程的使用进行管理
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread2 implements Callable {
@Override
public Object call() throws Exception {
int evenSum = 0;//记录偶数的和
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
evenSum += i;
}
}
return evenSum;
}
}
public class ThreadPoolTest {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// //设置线程池的属性
// System.out.println(service.getClass());//ThreadPoolExecutor
service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
try {
Future future = service.submit(new NumberThread2());//适合使用于Callable
System.out.println("总和为:" + future.get());
} catch (Exception e) {
e.printStackTrace();
}
//3.关闭连接池
service.shutdown();
}
}
//one
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
//two
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
线程中的常用方法:
start():①启动线程,②调用线程的run()
run():将线程要执行的操作,声明在run()中
currentThread():获取当前执行代码的对应的线程
getName():获取线程名
setName():设置线程名
sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
yield():静态方法,一旦执行此方法,就释放CPU的执行权
join():在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞,继续执行
isAlive():判断当前线程是否存活
已过时
,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。已过时
,不建议使用。每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
public enum State {
NEW,//新建
RUNNABLE,//可运行
BLOCKED,//锁阻塞
WAITING,//无限等待
TIMED_WAITING,//计时等待
TERMINATED;//死亡
}
synchronized(同步监视器){
//需要同步的代码
}
说明:
多个线程必须共用同一个同步监视器
在实现Runnable接口中,同步监视器可以考虑使用:this
在继承Thread类的方式中,同步监视器慎用this,可以考虑使用当前类.class
非静态的同步方法,默认同步监视器是this
静态的同步方法,默认同步监视器是当前类本身
同步代码块和同步方法,需要关注的两个事情:①共享数据②操作共享数据的代码
如果同步代码有异常,要将unlock()写入finally语句块。
class A{
//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
//2. 调动lock(),实现需共享的代码的锁定
lock.lock();
try{
//保证线程安全的代码;
}
finally{
//3. 调用unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
死锁产生原因:
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
//实现1:
package com.atguigu.single.lazy;
public class LazyOne {
private static LazyOne instance;
private LazyOne(){}
//方式1:
public static synchronized LazyOne getInstance1(){
if(instance == null){
instance = new LazyOne();
}
return instance;
}
//方式2:
public static LazyOne getInstance2(){
synchronized(LazyOne.class) {
if (instance == null) {
instance = new LazyOne();
}
return instance;
}
}
//方式3:
public static LazyOne getInstance3(){
if(instance == null){
synchronized (LazyOne.class) {
try {
Thread.sleep(10);//加这个代码,暴露问题
} catch (InterruptedException e) {
e.printStackTrace();
}
if(instance == null){
instance = new LazyOne();
}
}
}
return instance;
}
/*
注意:上述方式3中,有指令重排问题
mem = allocate(); 为单例对象分配内存空间
instance = mem; instance引用现在非空,但还未初始化
ctorSingleton(instance); 为单例对象通过instance调用构造器
从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要
volatile关键字,避免指令重排。
*/
}
//实现2:内部类
package com.atguigu.single.lazy;
public class LazySingle {
private LazySingle(){}
public static LazySingle getInstance(){
return Inner.INSTANCE;
}
private static class Inner{
static final LazySingle INSTANCE = new LazySingle();
}
}
说明:开发建议中处理线程安全问题优先使用顺序为:
• 开发中,Lock ----> 同步代码块 ----> 同步方法
为什么要处理线程间通信:
当我们需要多个线程
来共同完成一件任务,并且我们希望他们有规律的执行
,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即—— 等待唤醒机制
wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。
notify() : 一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程(如果被wait()的多个线程的优先级相同,则随机唤醒一个)被唤醒的线程从当初被wait的位置继续执行。
notifyAll() : 一旦执行此方法,就会唤醒所有被wait的线程。
此三个方法的使用,必须是在同步代码块或同步方法中。
此三个方法的调用者,必须是同步监视器。否则,会报IllegalMonitorStateExcpetion异常
此三个方法声明在object类中
//例题:使用两个线程打印 1-100。线程1, 线程2 交替打印
class Communication implements Runnable {
int i = 1;
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else
break;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
在Java中的消息同步机制中,notify在前、wait在后是因为wait()和notify()是用于实现线程间通信的方法,它们通常和synchronized一起使用来实现同步。具体原因如下:
因此,在通常的线程同步代码中,我们会先获取对象的锁(通常是通过synchronized关键字),然后调用wait()方法,进入等待状态,直到其他线程调用notify()方法唤醒线程。这就是为什么在 Java 中,notify在前,wait在后。
相同点:一旦执行,当前线程都会进入阻塞状态
不同点:
声明的位置:wait():声明在Object类中
sleep():声明在Thread类中
使用场景不同:wait():只能使用在同步代码块或同步方法中
sleep():可以在任何需要使用的场景
使用在同步代码块或同步方法中:wait():一旦执行,会释放同步监视器
sleep():一旦执行,不会释放同步监视器
结束阻塞的方式:wait():到达指定时间自动结束阻塞,或通过被notify唤醒,结束阻塞
sleep():到达指定时间自动结束阻塞
package prodconsumer;
/**
* ClassName: ConsumerProducerTest
* Package: prodconsumer
* Description:
* 问题描述:
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),
* 如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,
* 如果店中有产品了再通知消费者来取走产品。
*问题解析:
* 1.是否是多线程问题? 是,生产者,消费者
* 2.是否有共享数据? 有!共享数据是产品
* 3.是否有线程安全问题? 有!因为有共享数据
* 4.是否需要处理线程安全问题? 是!如何处理,使用同步机制
* 5.是否存在线程间的通信?存在。
* @Author 南城余
* @Create 2023/11/9 16:46
* @Version 1.0
*/
public class ConsumerProducerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor p1 = new Productor(clerk);
Customer c1 = new Customer(clerk);
Customer c2 = new Customer(clerk);
p1.setName("生产者p1");
c1.setName("消费者c1");
c2.setName("消费者c2");
p1.start();
c1.start();
c2.start();
}
}
//资源类
class Clerk{
private int productNum = 0;
private static final int MAX_PRODUCT = 20;
private static final int MIX_PRODUCT = 1;
//增加产品
public synchronized void addProduct(){
if(productNum < MAX_PRODUCT){
productNum++;
System.out.println(Thread.currentThread().getName()+"生产了第"+productNum+"个产品");
this.notify();
}else {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public synchronized void minusProduct(){
if (productNum >= MIX_PRODUCT){
System.out.println(Thread.currentThread().getName()+"消费了第" + productNum + "个产品");
productNum--;
this.notify();
}
else {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//生产者
class Productor extends Thread{
private Clerk clerk;
public Productor(Clerk clerk){
this.clerk = clerk;
}
/*是的,这段代码中的`private Clerk clerk;`是一个属性。它是`Producer`类中的一个私有属性,
用来存储引用类型变量`Clerk`的实例。在`Producer`类的构造方法中,我们通过`this.clerk = clerk;`
将传入的`Clerk`对象赋值给了类中的`clerk`属性。这样做的目的是让`Producer`类能够访问和操作`Clerk`对象的属性和方法。
*/
@Override
public void run() {
System.out.println("========生产者开始生产商品=========");
/*这段代码中的 while(true) 是为了创建一个无限循环,也就是一个始终为真的条件。它会一直循环执行里面的代码,直到程序被显式地中断或者退出。
在这种特定的情况下,这个无限循环被用来模拟一个持续的生产过程。生产者线程会一直执行商品生产的任务,不停地往货架上增加商品,直到某个条件触发停止。
但是,要特别注意的是,使用while(true)需要谨慎,因为它可能导致程序陷入死循环,从而影响整个程序的运行。在实际的生产环境中,我们通常会结合特定的条件来控制循环的终止,如使用 break 语句或者设定一个结束条件。*/
while(true){
try {
Thread.sleep(40);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
e.printStackTrace();
}
clerk.addProduct();
}
}
}
//消费者
class Customer extends Thread{
private Clerk clerk;
public Customer(Clerk clerk){
this.clerk = clerk;
}
public void run() {
System.out.println("========消费开者始消费商品=========");
while(true){
try {
Thread.sleep(80);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
e.printStackTrace();
}
clerk.minusProduct();
}
}
}
我是南城余!欢迎关注我的博客!一同成长!
一名从事运维开发的worker,记录分享学习。
专注于AI,运维开发,windows Linux 系统领域的分享!