认真聊聊并发编程的10个坑
对于从事后端开发的同学来说并发编程肯定再熟悉不过了。说实话在java中并发编程是一大难点至少我是这么认为的。不光理解起来比较费劲使用起来更容易踩坑。不信让继续往下面看。今天重点跟大家一起聊聊并发编程的10个坑希望对你有帮助。1. SimpleDateFormat线程不安全在java8之前我们对时间的格式化处理一般都是用的SimpleDateFormat类实现的。例如Service public class SimpleDateFormatService { public Date time(String time) throws ParseException { SimpleDateFormat dateFormat new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); return dateFormat.parse(time); } }如果你真的这样写是没问题的。就怕哪天抽风你觉得dateFormat是一段固定的代码应该要把它抽取成常量。于是把代码改成下面的这样Service public class SimpleDateFormatService { private static SimpleDateFormat dateFormat new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); public Date time(String time) throws ParseException { return dateFormat.parse(time); } }dateFormat对象被定义成了静态常量这样就能被所有对象共用。如果只有一个线程调用time方法也不会出现问题。但Serivce类的方法往往是被Controller类调用的而Controller类的接口方法则会被tomcat的线程池调用。换句话说可能会出现多个线程调用同一个Controller类的同一个方法也就是会出现多个线程会同时调用time方法的情况。而time方法会调用SimpleDateFormat类的parse方法Override public Date parse(String text, ParsePosition pos) { ... Date parsedDate; try { parsedDate calb.establish(calendar).getTime(); ... } catch (IllegalArgumentException e) { pos.errorIndex start; pos.index oldStart; return null; } return parsedDate; }该方法会调用establish方法Calendar establish(Calendar cal) { ... //1.清空数据 cal.clear(); //2.设置时间 cal.set(...); //3.返回 return cal; }其中的步骤1、2、3是非原子操作。但如果cal对象是局部变量还好坏就坏在parse方法调用establish方法时传入的calendar是SimpleDateFormat类的父类DateFormat的成员变量public abstract class DateFormat extends Forma { .... protected Calendar calendar; ... }这样就可能会出现多个线程同时修改同一个对象即dateFormat他的同一个成员变量即Calendar值的情况。这样可能会出现某个线程设置好了时间又被其他的线程修改了从而出现时间错误的情况。那么如何解决这个问题呢SimpleDateFormat类的对象不要定义成静态的可以改成方法的局部变量。使用ThreadLocal保存SimpleDateFormat类的数据。使用java8的DateTimeFormatter类。2. 双重检查锁的漏洞单例模式无论在实际工作还是在面试中都出现得比较多。我们都知道单例模式有饿汉模式和懒汉模式两种。饿汉模式代码如下public class SimpleSingleton { //持有自己类的引用 private static final SimpleSingleton INSTANCE new SimpleSingleton(); //私有的构造方法 private SimpleSingleton() { } //对外提供获取实例的静态方法 public static SimpleSingleton getInstance() { return INSTANCE; } }使用饿汉模式的好处是没有线程安全的问题但带来的坏处也很明显。private static final SimpleSingleton INSTANCE new SimpleSingleton();一开始就实例化对象了如果实例化过程非常耗时并且最后这个对象没有被使用不是白白造成资源浪费吗还真是啊。这个时候你也许会想到不用提前实例化对象在真正使用的时候再实例化不就可以了这就是我接下来要介绍的懒汉模式。具体代码如下public class SimpleSingleton2 { private static SimpleSingleton2 INSTANCE; private SimpleSingleton2() { } public static SimpleSingleton2 getInstance() { if (INSTANCE null) { INSTANCE new SimpleSingleton2(); } return INSTANCE; } }示例中的INSTANCE对象一开始是空的在调用getInstance方法才会真正实例化。嗯不错不错。但这段代码还是有问题。假如有多个线程中都调用了getInstance方法那么都走到 if (INSTANCE null) 判断时可能同时成立因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象即INSTANCE对象被创建了多次违背了只创建一个INSTANCE对象的初衷。为了解决饿汉模式和懒汉模式各自的问题于是出现了双重检查锁。具体代码如下public class SimpleSingleton4 { private static SimpleSingleton4 INSTANCE; private SimpleSingleton4() { } public static SimpleSingleton4 getInstance() { if (INSTANCE null) { synchronized (SimpleSingleton4.class) { if (INSTANCE null) { INSTANCE new SimpleSingleton4(); } } } return INSTANCE; } }需要在synchronized前后两次判空。但我要告诉你的是这段代码有漏洞的。有什么问题public static SimpleSingleton4 getInstance() { if (INSTANCE null) {//1 synchronized (SimpleSingleton4.class) {//2 if (INSTANCE null) {//3 INSTANCE new SimpleSingleton4();//4 } } } return INSTANCE;//5 }getInstance方法的这段代码我是按1、2、3、4、5这种顺序写的希望也按这个顺序执行。但是java虚拟机实际上会做一些优化对一些代码指令进行重排。重排之后的顺序可能就变成了1、3、2、4、5这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下public static SimpleSingleton4 getInstance() { if (INSTANCE null) {//1 if (INSTANCE null) {//3 synchronized (SimpleSingleton4.class) {//2 INSTANCE new SimpleSingleton4();//4 } } } return INSTANCE;//5 }原来如此那有什么办法可以解决呢答可以在定义INSTANCE是加上volatile关键字。具体代码如下public class SimpleSingleton7 { private volatile static SimpleSingleton7 INSTANCE; private SimpleSingleton7() { } public static SimpleSingleton7 getInstance() { if (INSTANCE null) { synchronized (SimpleSingleton7.class) { if (INSTANCE null) { INSTANCE new SimpleSingleton7(); } } } return INSTANCE; } }volatile关键字可以保证多个线程的可见性但是不能保证原子性。同时它也能禁止指令重排。双重检查锁的机制既保证了线程安全又比直接上锁提高了执行效率还节省了内存空间。此外如果你想了解更多单例模式的细节问题可以看看我的另一篇文章《单例模式真不简单》3. volatile的原子性从前面我们已经知道volatile是一个非常不错的关键字它能保证变量在多个线程中的可见性它也能禁止指令重排但是不能保证原子性。使用volatile关键字禁止指令重排前面已经说过了这里就不聊了。可见性主要体现在一个线程对某个变量修改了另一个线程每次都能获取到该变量的最新值。先一起看看反例public class VolatileTest extends Thread { private boolean stopFlag false; public boolean isStopFlag() { return stopFlag; } Override public void run() { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } stopFlag true; System.out.println(Thread.currentThread().getName() stopFlag stopFlag); } public static void main(String[] args) { VolatileTest vt new VolatileTest(); vt.start(); while (true) { if (vt.isStopFlag()) { System.out.println(stop); break; } } } }上面这段代码中VolatileTest是一个Thread类的子类它的成员变量stopFlag默认是false在它的run方法中修改成了true。然后在main方法的主线程中用vt.isStopFlag()方法判断如果它的值是true时则打印stop关键字。那么如何才能让stopFlag的值修改了在主线程中通过vt.isStopFlag()方法能够获取最新的值呢正例如下public class VolatileTest extends Thread { private volatile boolean stopFlag false; public boolean isStopFlag() { return stopFlag; } Override public void run() { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } stopFlag true; System.out.println(Thread.currentThread().getName() stopFlag stopFlag); } public static void main(String[] args) { VolatileTest vt new VolatileTest(); vt.start(); while (true) { if (vt.isStopFlag()) { System.out.println(stop); break; } } } }用volatile关键字修饰stopFlag即可。下面重点说说volatile的原子性问题。使用多线程给count加1代码如下public class VolatileTest { public volatile int count 0; public void add() { count; } public static void main(String[] args) { final VolatileTest test new VolatileTest(); for (int i 0; i 20; i) { new Thread() { Override public void run() { for (int j 0; j 1000; j) { test.add(); } } ; }.start(); } while (Thread.activeCount() 2) { //保证前面的线程都执行完 Thread.yield(); } System.out.println(test.count); } }执行结果每次都不一样但可以肯定的是count值每次都小于20000比如19999。这个例子中count是成员变量虽说被定义成了volatile的但由于add方法中的count是非原子操作。在多线程环境中count的数据可能会出现问题。由此可见volatile不能保证原子性。那么如何解决这个问题呢答使用synchronized关键字。改造后的代码如下public class VolatileTest { public int count 0; public synchronized void add() { count; } public static void main(String[] args) { final VolatileTest test new VolatileTest(); for (int i 0; i 20; i) { new Thread() { Override public void run() { for (int j 0; j 1000; j) { test.add(); } } ; }.start(); } while (Thread.activeCount() 2) { //保证前面的线程都执行完 Thread.yield(); } System.out.println(test.count); } }4. 死锁死锁可能是大家都不希望遇到的问题因为一旦程序出现了死锁如果没有外力的作用程序将会一直处于资源竞争的假死状态中。死锁代码如下public class DeadLockTest { public static String OBJECT_1 OBJECT_1; public static String OBJECT_2 OBJECT_2; public static void main(String[] args) { LockA lockA new LockA(); new Thread(lockA).start(); LockB lockB new LockB(); new Thread(lockB).start(); } } class LockA implements Runnable { Override public void run() { synchronized (DeadLockTest.OBJECT_1) { try { Thread.sleep(500); synchronized (DeadLockTest.OBJECT_2) { System.out.println(LockA); } } catch (InterruptedException e) { e.printStackTrace(); } } } } class LockB implements Runnable { Override public void run() { synchronized (DeadLockTest.OBJECT_2) { try { Thread.sleep(500); synchronized (DeadLockTest.OBJECT_1) { System.out.println(LockB); } } catch (InterruptedException e) { e.printStackTrace(); } } } }一个线程在获取OBJECT_1锁时没有释放锁又去申请OBJECT_2锁。而刚好此时另一个线程获取到了OBJECT_2锁也没有释放锁去申请OBJECT_1锁。由于OBJECT_1和OBJECT_2锁都没有释放两个线程将一起请求下去陷入死循环即出现死锁的情况。那么如果避免死锁问题呢4.1 缩小锁的范围出现死锁的情况有可能是像上面那样锁范围太大了导致的。那么解决办法就是缩小锁的范围。具体代码如下class LockA implements Runnable { Override public void run() { synchronized (DeadLockTest.OBJECT_1) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (DeadLockTest.OBJECT_2) { System.out.println(LockA); } } } class LockB implements Runnable { Override public void run() { synchronized (DeadLockTest.OBJECT_2) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (DeadLockTest.OBJECT_1) { System.out.println(LockB); } } }在获取OBJECT_1锁的代码块中不包含获取OBJECT_2锁的代码。同时在获取OBJECT_2锁的代码块中也不包含获取OBJECT_1锁的代码。4.2 保证锁的顺序出现死锁的情况说白了是一个线程获取锁的顺序是OBJECT_1和OBJECT_2。而另一个线程获取锁的顺序刚好相反为OBJECT_2和OBJECT_1。那么如果我们能保证每次获取锁的顺序都相同就不会出现死锁问题。具体代码如下class LockA implements Runnable { Override public void run() { synchronized (DeadLockTest.OBJECT_1) { try { Thread.sleep(500); synchronized (DeadLockTest.OBJECT_2) { System.out.println(LockA); } } catch (InterruptedException e) { e.printStackTrace(); } } } } class LockB implements Runnable { Override public void run() { synchronized (DeadLockTest.OBJECT_1) { try { Thread.sleep(500); synchronized (DeadLockTest.OBJECT_2) { System.out.println(LockB); } } catch (InterruptedException e) { e.printStackTrace(); } } } }两个线程每个线程都是先获取OBJECT_1锁再获取OBJECT_2锁。5. 没释放锁在java中除了使用synchronized关键字给我们所需要的代码块加锁之外还能通过Lock关键字加锁。使用synchronized关键字加锁后如果程序执行完毕或者程序出现异常时会自动释放锁。但如果使用Lock关键字加锁后需要开发人员在代码中手动释放锁。例如public class LockTest { private final ReentrantLock rLock new ReentrantLock(); public void fun() { rLock.lock(); try { System.out.println(fun); } finally { rLock.unlock(); } } }代码中先创建一个ReentrantLock类的实例对象rLock调用它的lock方法加锁。然后执行业务代码最后再finally代码块中调用unlock方法。但如果你没有在finally代码块中调用unlock方法手动释放锁线程持有的锁将不会得到释放。6. HashMap导致内存溢出HashMap在实际的工作场景中使用频率还是挺高的比如接收参数缓存数据汇总数据等等。但如果你在多线程的环境中使用HashMap可能会导致非常严重的后果。Service public class HashMapService { private MapLong, Object hashMap new HashMap(); public void add(User user) { hashMap.put(user.getId(), user.getName()); } }在HashMapService类中定义了一个HashMap的成员变量在add方法中往HashMap中添加数据。在controller层的接口中调用add方法会使用tomcat的线程池去处理请求就相当于在多线程的场景下调用add方法。在jdk1.7中HashMap使用的数据结构是数组链表。如果在多线程的情况下不断往HashMap中添加数据它会调用resize方法进行扩容。该方法在复制元素到新数组时采用的头插法在某些情况下会导致链表会出现死循环。死循环最终结果会导致内存溢出。此外如果HashMap中数据非常多会导致链表很长。当查找某个元素时需要遍历某个链表查询效率不太高。为此jdk1.8之后将HashMap的数据结构改成了数组链表红黑树。如果同一个数组元素中的数据项小于8个则还是用链表保存数据。如果大于8个则自动转换成红黑树。为什么要用红黑树答链表的时间复杂度是O(n)而红黑树的时间复杂度是O(logn)红黑树的复杂度是优于链表的。既然这样为什么不直接使用红黑树答树节点所占存储空间是链表节点的两倍节点少的时候尽管在时间复杂度上红黑树比链表稍微好一些。但是由于红黑树所占空间比较大HashMap综合考虑之后认为节点数量少的时候用占存储空间更多的红黑树不划算。jdk1.8中HashMap就不会出现死循环答错它在多线程环境中依然会出现死循环。在扩容的过程中在链表转换为树的时候for循环一直无法跳出从而导致死循环。那么如果想多线程环境中使用HashMap该怎么办呢答使用ConcurrentHashMap。7. 使用默认线程池我们都知道jdk1.5之后提供了ThreadPoolExecutor类用它可以自定义线程池。线程池的好处有很多比如降低资源消耗避免了频繁的创建线程和销毁线程可以直接复用已有线程。而我们都知道创建线程是非常耗时的操作。提供速度任务过来之后因为线程已存在可以拿来直接使用。提高线程的可管理性线程是非常宝贵的资源如果创建过多的线程不仅会消耗系统资源甚至会影响系统的稳定。使用线程池可以非常方便的创建、管理和监控线程。当然jdk为了我们使用更便捷专门提供了Executors类给我们快速创建线程池。该类中包含了很多静态方法newCachedThreadPool创建一个可缓冲的线程如果线程池大小超过处理需要可灵活回收空闲线程若无可回收则新建线程。newFixedThreadPool创建一个固定大小的线程池如果任务数量超过线程池大小则将多余的任务放到队列中。newScheduledThreadPool创建一个固定大小并且能执行定时周期任务的线程池。newSingleThreadExecutor创建只有一个线程的线程池保证所有的任务安装顺序执行。在高并发的场景下如果大家使用这些静态方法创建线程池会有一些问题。那么我们一起看看有哪些问题newFixedThreadPool允许请求的队列长度是Integer.MAX_VALUE可能会堆积大量的请求从而导致OOM。newSingleThreadExecutor允许请求的队列长度是Integer.MAX_VALUE可能会堆积大量的请求从而导致OOM。newCachedThreadPool允许创建的线程数是Integer.MAX_VALUE可能会创建大量的线程从而导致OOM。那我们该怎办呢优先推荐使用ThreadPoolExecutor类我们自定义线程池。具体代码如下ExecutorService threadPool new ThreadPoolExecutor( 8, //corePoolSize线程池中核心线程数 10, //maximumPoolSize 线程池中最大线程数 60, //线程池中线程的最大空闲时间超过这个时间空闲线程将被回收 TimeUnit.SECONDS,//时间单位 new ArrayBlockingQueue(500), //队列 new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略顺便说一下如果是一些低并发场景使用Executors类创建线程池也未尝不可也不能完全一棍子打死。在这些低并发场景下很难出现OOM问题所以我们需要根据实际业务场景选择。8. Async注解的陷阱之前在java并发编程中实现异步功能一般是需要使用线程或者线程池。线程池的底层也是用的线程。而实现一个线程要么继承Thread类要么实现Runnable接口然后在run方法中写具体的业务逻辑代码。开发spring的大神们为了简化这类异步操作已经帮我们把异步功能封装好了。spring中提供了Async注解我们可以通过它即可开启异步功能使用起来非常方便。具体做法如下1.在springboot的启动类上面加上EnableAsync注解。EnableAsync SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }2.在需要执行异步调用的业务方法加上Async注解。Service public class CategoryService { Async public void add(Category category) { //添加分类 } }3.在controller方法中调用这个业务方法。RestController RequestMapping(/category) public class CategoryController { Autowired private CategoryService categoryService; PostMapping(/add) public void add(RequestBody category) { categoryService.add(category); } }这样就能开启异步功能了。是不是很easy但有个坏消息是用Async注解开启的异步功能会调用AsyncExecutionAspectSupport类的doSubmit方法。默认情况会走else逻辑。而else的逻辑最终会调用doExecute方法protected void doExecute(Runnable task) { Thread thread (this.threadFactory ! null ? this.threadFactory.newThread(task) : createThread(task)); thread.start(); }我去这不是每次都会创建一个新线程吗没错使用Async注解开启的异步功能默认情况下每次都会创建一个新线程。如果在高并发的场景下可能会产生大量的线程从而导致OOM问题。建议大家在Async注解开启的异步功能时请别忘了定义一个线程池。9. 自旋锁浪费cpu资源在并发编程中自旋锁想必大家都已经耳熟能详了。自旋锁有个非常经典的使用场景就是CAS即比较和交换它是一种无锁化思想说白了用了一个死循环用来解决高并发场景下更新数据的问题。而atomic包下的很多类比如AtomicInteger、AtomicLong、AtomicBoolean等都是用CAS实现的。我们以AtomicInteger类为例它的incrementAndGet没有每次都给变量加1。public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) 1; }它的底层就是用的自旋锁实现的public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 var4)); return var5; }在do...while死循环中不停进行数据的比较和交换如果一直失败则一直循环重试。如果在高并发的情况下compareAndSwapInt会很大概率失败因此导致了此处cpu不断的自旋这样会严重浪费cpu资源。那么如果解决这个问题呢答使用LockSupport类的parkNanos方法。具体代码如下private boolean compareAndSwapInt2(Object var1, long var2, int var4, int var5) { if(this.compareAndSwapInt(var1,var2,var4, var5)) { return true; } else { LockSupport.parkNanos(10); return false; } }当cas失败之后调用LockSupport类的parkNanos方法休眠一下相当于调用了Thread.Sleep方法。这样能够有效的减少频繁自旋导致cpu资源过度浪费的问题。10. ThreadLocal用完没清空在java中保证线程安全的技术有很多可以使用synchroized、Lock等关键字给代码块加锁。但是它们有个共同的特点就是加锁会对代码的性能有一定的损耗。其实在jdk中还提供了另外一种思想即用空间换时间。没错使用ThreadLocal类就是对这种思想的一种具体体现。ThreadLocal为每个使用变量的线程提供了一个独立的变量副本这样每一个线程都能独立地改变自己的副本而不会影响其它线程所对应的副本。ThreadLocal的用法大致是这样的先创建一个CurrentUser类其中包含了ThreadLocal的逻辑。public class CurrentUser { private static final ThreadLocalUserInfo THREA_LOCAL new ThreadLocal(); public static void set(UserInfo userInfo) { THREA_LOCAL.set(userInfo); } public static UserInfo get() { THREA_LOCAL.get(); } public static void remove() { THREA_LOCAL.remove(); } }在业务代码中调用CurrentUser类。public void doSamething(UserDto userDto) { UserInfo userInfo convert(userDto); CurrentUser.set(userInfo); ... //业务代码 UserInfo userInfo CurrentUser.get(); ... }在业务代码的第一行将userInfo对象设置到CurrentUser这样在业务代码中就能通过CurrentUser.get()获取到刚刚设置的userInfo对象。特别是对业务代码调用层级比较深的情况这种用法非常有用可以减少很多不必要传参。但在高并发的场景下这段代码有问题只往ThreadLocal存数据数据用完之后并没有及时清理。ThreadLocal即使使用了WeakReference弱引用也可能会存在内存泄露问题因为 entry对象中只把key(即threadLocal对象)设置成了弱引用但是value值没有。那么如何解决这个问题呢public void doSamething(UserDto userDto) { UserInfo userInfo convert(userDto); try{ CurrentUser.set(userInfo); ... //业务代码 UserInfo userInfo CurrentUser.get(); ... } finally { CurrentUser.remove(); } }需要在finally代码块中调用remove方法清理没用的数据。

相关新闻

PPG vs PPO:3 大核心差异解析与 2 阶段训练机制对样本效率的影响

PPG vs PPO:3 大核心差异解析与 2 阶段训练机制对样本效率的影响

PPG vs PPO:3 大核心差异解析与 2 阶段训练机制对样本效率的影响深度强化学习领域近年来涌现出多种改进算法,其中PPG(Phasic Policy Gradient)作为PPO(Proximal Policy Optimization)的进阶版本&#xff0c…

2026/7/6 1:43:45阅读更多 →
HarmonyKit | 鸿蒙新特性实践:ToolCard 统一卡片布局设计迭代

HarmonyKit | 鸿蒙新特性实践:ToolCard 统一卡片布局设计迭代

HarmonyKit | 鸿蒙新特性实践:ToolCard 统一卡片布局设计迭代 卡片的困境 工具卡片看起来是最简单的 UI 组件——一个图标、一个标题、一行描述。但当你的网格里有 10 张卡片,每张卡片的描述文字长度从 8 个字到 21 个字不等时,“简单"…

2026/7/6 1:43:45阅读更多 →
如何快速提升视频画质:终极AI媒体增强指南

如何快速提升视频画质:终极AI媒体增强指南

如何快速提升视频画质:终极AI媒体增强指南 【免费下载链接】Waifu2x-Extension-GUI Video, Image and GIF upscale/enlarge(Super-Resolution) and Video frame interpolation. Achieved with Waifu2x, Real-ESRGAN, Real-CUGAN, RTX Video Super Resolution VSR, S…

2026/7/6 1:43:45阅读更多 →
AkShare 1.18.64 实战:3种实时A股数据接口对比与IP封禁规避策略

AkShare 1.18.64 实战:3种实时A股数据接口对比与IP封禁规避策略

AkShare 1.18.64 实战:3种实时A股数据接口对比与稳定性优化策略在量化交易和数据分析领域,获取实时、准确的A股市场数据是构建有效策略的基础。Python生态中的AkShare库提供了多个数据源接口,但不同接口在数据质量、响应速度和稳定性上存在显…

2026/7/6 2:44:16阅读更多 →
MP1584 开关电源 PCB 布局 3 大核心要点:基于手册指南的噪声与散热优化实测

MP1584 开关电源 PCB 布局 3 大核心要点:基于手册指南的噪声与散热优化实测

MP1584 开关电源 PCB 布局 3 大核心要点:基于手册指南的噪声与散热优化实测在硬件设计中,开关电源的 PCB 布局往往决定了最终产品的性能和可靠性。MP1584 作为一款广泛应用于工业、汽车和消费电子领域的高频降压稳压器,其 PCB 设计尤其考验工…

2026/7/6 2:44:16阅读更多 →
程序员就业:从问题定位到方案成型

程序员就业:从问题定位到方案成型

《程序员就业:从问题定位到方案成型》看起来是个大话题,但真落到项目里,常常就是几个具体选择。下面我尽量按实际开发时会遇到的问题来讲。摘要这篇面向准备找工作、跳槽或转型的程序员,但不会把“程序员就业:从问题定…

2026/7/6 2:44:16阅读更多 →
AI智能体记忆架构实战:从向量存储到长期记忆系统设计

AI智能体记忆架构实战:从向量存储到长期记忆系统设计

🚀 30款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度 如果你正在开发一个AI智能体,无论是客服机器人、代码助手还是个人助理,一定遇到过这样的问题:用户…

2026/7/6 2:44:16阅读更多 →
《Python + Streamlit + DeepSeek API 实现一个本地文档问答助手》

《Python + Streamlit + DeepSeek API 实现一个本地文档问答助手》

Python Streamlit DeepSeek API 实现一个本地文档问答助手本文会从 0 到 1 实现一个可以运行的大模型文档问答小项目:上传 PDF 或 TXT 文档,输入问题后,程序会先从文档中检索相关片段,再调用大模型生成回答。摘要 很多大模型应用…

2026/7/6 2:44:16阅读更多 →
破局数据孤岛:制造业基于 Trino 与统一元数据构建灵活数据资产底座

破局数据孤岛:制造业基于 Trino 与统一元数据构建灵活数据资产底座

在智能制造转型的深水区,制造企业正面临严峻的“数据烟囱”困境。ERP中的订单数据、MES里的生产工序、IoT平台的海量传感器时序数据以及质检系统记录,往往分散在数十种异构系统中。传统ETL模式不仅耗时费力,还极易产生数据副本与一致性风险。…

2026/7/6 2:39:14阅读更多 →
从GitHub安全案例解析常见漏洞与防护实践

从GitHub安全案例解析常见漏洞与防护实践

1. 项目概述:从GitHub Trending看安全实战 最近在GitHub Trending上看到一个项目,叫 skills4/skills ,它因为一些安全漏洞案例被大家讨论。这其实是一个挺典型的场景:一个旨在展示或教授某种技能的仓库,本身却成了安…

2026/7/5 0:01:08阅读更多 →
MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

# MLT 2026启示:因果推理与概率建模驱动下一代LLM应用## 一、背景与挑战:从“黑箱预测”到“可信推理”2026年6月,第7届机器学习与趋势国际会议(MLT 2026)将在悉尼召开。会议议程中,“因果与可解释机器学习…

2026/7/5 0:01:08阅读更多 →
通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

1. 项目概述与漏洞背景最近在梳理一些历史OA系统的安全风险时,通达OA v11.6版本中的一个老漏洞又进入了我的视线。这个漏洞位于/general/bi_design/appcenter/report_bi.func.php文件中,是一个典型的SQL注入点。虽然这个漏洞的利用方式看起来并不复杂&am…

2026/7/6 0:10:35阅读更多 →
Seraphine:基于LCU API的英雄联盟智能游戏助手技术解析与应用指南

Seraphine:基于LCU API的英雄联盟智能游戏助手技术解析与应用指南

Seraphine:基于LCU API的英雄联盟智能游戏助手技术解析与应用指南 【免费下载链接】Seraphine 英雄联盟战绩查询工具 项目地址: https://gitcode.com/gh_mirrors/se/Seraphine 技术架构先行:官方接口的合规应用 你是否曾在BP阶段手忙脚乱&#x…

2026/7/6 0:03:39阅读更多 →
多协议远程连接管理工具mRemoteNG:告别混乱,统一你的远程桌面管理

多协议远程连接管理工具mRemoteNG:告别混乱,统一你的远程桌面管理

多协议远程连接管理工具mRemoteNG:告别混乱,统一你的远程桌面管理 【免费下载链接】mRemoteNG mRemoteNG is the next generation of mRemote, open source, tabbed, multi-protocol, remote connections manager. 项目地址: https://gitcode.com/gh_m…

2026/7/6 0:03:39阅读更多 →
COUNT(DISTINCT) 与 GROUP BY 去重统计:5 亿数据量下的性能实测与选型指南

COUNT(DISTINCT) 与 GROUP BY 去重统计:5 亿数据量下的性能实测与选型指南

COUNT(DISTINCT) 与 GROUP BY 去重统计:5 亿数据量下的性能实测与选型指南在数据分析和处理领域,去重统计是最基础也是最频繁使用的操作之一。当数据量达到亿级规模时,不同的去重统计方法在性能上可能产生天壤之别。本文将基于 5 亿行数据的实…

2026/7/6 0:03:39阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/5 1:30:27阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/5 3:48:10阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/5 3:48:09阅读更多 →