使用JDK提供的常用工具在多线程编写线程安全和数据同步的程序

题图来自APOD
你好,这里是codetrend专栏“高并发编程基础”。
引言
在并发执行任务时,由于资源共享的存在,线程安全成为一个需要考虑的问题。与串行化程序相比,并发执行可以更好地利用CPU计算能力,提高系统的吞吐量。
例如,当B客户提交一个业务请求时,不需要等到A客户处理结束才能开始,这样可以提升用户体验。
然而,并发编程也带来了新的挑战。无论是互联网系统还是企业级系统,在追求高性能的同时,稳定性也是至关重要的。开发人员需要掌握高效编程的技巧,以确保程序在安全的前提下能够高效地共享数据。
共享资源指多个线程同时对同一份资源进行读写操作,这就需要保证多个线程访问到的数据是一致的,即数据同步或资源同步。为了实现安全且高效的共享数据,以下是一些常用的方法和技术:
- 使用锁(Lock):通过使用锁机制,只有获得锁的线程才能访问共享资源,其他线程需要等待锁的释放。常见的锁包括synchronized关键字、ReentrantLock等。锁机制可以保证共享资源在同一时间只被一个线程访问,从而避免数据竞争和不一致的问题。
- 使用同步块(Synchronized Block):通过在代码块前加上synchronized关键字,确保同一时间只有一个线程可以执行该代码块。这样可以限制对共享资源的访问,保证数据的一致性。
- 使用原子操作类(Atomic Classes):Java提供了一系列原子操作类,如AtomicInteger、AtomicLong等,它们可以保证针对共享资源的操作是原子性的,不会被其他线程中断,从而避免了数据不一致的问题。
- 使用并发集合(Concurrent Collections):Java提供了一些并发安全的集合类,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们在多线程环境下可以安全地进行读写操作,避免了手动处理同步和锁的麻烦。
- 使用线程安全的设计模式:在程序设计阶段,可以采用一些线程安全的设计模式,如不可变对象、线程本地存储(Thread-local Storage)等,来避免共享资源的竞争和冲突。
数据不一致的问题
package engineer.concurrent.battle.abasic;/*** 叫号机排队模拟,通过多线程并发*/
public class TicketWindow extends Thread {private final String name;private final static int MAX = 100;private static int ticket = 1;public TicketWindow(String name) {this.name = name;}public void run() {while (ticket<= MAX) {System.out.println(name + "柜台正在排队,排队号码为:" + ticket);ticket++;}}public static void main(String[] args) {new TicketWindow("一号窗口").start();new TicketWindow("二号窗口").start();new TicketWindow("三号窗口").start();new TicketWindow("四号窗口").start();}
}
可能的输出结果如下:
三号窗口柜台正在排队,排队号码为:1
四号窗口柜台正在排队,排队号码为:1
四号窗口柜台正在排队,排队号码为:3
三号窗口柜台正在排队,排队号码为:2
四号窗口柜台正在排队,排队号码为:7
...
四号窗口柜台正在排队,排队号码为:101
四号窗口柜台正在排队,排队号码为:102
其中 ticket 就是共享资源,多个TicketWindow运行多线程竞争共享资源。可能出现的问题如下。
ticket被重复使用,也就是一个号被多个窗口叫到。ticket超过最大限制,也就是实际没得这个号但是却叫号了。ticket没有被使用,也就是一张号没有被叫到。
下面的实例代码是叫号机排队模拟,通过多线程并发,使用synchronized解决资源共享问题
package engineer.concurrent.battle.esafe;import java.util.concurrent.TimeUnit;/*** 叫号机排队模拟,通过多线程并发,使用synchronized解决资源共享问题*/
public class TicketWindowSynchronized implements Runnable {private final static int MAX = 100;private static Integer ticket = 1;private static final Object lockObj = new Object();public void run() {while (ticket <= MAX) {synchronized (lockObj) {if (ticket <= MAX) { // 额外的判断System.out.println(Thread.currentThread() + "柜台正在排队,排队号码为:" + ticket);ticket++;}}try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}}public static void main(String[] args) {TicketWindowSynchronized ticketTask = new TicketWindowSynchronized();new Thread(ticketTask, "一号窗口").start();new Thread(ticketTask, "二号窗口").start();new Thread(ticketTask, "三号窗口").start();new Thread(ticketTask, "四号窗口").start();}
}
使用同步块(Synchronized Block)
在Java中,同步块(Synchronized Block)是一种用于实现线程同步的机制。它用于标记一段代码,确保在同一时间只有一个线程可以执行这段代码,以避免数据竞争和并发问题。synchronized 字段可以用于对象方法、代码块中。
- 同步实例方法:
public synchronized void synchronizedMethod() {// 执行需要同步的代码
}
在实例方法上使用synchronized关键字,将整个方法体标记为同步块。当一个线程进入同步方法时,它将获取该实例对象的锁,并且其他线程将被阻塞等待锁的释放。
- 同步静态方法:
public static synchronized void synchronizedStaticMethod() {// 执行需要同步的代码
}
在静态方法上使用synchronized关键字,将整个静态方法标记为同步块。与同步实例方法类似,当一个线程进入同步静态方法时,它将获取类对象的锁,并且其他线程将被阻塞等待锁的释放。
- 同步块:
synchronized (lockObj) {// 执行需要同步的代码
}
使用synchronized关键字结合一个对象来创建同步块。当一个线程进入同步块时,它将获取该对象的锁,并且其他线程将被阻塞等待锁的释放。在同步块内,只有一个线程可以执行被同步的代码。
- 同步块中的条件等待和唤醒:
synchronized (lockObj) {while (!conditionMet) {try {lockObj.wait(); // 条件不满足时,线程进入等待状态并释放锁} catch (InterruptedException e) {// 处理中断异常}}// 执行需要同步的代码
}synchronized (lockObj) {conditionMet = true; // 修改条件lockObj.notify(); // 唤醒一个等待的线程lockObj.notifyAll(); // 唤醒所有等待的线程
}
在同步块中,使用对象的wait()方法让线程进入等待状态并释放锁。当某个条件满足时,可以使用notify()或notifyAll()方法唤醒等待的线程。注意,在使用条件等待和唤醒时,需要确保线程在同一对象上等待和唤醒。
同步块提供了一种简单的方式来实现线程同步,通过获取对象的锁来保证同一时间只有一个线程可以执行同步块内的代码。这对于控制并发访问共享资源非常有用。但是需要注意,如果多个线程竞争相同的锁,可能会导致性能问题和死锁情况的发生。因此,在使用同步块时,需要仔细考虑锁的粒度和设计。
测试代码如下:
package engineer.concurrent.battle.esafe;
public class SynchronizedCounter {private int c = 0;public synchronized void increment() {System.out.println(Thread.currentThread());;c++;}public synchronized void decrement() {c--;}public synchronized int value() {return c;}
}package engineer.concurrent.battle.esafe;import java.util.concurrent.CountDownLatch;public class SynchronizedCounterTest {public static void main(String[] args) throws InterruptedException {SynchronizedCounter counter = new SynchronizedCounter();CountDownLatch countDownLatch = new CountDownLatch(1000);for (int i = 0; i < 1000; i++) {new Thread(() -> {for (int j = 0; j < 100; j++) {counter.increment();}countDownLatch.countDown();},"线程编号"+i).start();}countDownLatch.await();System.out.println(counter.value());}
}
输出结果如下:
100000
使用锁(Lock)
在Java中,锁(Lock)是一种用于实现线程同步的机制。它可以确保在同一时间只有一个线程可以访问共享资源,以避免数据竞争和并发问题。与传统的synchronized关键字相比,Lock提供了更大的灵活性和功能。使用锁(Lock)机制可以更细粒度地控制线程同步,并且提供了更多高级功能,例如可中断的锁获取、定时锁获取和条件变量等待。这使得锁成为Java中多线程编程的重要组件之一。
- 创建Lock对象:
Lock lock = new ReentrantLock();
- 获取锁:
lock.lock(); // 如果锁可用,获取锁;否则等待锁的释放
或者带有超时设置的获取锁:
boolean acquired = lock.tryLock(5, TimeUnit.SECONDS); // 尝试在指定时间内获取锁,返回是否成功获取锁
if (acquired) {try {// 执行需要同步的代码} finally {lock.unlock(); // 释放锁}
} else {// 获取锁失败的处理逻辑
}
- 释放锁:
lock.unlock(); // 释放锁
- 使用锁进行同步:
lock.lock();
try {// 执行需要同步的代码
} finally {lock.unlock();
}
- 使用锁的Condition进行条件等待和唤醒:
Condition condition = lock.newCondition();// 等待条件满足
lock.lock();
try {while (!conditionMet) {condition.await(); // 等待条件满足并释放锁}// 执行需要同步的代码
} catch (InterruptedException e) {// 处理中断异常
} finally {lock.unlock();
}// 唤醒等待的线程
lock.lock();
try {condition.signal(); // 唤醒一个等待的线程condition.signalAll(); // 唤醒所有等待的线程
} finally {lock.unlock();
}
测试代码如下:
package engineer.concurrent.battle.esafe;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SynchronizedCounter3 {private int c = 0;Lock lock = new ReentrantLock();public void increment() {lock.lock();System.out.println(Thread.currentThread());c++;lock.unlock();}public void decrement() {lock.lock();c--;lock.unlock();}public int value() {return c;}
}package engineer.concurrent.battle.esafe;import java.util.concurrent.CountDownLatch;public class SynchronizedCounter3Test {public static void main(String[] args) throws InterruptedException {SynchronizedCounter3 counter = new SynchronizedCounter3();CountDownLatch countDownLatch = new CountDownLatch(10000);for (int i = 0; i < 10000; i++) {new Thread(() -> {for (int j = 0; j < 100; j++) {counter.increment();}countDownLatch.countDown();},"线程编号"+i).start();}countDownLatch.await();System.out.println(counter.value());}
}
输出结果如下,在并发情况下输出一致:
100000
使用原子操作类(Atomic Classes)
在Java中,原子操作类(Atomic Classes)是一组线程安全的工具类,用于进行原子性操作。它们提供了一些原子操作,可以确保在多线程环境下对共享变量的操作是原子的,不会出现数据竞争和并发问题。原子操作类提供了一些常见的原子操作方法,可以确保对共享变量的操作是原子的。它们适用于高并发场景,并且性能较好。使用原子操作类可以避免使用锁带来的开销,并且能够简化线程同步的代码逻辑。
需要注意的是,虽然原子操作类可以保证单个操作的原子性,但不能保证多个操作的原子性。如果需要进行复合操作,例如读取-修改-写入操作,仍然需要使用锁或其他同步机制来保证原子性。另外,原子操作类在某些情况下可能会存在ABA问题,需要根据具体场景选择合适的解决方案。
- AtomicBoolean:
AtomicBoolean atomicBoolean = new AtomicBoolean();boolean currentValue = atomicBoolean.get(); // 获取当前值atomicBoolean.set(true); // 设置新值boolean oldValue = atomicBoolean.getAndSet(false); // 先获取当前值,再设置新值,并返回旧值
- AtomicInteger:
AtomicInteger atomicInteger = new AtomicInteger();int currentValue = atomicInteger.get(); // 获取当前值atomicInteger.set(10); // 设置新值int oldValue = atomicInteger.getAndSet(5); // 先获取当前值,再设置新值,并返回旧值int newValue = atomicInteger.incrementAndGet(); // 原子地增加1,并返回新值int updatedValue = atomicInteger.updateAndGet(x -> x * 2); // 使用lambda表达式更新值,并返回更新后的值
- AtomicLong:
AtomicLong atomicLong = new AtomicLong();long currentValue = atomicLong.get(); // 获取当前值atomicLong.set(100L); // 设置新值long oldValue = atomicLong.getAndSet(50L); // 先获取当前值,再设置新值,并返回旧值long newValue = atomicLong.incrementAndGet(); // 原子地增加1,并返回新值long updatedValue = atomicLong.updateAndGet(x -> x * 2); // 使用lambda表达式更新值,并返回更新后的值
- AtomicReference:
AtomicReference<String> atomicReference = new AtomicReference<>();String currentValue = atomicReference.get(); // 获取当前值atomicReference.set("Hello"); // 设置新值String oldValue = atomicReference.getAndSet("World"); // 先获取当前值,再设置新值,并返回旧值boolean updated = atomicReference.compareAndSet("World", "Java"); // 原子地比较和设置值,返回是否成功更新
测试代码如下:
package engineer.concurrent.battle.esafe;import java.util.concurrent.atomic.AtomicInteger;public class SynchronizedCounter2 {private AtomicInteger c = new AtomicInteger(0);public synchronized void increment() {System.out.println(Thread.currentThread());c.incrementAndGet();}public void decrement() {c.decrementAndGet();}public int value() {return c.get();}
}package engineer.concurrent.battle.esafe;import java.util.concurrent.CountDownLatch;public class SynchronizedCounter2Test {public static void main(String[] args) throws InterruptedException {SynchronizedCounter2 counter = new SynchronizedCounter2();CountDownLatch countDownLatch = new CountDownLatch(1000);for (int i = 0; i < 1000; i++) {new Thread(() -> {for (int j = 0; j < 100; j++) {counter.increment();}countDownLatch.countDown();},"线程编号"+i).start();}countDownLatch.await();System.out.println(counter.value());}
}
输出结果如下,在并发情况下输出一致:
100000
使用并发集合(Concurrent Collections)
在Java中,有一组并发集合(Concurrent Collections)可以用于在多线程环境下安全地操作共享数据。这些集合类提供了线程安全的操作,并且能够处理高并发的情况,常用于多线程编程和并发控制。并发集合提供了一些常见的数据结构和操作方法,能够在多线程环境下安全地进行读写操作。它们采用了特定的并发控制策略,以提供高效的线程安全性能。需要根据具体的场景选择合适的并发集合类,以满足线程安全和并发控制的需求。
需要注意的是,并发集合并不适用于所有情况。在某些场景下,例如需要保持原子性操作或依赖复合操作的情况下,可能需要使用其他的同步机制来确保线程安全性。此外,虽然并发集合可以提供更好的性能和扩展性,但在某些情况下可能会占用更多的内存,需要根据具体情况进行权衡和选择。
- ConcurrentHashMap:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();map.put("key1", 1); // 插入键值对int value = map.get("key1"); // 获取指定键的值boolean containsKey = map.containsKey("key2"); // 检查是否包含指定的键Integer oldValue = map.putIfAbsent("key1", 2); // 当键不存在时才插入新值map.remove("key1"); // 移除指定键的键值对
- ConcurrentLinkedQueue:
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();queue.add("item1"); // 添加元素到队列String head = queue.peek(); // 获取队列头部元素String removedItem = queue.poll(); // 移除并返回队列头部元素
- CopyOnWriteArrayList:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("item1"); // 添加元素到列表String item = list.get(0); // 获取指定索引处的元素list.set(0, "newItem"); // 替换指定索引处的元素boolean removed = list.remove("item1"); // 移除指定元素
- ConcurrentSkipListMap:
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();map.put(1, "value1"); // 插入键值对Integer key = map.firstKey(); // 获取第一个键String value = map.get(key); // 根据键获取值map.remove(key); // 移除指定键的键值对
测试代码如下:
package engineer.concurrent.battle.esafe;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;public class SynchronizedCounter5 {private ConcurrentMap<String, Integer> counter = new ConcurrentHashMap<>();private final String key = "threadName";public void increment() {counter.compute(key, (key, value) -> (value == null) ? 1 : value + 1);}public void decrement() {counter.compute(key, (key, value) -> (value != null && value > 0) ? value - 1 : 0);}public int value() {return counter.values().stream().mapToInt(Integer::intValue).sum();}
}package engineer.concurrent.battle.esafe;import java.util.concurrent.CountDownLatch;public class SynchronizedCounterTest5 {public static void main(String[] args) throws InterruptedException {SynchronizedCounter5 counter = new SynchronizedCounter5();CountDownLatch countDownLatch = new CountDownLatch(1000);for (int i = 0; i < 1000; i++) {new Thread(() -> {for (int j = 0; j < 100; j++) {counter.increment();}countDownLatch.countDown();},"线程编号"+i).start();}countDownLatch.await();System.out.println(counter.value());}
}
输出结果如下,在并发情况下输出一致:
100000
死锁原因和分析
死锁的产生
因为线程中锁的加入和线程同步到需求存在,资源的竞争问题解决了,但问题出现在解决办法(也就是锁)的不合理使用会导致死锁的出现。死锁是多线程编程中常见的问题,指两个或多个线程因为互相持有对方需要的锁而陷入了无限等待的状态。Java中的死锁通常发生在如下情况下:
- 竞争有限资源:多个线程同时竞争一些有限的资源,例如数据库连接、文件句柄等。
- 锁嵌套:一个线程持有一个锁,尝试获取另一个锁,而另一个线程持有第二个锁并尝试获取第一个锁。
下面是一个造成死锁的示例代码:
/*** 在示例代码中,两个线程分别持有 lock1 和 lock2,并尝试获取对方持有的锁。如果这两个线程同时运行,就会发生死锁,因为它们互相持有了对方需要的锁。*/
public class DeadlockExample {static Object lock1 = new Object();static Object lock2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock1) { // 获取 lock1System.out.println("Thread 1: Holding lock 1...");try {Thread.sleep(10);} catch (InterruptedException e) {}synchronized (lock2) { // 尝试获取 lock2System.out.println("Thread 1: Holding lock 1 & 2...");}}},"线程001");Thread t2 = new Thread(() -> {synchronized (lock2) { // 获取 lock2System.out.println("Thread 2: Holding lock 2...");try {Thread.sleep(10);} catch (InterruptedException e) {}synchronized (lock1) { // 尝试获取 lock1System.out.println("Thread 2: Holding lock 1 & 2...");}}},"线程002");t1.start();t2.start();}
}
死锁的分析
通过jdk提供的开箱即用工具可以快速定位问题。以下是DeadlockExample产生死锁的定位过程。
- 使用
jps查看当前进程的pid。
jps
20580 Launcher
44712 RemoteMavenServer36
12428 Jps
14556
25052 DeadlockExample
- 使用
jstack pid命令分析堆栈信息。输出结果如下。
Found one Java-level deadlock:
=============================
"线程001":waiting to lock monitor 0x0000019d2d1eb820 (object 0x0000000713cfa108, a java.lang.Object),which is held by "线程002""线程002":waiting to lock monitor 0x0000019d2d1eb740 (object 0x0000000713cfa0f8, a java.lang.Object),which is held by "线程001"Java stack information for the threads listed above:
===================================================
"线程001":at engineer.concurrent.battle.esafe.DeadlockExample.lambda$main$0(DeadlockExample.java:20)- waiting to lock <0x0000000713cfa108> (a java.lang.Object)- locked <0x0000000713cfa0f8> (a java.lang.Object)at engineer.concurrent.battle.esafe.DeadlockExample$$Lambda$14/0x0000000800c01200.run(Unknown Source)at java.lang.Thread.run(java.base@17.0.7/Thread.java:833)
"线程002":at engineer.concurrent.battle.esafe.DeadlockExample.lambda$main$1(DeadlockExample.java:34)- waiting to lock <0x0000000713cfa0f8> (a java.lang.Object)- locked <0x0000000713cfa108> (a java.lang.Object)at engineer.concurrent.battle.esafe.DeadlockExample$$Lambda$15/0x0000000800c01418.run(Unknown Source)at java.lang.Thread.run(java.base@17.0.7/Thread.java:833)Found 1 deadlock.
上面的jstack信息清晰的给出了死锁的代码位置和线程名称。通过这两个信息可以定位到代码块进行对应问题的修复。
死锁的避免
要避免死锁,可以采取以下策略:
- 避免锁嵌套:尽量减少锁嵌套的层数,以避免死锁的发生。
- 按固定顺序获取锁:多个线程按照固定的顺序获取锁,以避免交叉竞争造成的死锁。
- 使用 tryLock() 方法:tryLock() 方法可以尝试获取锁一段时间,如果失败则放弃获取锁,避免一直等待造成的死锁。
- 使用 LockInterruptibly() 方法:LockInterruptibly() 方法可以在等待锁的过程中响应中断信号,避免无限等待造成的死锁。
- 合理设计资源分配:合理地划分和分配资源,以避免资源争用和死锁的产生。
修改后的代码如下:
package engineer.concurrent.battle.esafe;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 使用 tryLock() 方法:tryLock() 方法可以尝试获取锁一段时间,如果失败则放弃获取锁,避免一直等待造成的死锁。*/
public class DeadlockExampleFix {static Lock lock1 = new ReentrantLock();static Lock lock2 = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(() -> {if(lock1.tryLock()) { // 获取 lock1System.out.println("Thread 1: Holding lock 1...");try {Thread.sleep(10);} catch (InterruptedException e) {}if(lock2.tryLock()) { // 尝试获取 lock2System.out.println("Thread 1: Holding lock 1 & 2...");}}},"线程001");Thread t2 = new Thread(() -> {if(lock2.tryLock()) { // 获取 lock2System.out.println("Thread 2: Holding lock 2...");try {Thread.sleep(10);} catch (InterruptedException e) {}if(lock1.tryLock()) { // 尝试获取 lock1System.out.println("Thread 2: Holding lock 1 & 2...");}}},"线程002");t1.start();t2.start();}
}
输出结果如下:
Thread 2: Holding lock 2...
Thread 1: Holding lock 1...
参考
- 《Java高并发编程详解:多线程与架构设计》
关于作者
来自一线全栈程序员nine的探索与实践,持续迭代中。欢迎关注公众号“雨林寻北”或添加个人卫星codetrend(备注技术)。
相关文章:
使用JDK提供的常用工具在多线程编写线程安全和数据同步的程序
题图来自APOD 你好,这里是codetrend专栏“高并发编程基础”。 引言 在并发执行任务时,由于资源共享的存在,线程安全成为一个需要考虑的问题。与串行化程序相比,并发执行可以更好地利用CPU计算能力,提高系统的吞吐量…...
八道Python入门级题目及答案详解
前言 介绍Python作为一门流行的编程语言,易学易用的特点。强调通过练习题目来加深对Python语法和编程概念的理解。 题目一:计算两个数的和 描述:编写一个Python程序,计算两个数的和,并输出结果。举例:输…...
Git 的cherry-pick含义
目录 1. cherry-pick的基本概念 2. cherry-pick的使用场景 3. cherry-pick的使用方法 结论 1. cherry-pick的基本概念 git cherry-pick是一个Git命令,它允许你选择一个或多个其他分支上的提交(commits),并将它们复制到你当前的…...
大数据中TopK问题
1.给定100个int数字,在其中找出最大的10个; import java.util.PriorityQueue;public class Main {public static void main(String[] args) {final int topK 3;int[] vec {4, 1, 5, 8, 7, 2, 3, 0, 6, 9};PriorityQueue<Integer> pq new PriorityQueue<…...
基于SpringBoot+MyBatis+Vue的电商智慧仓储管理系统的设计与实现(源码+LW+部署+讲解)
前言 博主简介👨🏼⚕️:国内某一线互联网公司全栈工程师👨🏼💻,业余自媒体创作者💻,CSDN博客专家🏆,Java领域优质创作者📕&#x…...
C++经典面试题目(四)
1、请解释const关键字的作用。 在C中,const关键字主要用来表示“不变性”,即被它修饰的东西是不可修改的。它可以用于多种上下文: 修饰基本数据类型变量:声明一个常量,一旦初始化后,其值就不能再更改。 co…...
2024/3/24 蓝桥杯
P1678 烦恼的高考志愿 二分 import java.util.Arrays; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();int m sc.nextInt();int[] a new int[n1];//学校int[] b new int[m…...
用户验证:Streamlit应用程序与Streamlit-Authenticator
写在前面 在数字化时代,数据安全和用户隐私越来越受到重视。对于使用Streamlit构建的Web应用程序来说,确保用户的安全身份验证是至关重要的。而Streamlit-Authenticator,作为一个专门为Streamlit应用程序设计的身份验证库,正成为保…...
风丘EV能量流测试解决方案 提高电动汽车续航能力
电动汽车(EV)近些年发展迅猛,已被汽车业内普遍认为是未来汽车发展的新方向,但现如今电动汽车仍然存在一些短板,导致其还无法替代传统燃油车。对此,首先想到的肯定就是电动车的续航问题。其实解决电动车续航…...
【Python】输出一个 Python 项目下需要哪些第三方包
方法一 pycharm 方法二 要分析一个 Python 项目下需要哪些第三方包并生成 requirements.txt 文件,你可以使用 pipreqs 工具。以下是具体的步骤: 首先,确保你已经安装了 pipreqs 工具。如果未安装,可以使用以下命令进行安装&a…...
程序员35岁会失业吗?【来自主流AI的回答】
程序员35岁会失业吗? 35岁被认为是程序员职业生涯的分水岭,许多程序员开始担忧自己的职业发展是否会受到年龄的限制。有人担心随着年龄的增长,技术更新换代的速度会使得资深程序员难以跟上;而另一些人则认为,丰富的经…...
每天30分钟python(第一天)
1.input 1.规则 input输入的是字符串 2.print打印规则: 整数不能与文字一起打印,但是字符串可以,所以将文字转换为字符串即可 print("小明今年"str(5)"岁了") 代码实践: 错误代码: # 实现 …...
gitlab简单介绍及安装使用
gitlab 概述 什么是 gitlab GitLab 是一个基于 Web 的 Git 仓库管理工具,提供了代码托管、版本控制、协作开发、持续集成和部署等功能。它类似于 GitHub,但是 GitLab 可以在私有服务器上部署,也可以使用 GitLab 提供的托管服务。GitLab 支持…...
NetCore itext7 创建、编辑PDF插入表格、图片、文字(三)
NetCore 创建、编辑PDF插入表格、图片、文字 NetCore 创建、编辑PDF插入表格、图片、文字(二) NetCore 创建、编辑PDF插入表格、图片、文字(三) 直接上代码 nuget引入 itext7 using System; using System.IO;using iText.IO.Image; using iText.Kernel.Colors; // 导入颜色…...
数据结构奇妙旅程之深入解析冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成…...
解决 sudo apt update E: The repository is not signed.
一段时间没有用ubuntu系统,出现了很多这样的报错 解决方法 cd /etc/apt/sources.list.d ls然后把报错的项目从里面移除,例如 sudo rm cudnn-local-ubuntu2004-8.7.0.84.list全部移除后,再sudo apt-get update就能成功...
SCT2A26STER5.5V-100V Vin,4A峰值限流,高效异步降压DCDC转换器,替代LM5012、LM5013、LM5017、LM5164
• 5.5V-100V 输入电压 • 最大输出电压:30V • 2A 连续输出电流 • 4A峰值电流限制 • 1.2V 1% 反馈电压 • 集成500mΩ 高侧功率 MOSFETs • 140uA静态电流 • 恒定导通时间控制模式 • 4ms 内置软启动时间 • 300KHz 固定开关频率 • 可编程输入电压欠…...
前端学习资源整合
整合优质前端学习资源和文章,不定期更新。 JavaScript 现代 JavaScript 教程 官网:https://zh.javascript.info/GitHub:https://github.com/javascript-tutorial/zh.javascript.info 优秀的JS代码规范 官方英文版:https://gi…...
第16篇:奇偶校验器
Q:本期我们将实现4位奇偶校验逻辑电路,即校验4位二进制代码中 “1” 的个数是奇数或偶数。 A:奇偶校验器的基本原理:采用异或运算对“1”的奇偶个数进行校验,从最高位依次往最低位进行连续异或运算。如果最后的异或运…...
Obsidian+PicGo+Gitee搭建免费图床
之前使用PicGoGitee配合Typora,后来因为换电脑Typora管理笔记不方便,换到Obsidian笔记,此处记录重新搭建图床的坑与经验。 主要参考# picGogitee搭建Obsidian图床,实现高效写作! 1 下载安装PicGo 下载链接https://mo…...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Python ROS2【机器人中间件框架】 简介
销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...
佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...
Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
