JUC并发编程--------基础篇
一、多线程的相关知识
栈与栈帧
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换
上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
1. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
2. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
从数据来说,以程序员的角度来看, 是方法调用过程中的各种局部的变量与资源; 以线程的角度来看, 是方法的调用栈中存储的各类信息。
引发上下文切换的原因一般包括:线程、进程切换、系统调用等等。上下文切换通常是计算密集型的,因为涉及一系列数据在各种寄存器、 缓存中的来回拷贝。就CPU时间而言,一次上下文切换大概需要5000~20000个时钟周期,相对一个简单指令几个乃至十几个左右的执行时钟周期,可以看出这个成本的巨大。
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch(上下文切换) 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 Context Switch 频繁发生会影响性能
并行于并发
- 并行Parallel:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发Concurrent:在一段时间内,有多个指令在单个CPU上交替执行。
进程和线程的区别
- 进程:是正在运行的程序
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
线程状态介绍
- 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程状态被定义在了java.lang.Thread.State枚举类中
State枚举类的源码如下:
public class Thread {public enum State {/* 新建 */NEW ,
/* 可运行状态 */RUNNABLE ,
/* 阻塞状态 */BLOCKED ,
/* 无限等待状态 */WAITING ,
/* 计时等待 */TIMED_WAITING ,
/* 终止 */TERMINATED;}// 获取当前线程的状态public State getState() {return jdk.internal.misc.VM.toThreadState(threadStatus);}}
通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下
| 线程状态 | 具体含义 |
|---|---|
| NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
| RUNNABLE | 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
| BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
| WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
| TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
| TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
各个状态的转换,如下图所示:

情况 1 NEW --> RUNNABLE
当调用 t.start() 方法时,由 NEW --> RUNNABLE
情况 2 RUNNABLE <--> WAITING
t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING ,调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 竞争锁成功,t 线程从WAITING -->RUNNABLE ,竞争锁失败,t 线程从WAITING --> BLOCKED
情况 3 RUNNABLE <--> WAITING
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING ,注意是当前线程在t 线程对象的监视器上等待 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
情况 4 RUNNABLE <--> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->
RUNNABLE
情况 5 RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING ,t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 ,竞争锁成功,t 线程从TIMED_WAITING --> RUNNABLE ,竞争锁失败,t 线程从
TIMED_WAITING --> BLOCKED
情况 6 RUNNABLE <--> TIMED_WAITING
当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待 ,当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE
情况 7 RUNNABLE <--> TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8 RUNNABLE <--> TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING ,调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE
情况 9 RUNNABLE <--> BLOCKED
t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
二、创建线程的几种方式
继承Thread类
方法介绍:
实现步骤:
- 定义一个类(MyThread),继承Thread类
- 在MyThread类中实现run()重写
- 创建MyThread类对象
- 启动线程,调用start()放法
代码实现:
public class MyThread extends Thread {@Overridepublic void run() {for(int i=0; i<100; i++) {System.out.println(i);}}
}
public class MyThreadDemo {public static void main(String[] args) {MyThread my1 = new MyThread();MyThread my2 = new MyThread();
my1.start();my2.start();}
}
实现Runnable接口
实现步骤:
- 定义一个类(MyRunnable),实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类对象
- 创建Thread对象,将创建的MyRunnable类作为Thread对象构造方法的参数传入
- 调用Thread对象的start()方法
代码实现:
public class MyRunnable implements Runnable {@Overridepublic void run() {for(int i=0; i<100; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}
}
public class MyRunnableDemo {public static void main(String[] args) {//创建MyRunnable类的对象MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数//Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);//Thread(Runnable target, String name)Thread t1 = new Thread(my,"坦克");Thread t2 = new Thread(my,"飞机");
//启动线程t1.start();t2.start();}
}
实现callable接口
方法介绍:
实现步骤:
- 创建一个类(MyCallable)实现Callable接口
- 重写Callable中的call()方法
- 创建MyCallable对象
- 创建Future实现类FutureTask对象,把Mycallable作为构造方法的参数
- 创建Thread对象,把FutureTask作为构造方法的参数
- 启动Thread线程,调用start()方法
- 调用FutureTask的get()方法,获取Mycallable的返回值,get方法会一直等待线程执行完获取结果,必须要放在start后面,否则会一直等待
代码实现:
public class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {for (int i = 0; i < 100; i++) {System.out.println("跟女孩表白" + i);}//返回值就表示线程运行完毕之后的结果return "答应";}
}
public class Demo {public static void main(String[] args) throws ExecutionException, InterruptedException {//线程开启之后需要执行里面的call方法MyCallable mc = new MyCallable();
//Thread t1 = new Thread(mc);
//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象Thread t1 = new Thread(ft);//开启线程t1.start();
String s = ft.get();//该方法会一直等到线程执行完获取返回值,否则会一直等待,所以要放在start方法后面System.out.println(s);}
}
三种创建方式的区别
实现Runnable、Callable接口的的优点:
- 好处:扩展性强,实现该接口的同时,还可以继承其他类
- 缺点:编程相对复杂,不能直接调用Thread类方法
继承Thread类:
- 好处:编程相对简单,可以直接调用Thread类方法
- 缺点:可以拓展性较差,不能再继承其他类
Runnable和Callable的区别:
- Runnable规定的方法是run(),Callable规定的方法
- Runnable的run()方法不能抛出异常,而Callable的call()方法可以抛出异常
- Runnable执行完毕后没有返回值,而Callable执行完毕后有返回值
- Callable可与通过Future的实现类FutureTask的get()方法计算返回值
Thread和Runnable的区别:
Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了FutureTask。FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。因此我们通过一个线程运行Callable,但是Thread不支持构造方法中传递Callable的实例,所以我们需要通过FutureTask把一个Callable包装成Runnable,然后再通过这个FutureTask拿到Callable运行后的返回值。
新启线程有几种方式?
这个问题的答案其实众说纷纭,有2种,3种,4种等等答案,建议比较好的回答是:
按照Java源码中Thread上的注释:

官方说法是在Java中有两种方式创建一个线程用以执行,一种是派生自Thread类,另一种是实现Runnable接口。
当然本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程对象,调用Thread#start启动线程。
至于基于callable接口的方式,因为最终是要把实现了callable接口的对象通过FutureTask包装成Runnable,再交给Thread去执行,所以这个其实可以和实现Runnable接口看成同一类。
而线程池的方式,本质上是池化技术,是资源的复用,和新启线程没什么关系。
所以,比较赞同官方的说法,有两种方式创建一个线程用以执行。
三、多线程的常见方法
start和run
- run:是多线程的方法体,调用多线程时执行的代码块,如果直接调用run那么就像直接调用方法一样,不会启动多线程(正确方法时通过调用start方法启动多线程执行run方法)
- start:使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码,底层会调用native方法,通过操作系统进行操作
Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。
从Thread的源码可以看到,Thread的start方法中调用了start0()方法,而start0()是个native方法,这就说明Thread#start一定和操作系统是密切相关的。
start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常(注意,此处可能有面试题:多次调用一个线程的start方法会怎么样?)。
而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
优先级Priority
线程调度两种调度方式:
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型
- 随机性:假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
优先级相关方法
代码演示:
public class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "---" + i);}return "线程执行完毕了";}
}
public class Demo {public static void main(String[] args) {//优先级: 1 - 10 默认值:5MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);t1.setName("飞机");t1.setPriority(10);//System.out.println(t1.getPriority());//5t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);t2.setName("坦克");t2.setPriority(1);//System.out.println(t2.getPriority());//5t2.start();}
}
守护线程Daemon
相关方法:
代码演示:
public class MyThread1 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "---" + i);}}
}
public class MyThread2 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + "---" + i);}}
}
public class Demo {public static void main(String[] args) {MyThread1 t1 = new MyThread1();MyThread2 t2 = new MyThread2();
t1.setName("女神");t2.setName("备胎");
//把第二个线程设置为守护线程//当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.t2.setDaemon(true);
t1.start();t2.start();}
}
sleep
特点:
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
代码演示:
public static void main(String[] args) {new Thread(()->{try {//方式一Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}try {//方式二TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}).start();}
yield
特点:
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
- 有可能会出现让出时间片不成功的现象,因为当让出时间片后就进入就绪状态,很可能会重新再次获得时间片
代码演示:
public static void main(String[] args) {Thread thread = new Thread();thread.yield();//进入就绪状态,让出时间片}
join
特点:
- 当线程调用join方法之后,就会让主线程进入阻塞状态,直到所有被标记为join方法的线程执行完成后才能开始执行主线程
join的两种调用方式:
- join() :不添加时间参数,即等待线程完成后主线程才开始运行
- join(i) :添加时间参数,主线程最多等待i毫秒,超过i毫秒后如果还没完成,主线程也开始执行,如果参数是0,就无限等待,相当于无参调用
代码演示:
public static void main(String[] args) throws InterruptedException {MyThread myThread1 = new MyThread("线程一");myThread1.start();myThread1.join();System.out.println("我主线程先完成了");}
park,unpark
- 通过LockSupport.park可以暂停某个线程。通过LockSupport.unpark可以恢复某个线程运行
- 与 Object 的 wait & notify 相比 wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- LockSupport.unpark(t1);park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
原理:
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex 打个比喻
线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足) 调用 park 就是要看需不需要停下来歇息 ,如果备用干粮耗尽,那么钻进帐篷歇息 ,如果备用干粮充足,那么不需停留,继续前进 ,调用 unpark,就好比令干粮充足 ,如果这时线程还在帐篷,就唤醒让他继续前进,如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进,因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

1. 当前线程调用 Unsafe.park() 方法
2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
3. 线程进入 _cond 条件变量阻塞
4. 设置 _counter = 0

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 当前线程调用 Unsafe.park() 方法
3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4. 设置 _counter 为 0
interrupt
方法介绍:
interrupt中断sleep,wait,join的线程:
会打断当前的等待状态,如果需要锁的则会等待获取锁,如果不需要所得会进入就绪状态,等待获取时间片
interrupt的作用:是中断线程,如果线程不是处于wait、timewait、block状态的话,实际上并不会直接中断线程,而是给线程一个打断状态,我们需要在代码中判断打断状态是否为true(默认值为false),如果为true表示按照逻辑是希望线程终止的,这时候我们可以写一些善后代码,体面终止线程
如果线程是处于wait、timewait、block状态,使线程开始运行,并且会清空打断状态,打断状态还是fasle
-
方法 说明 interrupt() 打断线程 isinterrupted() 判断是否被打断,不会清空打断标记 interrupted() 判断是否被打断,会清空打断标记
interrupt打断正常运行的线程:
- 表面上并不会出现任何情况,但是会将打断状态设置成true,用户可以根据实际情况进行处理
interrupt打断park线程:
代码演示:
private static void test4() {Thread t1 = new Thread(() -> {for (int i = 0; i < 5; i++) {log.debug("park...");LockSupport.park();log.debug("打断状态:{}", Thread.currentThread().isInterrupted());}});t1.start();sleep(1);t1.interrupt();
}
- park线程只会使打断状态为false的线程进入阻塞,第一次park能够成功,但是当通过interrupt打断了之后,再次park就会失效,除非将打断状态设置成false
对于线程的中断,除了interrupt还有其他两种方式,但是都不推荐使用:
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
wait、notify
原理:

- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
- wait和notify都是object的方法,必须要获取对象的锁之后才能使用wait和notify进行等待和唤醒
方法介绍
| 方法 | 介绍 |
| wait() | 无限期等待,直到被唤醒 |
| wait(long i) | 等待i毫秒,如果没被唤醒则停止等待 |
| notify() | 随机唤醒一个 |
| notifyAll() | 唤醒所有wait线程 |
wait和sleep的区别
sleep是thread方法,而wait是object方法
sleep不需要强制和synchronized配合使用,wait需要和synchronized配合使用
sleep在睡眠的同时不会释放锁,wait在睡眠的时候会释放锁
相关文章:
JUC并发编程--------基础篇
一、多线程的相关知识 栈与栈帧 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。 每个栈由多个栈帧(Frame)组成…...
秒懂算法2
视频链接 : 希望下次秒懂的是算法题_哔哩哔哩_bilibili P1094 [NOIP2007 普及组] 纪念品分组 原题链接 : [NOIP2007 普及组] 纪念品分组 - 洛谷 思路 : 排序 贪心 双指针首先先对输入进来的数组进行排序(由小到大)运用贪心的思想 : 前后结合,令l1,rn,若a[l]a[r]<w…...
隐秘的角落:Java连接Oracle提示Connection timed out
前言 这个报错相信各位后端开发都不陌生,大体的原因就那么几种: 检查网络连接:确保您的计算机与数据库服务器之间的网络连接正常。尝试通过其他方式验证您的网络连接是否正常。 检查数据库服务器状态:确保数据库服务器正在运行&…...
基于微信小程序的餐厅预订系统的设计与实现(论文+源码)_kaic
摘 要 随着消费升级,越来越多的年轻人已经开始不再看重餐饮等行业的服务,而是追求一种轻松自在的用餐、购物环境。因此,无人餐厅、无人便利店、无人超市等一些科技消费场所应势而生。餐饮企业用工荒已成为不争的事实。服务员行业的低保障、低…...
科技政策 | 四川省科学技术厅关于发布2024年第一批省级科技计划项目申报指南的通知
原创 | 文 BFT机器人 近日,四川省科学技术厅发布了2024年第一批省级科技计划项目申报指南;其中包括自然科学基金项目、重点研发计划、科技成果转移转化引导计划、科技创新基地(平台)和人才计划。 01 自然科学基金项目 实施周期 …...
深入了解Webpack:特性、特点和结合JS混淆加密的实例
Webpack是现代前端开发中最受欢迎的构建工具之一,其强大的特性和灵活性使得开发者能够更有效地管理和优化项目资源。在本文中,我们将深入探讨Webpack的特性和特点,并结合实例演示如何使用Webpack与JS混淆加密相结合。Webpack的特性和特点 1.…...
2023-08-23力扣每日一题
链接: 1782. 统计点对的数目 题意: 给n个点和m条无向边(可重复),q个查询 定义edge[a]为一个点是a的边数量,定义ret[a,b]是edge[a]edge[b]-(a与b的边) q个查询q个答案࿰…...
分发饼干【贪心算法】
分发饼干 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个…...
为什么网络互联地址设置为30位地址
对于点对点链路,为了节约IPv4地址,一般为其分配/30地址块,这样包含4个地址:最小地址作为网络地址,最大地址作为广播地址,剩余两个可分配地址,分配给链路两端的接口,这是最普遍的方法…...
青少年棒球锦标赛发展·棒球1号位
青少年棒球锦标赛发展 1. 青少年棒球锦标赛简介 青少年棒球锦标赛是一个令人兴奋的国际性比赛,每年都有来自世界各地的优秀青少年棒球选手参加。这个锦标赛旨在提供一个展示青少年棒球选手的技能和才华的平台,同时也是为了推动棒球在全球范围内的普及和…...
Unity实现UI图片面板滚动播放效果第二弹
效果: 场景结构: 特殊物体:panel下面用排列组件horizent layout group放置多个需要显示的面板,用mask遮罩好。 主要思路: 这次是要在最后一个toggle的地方,依然向左滚动回1,这是难点。因此实际…...
Redis的基本操作
文章目录 1.Redis简介2.Redis的常用数据类型3.Redis的常用命令1.字符串操作命令2.哈希操作命令3.列表操作命令4.集合操作命令5.有序集合操作命令6.通用操作命令 4.Springboot配置Redis1.导入SpringDataRedis的Maven坐标2.配置Redis的数据源3.编写配置类,创还能Redis…...
省级智慧农业大数据平台项目规划建设方案[195页Word]
导读:原文《省级智慧农业大数据平台项目规划建设方案[195页Word]》(获取来源见文尾),本文精选其中精华及架构部分,逻辑清晰、内容完整,为快速形成售前方案提供参考。 1 农业大数据平台项目概述 1.1 建设…...
php图片批量压缩并同时保持清晰度
php图片压缩可以通过GD库来实现。以下是一个使用GD库进行图片压缩的示例代码: // 原始图片路径 $sourceImage path/to/source/image.jpg; // 压缩后保存的路径及文件名 $compressedImage path/to/compressed/image.jpg; // 压缩后的图片质量(1-100&…...
243:vue+Openlayers 更改鼠标滚轮缩放地图大小,每次缩放小一点
第243个 点击查看专栏目录 本示例的目的是介绍如何在vue+openlayers项目中设置鼠标滚轮缩放地图大小,每次滑动一格滚轮,设定的值非默认值1。具体的设置方法,参考源代码。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源…...
NOI2015D. 荷马史诗
荷马史诗 题目描述 追逐影子的人,自己就是影子。 ——荷马 Allison 最近迷上了文学。她喜欢在一个慵懒的午后,细细地品上一杯卡布奇诺,静静地阅读她爱不释手的《荷马史诗》。但是由《奥德赛》和《伊利亚特》组成的鸿篇巨制《荷马史诗》实在是…...
并法编程(集合类不安全)03详细讲解未补充
还未补充...
软考:中级软件设计师:大数据
软考:中级软件设计师:大数据 提示:系列被面试官问的问题,我自己当时不会,所以下来自己复盘一下,认真学习和总结,以应对未来更多的可能性 关于互联网大厂的笔试面试,都是需要细心准备的 &#x…...
【持续更新中】QAGroup1
OVERVIEW Q&AGroup1一、语言基础1.C语言(1)含参数的宏与函数的不同点(2)sizeof与strlen的区别(3)大/小端(4)strcpy与memcpy的区别(5)extern与static的区别…...
redis应用 2:延时队列
我们平时习惯于使用 Rabbitmq 和 Kafka 作为消息队列中间件,来给应用程序之间增加异步消息传递功能。这两个中间件都是专业的消息队列中间件,特性之多超出了大多数人的理解能力。 使用过 Rabbitmq 的同学知道它使用起来有多复杂,发消息之前要…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...
安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...
【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验
Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...
动态规划-1035.不相交的线-力扣(LeetCode)
一、题目解析 光看题目要求和例图,感觉这题好麻烦,直线不能相交啊,每个数字只属于一条连线啊等等,但我们结合题目所给的信息和例图的内容,这不就是最长公共子序列吗?,我们把最长公共子序列连线起…...
