《Java-SE-第二十四章》之线程间协作

news2024/12/23 10:24:57

前言

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


文章目录

  • 线程之间的协作
    • wait()
    • notify()方法
    • notifyAll()方法
      • wait 和 sleep 的对比(面试题)
    • 阻塞式队列
      • 阻塞队列是什么?
      • 标准库中阻塞队列类
    • 生产者-消费者模型
      • 为什么需要使用生产者-消费者模型
      • 生产者-消费者模型特点
      • 生产者-消费者模型作用
        • 基于BlockingQueue 实现生产者-消费者模型
        • 模拟阻塞队列
        • 基于模拟阻塞队列实现生产者-消费者模型
      • 任务间使用管道进行输入/输出

线程之间的协作

​ 再次之前我们已经解决了,如果多个任务交替着步入某项共享资源,可以使用互斥来使得任何时刻只有一个任务可以访问这项资源。现在我们需要学习如何使任务彼此之间可以协作,可以达到多个任务一起工作去解决某个问题。现在的问题不是线程之间的干涉,而是线程之间的协作。线程之间的协调涉及到某些部分任务必须在其他 部分被解决之前解决。这非常像盖房子,必须先挖好房子的地基,然后同时设计好地基所需的钢结构和和水泥,而这两项任务必须在浇筑地基之前完成。水泥浇筑完之后才可以在此基础上砌墙。在这些任务中,某些可以并行执行,但是某些步骤需要所有的任务结束之后才能开动。

​ 当线程协作时,关键的问题是这些任务之间的握手,所谓的握手可以视为一种通知机制。为了实现这种握手,依旧需要使用到互斥,在多线程环境下,互斥能保证只有一个线程可以响应某个信号,这样就可以避免多个线程之间的竞争。在互斥的基础上,我们为线程添加了一种新途径,可以将自身挂起,直到某些外部条件发生变化时,表示是时候这个线程可以干活了。这种握手可以通过Object的方法wait()和notify()来安全地实现。

wait()

​ wait()使得线程可以等待某个条件发生变化,而自身是无法改变这个条件。通常,这种条件将由另一个任务来改变。你肯定不想你的线程不断测试这个任务,不断的进行空循环,这个被称为忙等,通常是一种不良好的CPU周期使用方式。这就好比张三的舍友率先进入了厕所,巧了此时张三也想上厕所,张三就不断在敲门说:“你好了没”。因此wait()方法会在等待外界条件的时候会将任务挂起,并且只有在notify()或notifyAll()触发时,即表示发生某些感感兴趣的事物,这个线程才会被唤醒去检查所产生的变化。这个通知就像,舍友告诉张三我已经解决了,你可以进去了。wait通常搭配synchronized使用,脱离synchronized使用wait会直接抛出异常。所以使用wait首先得获取锁,然后使当前执行代码的线程进行等待,然后释放锁,当满足条件时会被唤醒,重新尝试获取锁。

wait 结束等待的条件:

  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

代码示例: 观察wait()方法使用

public class WaitTask implements Runnable{
    private Object lock;

    public WaitTask(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("你好,我是:"+Thread.currentThread().getName());
            try {
                System.out.println("等待林妹妹回复");
                lock.wait();
              //lock.wait(1000);//具有时间的等待,过期不候。
                System.out.println("林妹妹回复我了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(new WaitTask(lock),"贾宝玉");
        t1.start();
    }
}

​ wait方法属于Object,而Object是被所有类都继承的。当我们调用的时候实际前面省略了this.wait是必须包含在同步代码块或者同步代码块中,其同步监视器的对象(锁 的对象)与this也就是当前的对象必须一致,不然会抛出IllegalMonitorStateException。

运行结果:

在这里插入图片描述

该程序执行到wait之后就会一直等待下去,那么程序不可能一直等待下去,这个时候就该唤醒方法notify()出场 了。

IllegalMonitorStateException复现

示例代码

public class WaitTask implements Runnable{
    private Object lock;

    public WaitTask(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (this) {
            System.out.println("你好,我是:"+Thread.currentThread().getName());
            try {
                System.out.println("等待林妹妹回复");
                lock.wait();
                System.out.println("林妹妹回复我了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(new WaitTask(lock),"贾宝玉");
        t1.start();
    }
}

运行结果:

在这里插入图片描述

notify()方法

​ notify 方法是唤醒等待的线程,notify()所在的同步代码块或者同步方法的锁对象必须和wait方法所在的同步代码块或者同步方法的锁对象一致,不然不会唤醒。

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

示例代码

public class NotifyTask implements Runnable {
    private Object lock;

    public NotifyTask(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("你好,我是:"+Thread.currentThread().getName());
            lock.notify();
        }
    }
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(new WaitTask(lock),"贾宝玉");
        t1.start();
        Thread t2 = new Thread(new NotifyTask(lock),"林黛玉");
        t2.start();
    }
}

运行结果:
在这里插入图片描述

notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

代码示例

使用notifyAll()方法唤醒所有等待线程, 在上面的代码基础上做出修改,创建 3 个 WaitTask 实例. 1 个 NotifyTask 实例.。

public class NotifyTask implements Runnable {
    private Object lock;

    public NotifyTask(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("你们好,我是:"+Thread.currentThread().getName());
            lock.notifyAll();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(new WaitTask(lock),"贾宝玉");
        Thread t2 = new Thread(new WaitTask(lock),"妙玉");
        Thread t3 = new Thread(new WaitTask(lock),"史湘云");
        Thread t4 = new Thread(new NotifyTask(lock),"林黛玉");
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(2000);
        t4.start();
    }
}

运行结果:
在这里插入图片描述

wait 和 sleep 的对比(面试题)

​ 理论上wait和sleep没有可比性,因为一个是用于线程通信,一个是让线程阻塞一段时间。唯一的相同点就是让线程放弃执行一段时间。

在此就浅浅的总结:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.

  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.

阻塞式队列

阻塞队列是什么?

​ 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则,在此基础上,如果队列满的时候,继续入队列就会阻塞,到有其他线程从队列中取走元素。如果队列空的时候,继续出队列也会阻塞, 直到有其他线程往队列中插入元素。、

标准库中阻塞队列类

​ 在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可. BlockingQueue 是个接口,需要使用它的实现之一来使用 BlockingQueue,java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类:

JDK 提供了 7 个阻塞队列。分别是

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列
  • SynchronousQueue:一个不存储元素的阻塞队列
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列(实现了继承于 BlockingQueue 的 TransferQueue)
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

BlockingQueue 主要提供四类方法,如下表所示

方法抛出异常返回特定值阻塞阻塞特定时间
入队add(e)offer(e)put(e)offer(e,time,unit)
出队remove()poll()take()poll(e,time,unit)
获取队首元素element()peek()

生产者-消费者模型

​ 假设有两个线程分别是线程A和线程B,两个线程共享一个缓冲区,线程A负责往缓冲区中放入数据,线程B往缓冲区取出数据,那么这就是 生产者-消费者模型,其中线程A就是生产者,线程B就是消费者。

为什么需要使用生产者-消费者模型

​ 在多线程环境下,如果生产者生产数据的速度足够快,而消费者消费数据的速度相对于生产者比慢,那么生产者就得等到消费者把数据消费完了再生产,因为生产者再生产数据没地方放啊!!!。同理,如果消费者消费的速度赶上了生产者生产的速度,那么消费者就经常处于等待状态。所以 为了平衡生产者和消费者之间的生产和消费数据的能力,就引入了缓冲区来存储生产者生产的数据,所以就有生产者-消费者模型。

生产者-消费者模型特点

  • 保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据。
  • 当缓冲区满的时候,生产者会进入等待状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入等待状态,直到生产者往缓冲区中添加数据时才会被唤醒

生产者-消费者模型作用

  1. 削峰填谷:当服务器短时间收到了大量的请求,服务器可能直接被打没了,为了避免服务器宕机,可以将请求放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个请求.
  2. 解耦:生产者不需要关心谁去消费数据,反正有人消费就行。消费者不需要关心生产数据,反正有人生产就行。

基于BlockingQueue 实现生产者-消费者模型

示例代码

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

public class ProducerConsumerModel {
    private  static int count;
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> blockingQue = new LinkedBlockingDeque<>();
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    Integer num = blockingQue.take();
                    TimeUnit.MILLISECONDS.sleep(1000);
                    count++;
                    System.out.println("消费者消费了"+count+"个数据,"+"数据是:"+num);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        consumer.start();
        Thread producer = new Thread(() -> {
            while (true) {
                Random rand = new Random();
                try {
                    Integer num = rand.nextInt();
                    blockingQue.put(num);
                    TimeUnit.MILLISECONDS.sleep(1000);
                    count++;
                    System.out.println("生产者生产了"+count+"个数据,"+"数据是:"+num);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
        consumer.join();
        producer.join();
    }

运行结果:

在这里插入图片描述

模拟阻塞队列

使用循环队列以及synchronized来模拟阻塞队列

示例代码

public class BlockingQueue {
    /**
     * 队列数据
     */
    private int[] elem = new int[100];
    /**
     * 队头指针
     */
    private int head;
    /**
     * 队尾指针
     */
    private int tail;
    /**
     * 队列元素个数
     */
    private int size;


    /**
     * 出队头元素
     * @return
     */
    public Integer take() throws InterruptedException {
        synchronized (this) {
            if (size == 0) {
                //队列为空
                wait();
            }
            int ret = elem[head];
            head++;
            //作用等价于 head %= elem.length
            if (head >= elem.length) {
                head = 0;
            }
            size--;
            notifyAll();
            return ret;
        }
    }

    /**
     * 入队尾元素
     * @param val
     */
    public void put(int val) throws InterruptedException {
        synchronized (this) {
            while (size == elem.length) {
                //队列满
                wait();
            }
            elem[tail++] = val;
            //作用等价于 tail %= elem.length
            if (tail >= elem.length) {
                tail = 0;
            }
            size++;
            notifyAll();
        }
    }

}

基于模拟阻塞队列实现生产者-消费者模型

示例代码


import java.util.Random;

import java.util.concurrent.TimeUnit;

public class ProducerConsumerModel {
    private  static int count;
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQue = new BlockingQueue();
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    Integer num = (Integer) blockingQue.take();
                    TimeUnit.MILLISECONDS.sleep(1000);
                    System.out.println("消费者消费了"+count+"个数据,"+"数据是:"+num);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        consumer.start();
        Thread producer = new Thread(() -> {
            while (true) {
                Random rand = new Random();
                try {
                    Integer num = rand.nextInt();
                    count++;
                    blockingQue.put(num);
                    TimeUnit.MILLISECONDS.sleep(1000);
                    System.out.println("生产者生产了"+count+"个数据,"+"数据是:"+num);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
        consumer.join();
        producer.join();
    }
}

运行结果:
在这里插入图片描述

任务间使用管道进行输入/输出

Java 中以标准库的形式支持了对线程间的输入/输出。其中输出类库中的对应物是PipedWriter类,允许任务向管道写,输入类库中的对应物是PipedReader类,允许不同的任务从同一个管道中读取。管道基本上是一个阻塞队列,而任务间使用管道进行输入/输出,可以看做是生产者-消费者”问题的变体。

示例代码

下面是一个简单例子,两个任务使用一个管道进行通信。

Sender负责向管道写数据

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Sender implements Runnable {
    private Random random = new Random(47);
    private PipedWriter out = new PipedWriter();
    public PipedWriter getPipedWriter() {
        return out;
    }
    @Override
    public void run() {
        try {
            while (true) {
                for (char c='A'; c <= 'Z'; c++) {
                    out.write(c);
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(500));
                }
            }
        } catch (IOException e) {
            System.out.println(e+"Sender writer exception");
        } catch (InterruptedException e) {
            System.out.println(e+"Sender sleep exception");
        }
    }
}

Receiver负责向管道读数据


import java.io.IOException;
import java.io.PipedReader;

public class Receiver implements Runnable {
    private PipedReader in;

    public Receiver(Sender sender) throws IOException {
        in = new PipedReader(sender.getPipedWriter());
    }

    @Override
    public void run() {
        try {
            while (true) {
                System.out.println("Read:"+(char)in.read());
            }
        } catch (IOException e) {
            System.out.println(e+"Receiver read exception");
        }
    }
}

测试代码

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class PipedIO {
    public static void main(String[] args) throws IOException, InterruptedException {
        Sender sender = new Sender();
        Receiver receiver = new Receiver(sender);
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(sender);
        exec.execute(receiver);
        TimeUnit.SECONDS.sleep(6);
        exec.shutdownNow();
    }
}

运行结果:

Read:A
Read:B
Read:C
Read:D
Read:E
Read:F
Read:G
Read:H
Read:I
Read:J
Read:K
Read:L
Read:M
Read:N
Read:O
Read:P
Read:Q
Read:R
Read:S
Read:T
Read:U
Read:V
Read:W
Read:X
Read:Y
Read:Z
java.lang.InterruptedException: sleep interruptedSender sleep exception
java.io.InterruptedIOExceptionReceiver read exception

Process finished with exit code 0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/825340.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【雕爷学编程】Arduino动手做(179)---超低成本,尝试五十元的麦克纳姆轮小车!

因为是第一次学习制作智能小车&#xff0c;没有什么把握&#xff0c;所有就找了一些便宜的配件&#xff08;大多在咸鱼上买的&#xff09;&#xff0c;万一搞不掂的话&#xff0c;损失也不大&#xff0c;呵呵… 车架底盘采用了快递保鲜箱的盖子&#xff0c;硬泡沫的&#xff0…

leetcode----JavaScript 详情题解(1)

目录 2618. 检查是否是类的对象实例 2619. 数组原型对象的最后一个元素 2620. 计数器 2621. 睡眠函数 2622. 有时间限制的缓存 2623. 记忆函数 2625. 扁平化嵌套数组 2626. 数组归约运算 2627. 函数防抖 2618. 检查是否是类的对象实例 请你编写一个函数&#xff0c;…

【C++】模板学习(二)

模板学习 非类型模板参数模板特化函数模板特化类模板特化全特化偏特化 模板分离编译模板总结 非类型模板参数 模板参数除了类型形参&#xff0c;还可以是非类型的形参。 非类型形参要求用一个常量作为类(函数)模板的一个参数。这个参数必须是整形家族的。浮点数&#xff0c;字…

协同办公企业网盘:实用性探究与切实可行的共享方式

在数字时代&#xff0c;如何解决信息存储是企业管理中的一个课题。怎样在安全存储文件的信息的同时又能够满足企业协同办公的需求呢&#xff1f; 企业网盘的出现&#xff0c;为企业提供安全存储服务的同时&#xff0c;也满足了协同办公的需求&#xff0c;因此受到了广泛关注。 …

Gartner最新报告,分析超大规模边缘解决方案

当下&#xff0c;酝酿能量的超级边缘。 最近&#xff0c;我们在谈视频化狂飙、谈AIGC颠覆、谈算力动能不足&#xff0c;很少谈及边缘。但“边缘”恰恰与这一切相关&#xff0c;且越发密不可分&#xff0c;它是未来技术发展的极大影响因子。 “到2025年&#xff0c;超过70%的组…

opencv的Mask操作,选择图片中感兴趣的区域

最近做目标检测任务的时候&#xff0c;需要对固定区域的内容进行检测&#xff0c;要用到opencv的mask操作&#xff0c;选择图片固定的区域 代码 import cv2 import numpy as npimg cv2.imread(data/images/smoking.png)# 弹出一个框 让你选择ROI | x,y是左上角的坐标 x,y,w,…

【论文精读3】基于历史抽取信息的摘要抽取方法

前言 论文分享 今天分享的是来自2018ACL的长文本抽取式摘要方法论文&#xff0c;作者来自哈尔滨工业大学和微软&#xff0c;引用数369 Neural Document Summarization by Jointly Learning to Score and Select Sentences 摘要抽取通常分为两个部分&#xff0c;句子打分和句子…

vue3单选选择全部传all,否则可以多选

<el-form-item label"发布范围-单位选择"><el-radio-group v-model"formData.unitRadio" change"getUnit"><el-radio label"ALL" click.prevent"radioChange(ALL)">全部</el-radio><el-radio la…

原子操作的重要性

原子操作&#xff1a;要么不做&#xff0c;要么一次性做完 非原子操作 其实ABCD都是对的。 B选项&#xff1a;正常执行&#xff0c;I线程执行2条语句全部执行完毕,再执行II线程重新执行一遍foo函数。 C选项&#xff1a;先执行I线程foo函数第一行代码&#xff0c;然后跳转执行…

【JS】类 class

【JS】类 class 定义类类的方法类继承静态方法 类&#xff08;class&#xff09;是用于创建对象的模板。 我们使用 class 关键字来创建一个类&#xff0c;类体在一对大括号 {} 中&#xff0c;我们可以在大括号 {} 中定义类成员的位置&#xff0c;如方法或构造函数。 每个类中…

Java-很深我只知其一-泛型

Java-很深我只知其一-泛型 目录 泛型历史 泛型类/泛型接口 泛型方法 泛型属性 泛型约束 泛型历史 JAVA 泛型&#xff08;generics&#xff09;是 JDK 5 中引入的一个新特性, 允许程序员在编程时指定类型参数&#xff0c;使编译器可以在编译代码时检测到非法的类型。泛型的…

商城免费搭建之java商城 开源java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景+b2b2c bbc

&#xfeff; 1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、R…

面试之HashMap

1.什么是集合框架Java的集合主要有两个根接口Collection和Map派生出来的,Collection派生出来了三个子接口:List,Queue,Set。因此Java集合大致可分为List,Queue,Set,Map四种体系结构。2.HashMap与TreeMapHashMap是直接实现Map接口,而TreeMap是实现SortedMap接口的,所以两个还…

RISC-V - 小记

文章目录 关于 RISC-V安装 关于 RISC-V RISC : Reduced Instruction Set Computing RISC-V(“RISC five”)的目标是成为一个通用的指令集架构(ISA) 官网&#xff1a;https://riscv.orggithub : https://github.com/riscv 教程 [完结] 循序渐进&#xff0c;学习开发一个RISC-…

如何与 Dillard‘s 建立 EDI 连接?

Dillards 是主营时装、化妆品和家居用品的零售商&#xff0c;为顾客提供高质量的商品和优质的购物体验。2022年&#xff0c;Dillards 公司位列当年《财富》美国 500 强排行榜第 488 名。本文将为大家介绍 Dillards 的 EDI 需求&#xff0c;了解如何快速对接 Dillards EDI。 Dil…

Stable Diffusion - Stable Diffusion WebUI 支持 SDXL 1.0 模型的环境配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132056980 SDXL v1.0 版本 是 Stable Diffusion 的最新版本&#xff0c;是基于潜在扩散模型的文本到图像生成技术&#xff0c;能够根据输入的任何…

瀑布流布局columns

瀑布流布局其核心是基于一个网格的布局&#xff0c;而且每行包含的项目列表高度是随机的&#xff08;随着自己内容动态变化高度&#xff09;&#xff0c;同时每个项目列表呈堆栈形式排列&#xff0c;最为关键的是&#xff0c;堆栈之间彼此之间没有多余的间距差存大。还是上张图…

sass的模块化,@import的编译时和运行时详解

sass如同css一样 可以模块化。 css中通过import来实现模块化&#xff0c; 但是两者有些不同&#xff0c;css是会参与到运行时的&#xff0c;也就是说运行的过程中有多个css文件&#xff0c;通过import指令导入进来。但是sass并不是个运行时态&#xff0c;sass是一个编译时态。…

六、JVM-垃圾收集器浅析

垃圾收集器浅析 主 JVM参数 3.1.1 标准参数 -version -help -server -cp3.1.2 -X参数 非标准参数&#xff0c;也就是在JDK各个版本中可能会变动 -Xint 解释执行 -Xcomp 第一次使用就编译成本地代码 -Xmixed 混合模式&#xff0c;JVM自己来决定3.1.3 -XX参数 使用得…

图片怎么压缩大小?值得一看的图片压缩方法

图片是我们生活和工作中必不可少的文件&#xff0c;为了能够有效保存、发送&#xff0c;我们需要适当压缩其大小&#xff0c;但很多朋友不知道怎么压缩图片&#xff0c;或者压缩后造成了图片清晰度受损&#xff0c;怎么才能既压缩图片的大小&#xff0c;又能避免清晰度受到影响…