An in-depth introduction to thread pools



1. Thread

1. What is a thread?

Thread is the smallest unit that the operating system can perform calculation scheduling. It is included in the process and is the actual operating unit in the process. A thread refers to a single sequential control flow in a process. Multiple threads can run concurrently in a process, and each thread performs different tasks in parallel.

2. How to create a thread

2.1. Creating threads in JAVA

  
  
  
  
  
/** * 继承Thread类,重写run方法 */class MyThread extends Thread {    @Override    public void run() {        System.out.println("myThread..." + Thread.currentThread().getName());} }
/** * 实现Runnable接口,实现run方法 */class MyRunnable implements Runnable { @Override public void run() { System.out.println("MyRunnable..." + Thread.currentThread().getName());} }
/** * 实现Callable接口,指定返回类型,实现call方法 */class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "MyCallable..." + Thread.currentThread().getName();} }

2.2. Test it

  
  
  
  
  
public static void main(String[] args) throws Exception {    MyThread thread = new MyThread();    thread.run();   //myThread...main    thread.start(); //myThread...Thread-0        MyRunnable myRunnable = new MyRunnable();    Thread thread1 = new Thread(myRunnable);    myRunnable.run();   //MyRunnable...main    thread1.start();    //MyRunnable...Thread-1        MyCallable myCallable = new MyCallable();    FutureTask<String> futureTask = new FutureTask<>(myCallable);    Thread thread2 = new Thread(futureTask);    thread2.start();    System.out.println(myCallable.call());  //MyCallable...main    System.out.println(futureTask.get());   //MyCallable...Thread-2
}

2.3. Problem

Since we have created a thread, why are the results different when we call the method directly and when we call the start() method? Does new Thread() actually create a thread?

2.4. Problem analysis

When we call the method directly, we can see that it is the main thread of execution, and calling the start() method starts a new thread. This means that new Thread() does not create a thread, but creates a thread in start().
Then let’s take a look at the Thread class start() method:
  
  
  
  
  
class Thread implements Runnable { //Thread类实现了Runnalbe接口,实现了run()方法         private Runnable target;
public synchronized void start() { ...
boolean started = false; try { start0(); //可以看到,start()方法真实的调用时start0()方法 started = true; } finally { ... } } private native void start0(); //start0()是一个native方法,由JVM调用底层操作系统,开启一个线程,由操作系统过统一调度
@Override public void run() { if (target != null) { target.run(); //操作系统在执行新开启的线程时,回调Runnable接口的run()方法,执行我们预设的线程任务
} } }

2.5. Summary

  • JAVA cannot directly create threads to perform tasks. Instead, it calls the operating system to start the thread by creating a Thread object, and the operating system calls back the run() method of the Runnable interface to perform the task;
  • The way to implement Runnable separates the actual callback tasks to be executed by the thread, thereby decoupling the startup of the thread from the callback tasks;
  • The way to implement Callable is to use the Future mode to not only decouple the startup of the thread from the callback task, but also to obtain the execution result after the execution is completed;


2. Multi-threading

1. What is multithreading?

Multithreading refers to the technology that enables concurrent execution of multiple threads from software or hardware. The same thread can only process one task before processing the next task. Sometimes we need to process multiple tasks at the same time. In this case, we need to create multiple threads to process the tasks at the same time.

2. What are the benefits of multi-threading?

2.1. Serial processing

  
  
  
  
  
public static void main(String[] args) throws Exception {    System.out.println("start...");    long start = System.currentTimeMillis();    for (int i = 0; i < 5; i++) {        Thread.sleep(2000);  //每个任务执行2秒         System.out.println("task done..."); //处理执行结果    }    long end = System.currentTimeMillis();    System.out.println("end...,time = "  + (end - start));}//执行结果start...task done...task done...task done...task done...task done... end...,time = 10043

2.2. Parallel processing

  
  
  
  
  
public static void main(String[] args) throws Exception {    System.out.println("start...");    long start = System.currentTimeMillis();    List<Future> list = new ArrayList<>();
for (int i = 0; i < 5; i++) { Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(2000); //每个任务执行2秒 return "task done..."; }
}; FutureTask task = new FutureTask(callable); list.add(task); new Thread(task).start();
} list.forEach(future -> { try { System.out.println(future.get()); //处理执行结果 } catch (Exception e) { } }); long end = System.currentTimeMillis(); System.out.println("end...,time = " + (end - start));
} //执行结果 start... task done... task done... task done... task done... task done... end...,time = 2005

2.3. Summary

  • Multithreading can split a task into several subtasks, and multiple subtasks can be executed concurrently. Each subtask is a thread.
  • Multi-threading is to complete multiple tasks simultaneously, not to improve operating efficiency, but to improve resource usage efficiency to improve system efficiency.

2.4. Multi-threading issues

We can see in the above example that if we create a thread for every task, if there are many tasks, we will create a large number of threads, which may lead to the exhaustion of system resources. At the same time, we know that the execution of threads needs to seize CPU resources. If there are too many threads, a lot of time will be spent on thread switching overhead.
Furthermore, each task requires the creation of a thread, and creating a thread requires calling the underlying method of the operating system, which is expensive, and the thread is recycled after completion. When a large number of threads are needed, creating threads takes a lot of time.


3. Thread pool

1. How to design a thread pool

Since multi-threaded development has some of the above problems, can we design something to avoid these problems? Of course! The thread pool was born to solve these problems. So how should we design a thread pool to solve these problems? In other words, what functions should a thread pool have?

1.1. Basic functions of thread pool

  • Multi-threading will create a large number of threads to exhaust resources, so the thread pool should limit the number of threads to ensure that system resources are not exhausted;
  • Each time a new thread is created , it will increase the creation overhead, so the thread pool should reduce the creation of threads and try to reuse already created threads;

1.2. Thread pool faces problems

  • We know that threads will be recycled after completing their tasks, so how do we reuse threads?
  • We have specified the maximum number of threads. When the number of tasks exceeds the number of threads, how should we handle it?

1.3. Innovation comes from life

Let’s first assume a scenario: Suppose we are the managers of a logistics company. The goods to be delivered are our tasks, and the trucks are our delivery tools. Of course, we cannot prepare as many trucks as there are goods. So when customers continue to deliver goods to us, how should we manage them so that the company can run the best?
  • When the goods first arrived, we didn’t have trucks yet. We had to buy a truck for each batch of goods to be transported;
  • When the truck transportation is completed and the next batch of goods has not arrived yet, the truck will be parked in the warehouse and can be transported immediately when the goods arrive;
  • When we have a certain number of cars and we think they are enough, we will no longer buy cars. If new goods arrive at this time, we will put the goods in the warehouse first and wait for the cars to come back. delivery;
  • When the 618 big sale comes, there are too many goods to be delivered, the cars are on the road, and the warehouses are full. What should we do? We choose to temporarily rent some cars to help with delivery and improve the efficiency of delivery;
  • However, there are still too many goods. We have added temporary trucks, but they still cannot deliver them. At this time, we have no choice but to make customers wait in line or not accept them at all.
  • After the big sale was successfully completed, the accumulated goods have been delivered. In order to reduce costs, we returned all the temporarily rented cars;

1.4. Technology comes from innovation

Based on the above scenario, the logistics company is our thread pool, the goods are our thread tasks, and the trucks are our threads. How we design the company's process of managing trucks should be how we design the process of thread pool management threads.
  • When a task comes in and we don’t have a thread yet, we should create a thread to execute the task;
  • When the thread task is completed, the thread is not released and waits for the next task to come in and then execute it;
  • When the number of created threads reaches a certain amount, we store new tasks and wait for idle threads to execute, which requires the thread pool to have a container to store tasks;
  • When the container is full, we need to add some temporary threads to improve processing efficiency;
  • When a task cannot be processed after adding a temporary thread, the task should be rejected;
  • When all tasks are completed, the temporary threads should be released to avoid unnecessary overhead;

2. Detailed analysis of thread pool

Above, we talked about how to design a thread pool. Now let’s see how the master designed it;

2.1. How is the thread pool in JAVA designed?

2.1.1. Thread pool design

Take a look at the properties in the thread pool to understand the design of the thread pool.
  
  
  
  
  
public class ThreadPoolExecutor extends AbstractExecutorService {
//线程池的打包控制状态,用高3位来表示线程池的运行状态,低29位来表示线程池中工作线程的数量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //值为29,用来表示偏移量 private static final int COUNT_BITS = Integer.SIZE - 3;
//线程池的最大容量 private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//线程池的运行状态,总共有5个状态,用高3位来表示 private static final int RUNNING = -1 << COUNT_BITS; //接受新任务并处理阻塞队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS; //不接受新任务但会处理阻塞队列中的任务
private static final int STOP = 1 << COUNT_BITS; //不会接受新任务,也不会处理阻塞队列中的任务,并且中断正在运行的任务
private static final int TIDYING = 2 << COUNT_BITS; //所有任务都已终止, 工作线程数量为0,即将要执行terminated()钩子方法
private static final int TERMINATED = 3 << COUNT_BITS; // terminated()方法已经执行结束
//任务缓存队列,用来存放等待执行的任务 private final BlockingQueue<Runnable> workQueue;
//全局锁,对线程池状态等属性修改时需要使用这个锁 private final ReentrantLock mainLock = new ReentrantLock();
//线程池中工作线程的集合,访问和修改需要持有全局锁 private final HashSet<Worker> workers = new HashSet<Worker>();
// 终止条件 private final Condition termination = mainLock.newCondition();
//线程池中曾经出现过的最大线程数 private int largestPoolSize; //已完成任务的数量 private long completedTaskCount; //线程工厂 private volatile ThreadFactory threadFactory; //任务拒绝策略 private volatile RejectedExecutionHandler handler;
//线程存活时间 private volatile long keepAliveTime;
//是否允许核心线程超时 private volatile boolean allowCoreThreadTimeOut;
//核心池大小,若allowCoreThreadTimeOut被设置,核心线程全部空闲超时被回收的情况下会为0 private volatile int corePoolSize;
//最大池大小,不得超过CAPACITY private volatile int maximumPoolSize; //默认的任务拒绝策略 private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
//运行权限相关 private static final RuntimePermission shutdownPerm = new RuntimePermission("modifyThread");
... }
To summarize: From the above thread pool design, it can be seen that the function of the thread pool is still very complete.
  • Provides management of thread creation, number and survival time;
  • Provides management of thread pool status transfer;
  • Various containers for task caching are provided;
  • Provides a mechanism for handling redundant tasks;
  • Provides simple statistical functions ;

2.1.2. Thread pool constructor

  
  
  
  
  
//构造函数 public ThreadPoolExecutor(int corePoolSize, //核心线程数                            int maximumPoolSize, //最大允许线程数                            long keepAliveTime, //线程存活时间                            TimeUnit unit, //存活时间单位                            BlockingQueue<Runnable> workQueue, //任务缓存队列                           ThreadFactory threadFactory, //线程工厂                            RejectedExecutionHandler handler) { //拒绝策略     if (corePoolSize < 0 ||        maximumPoolSize <= 0 ||        maximumPoolSize < corePoolSize ||        keepAliveTime < 0)        throw new IllegalArgumentException();            if (workQueue == null || threadFactory == null || handler == null)        throw new NullPointerException();            this.corePoolSize = corePoolSize;    this.maximumPoolSize = maximumPoolSize;    this.workQueue = workQueue;    this.keepAliveTime = unit.toNanos(keepAliveTime);    this.threadFactory = threadFactory;    this.handler = handler;}
To summarize:
  • The constructor tells us how to use the thread pool and which features of the thread pool we can control;

2.1.3. Thread pool execution

2.1.3.1. Submit task method
  • public void execute(Runnable command);
  • Future<?> submit(Runnable task);
  • Future submit(Runnable task, T result);
  • Future submit(Callable task);
  
  
  
  
  
public Future<?> submit(Runnable task) {        if (task == null) throw new NullPointerException();        RunnableFuture<Void> ftask = newTaskFor(task, null);        execute(ftask);        return ftask;}
You can see that the underlying method of the submit method also calls the execute method, so we only analyze the execute method here;
  
  
  
  
  
    public void execute(Runnable command) {        if (command == null)            throw new NullPointerException();                int c = ctl.get();        //第一步:创建核心线程        if (workerCountOf(c) < corePoolSize) {  //worker数量小于corePoolSize            if (addWorker(command, true))       //创建worker                return;            c = ctl.get();        }        //第二步:加入缓存队列        if (isRunning(c) && workQueue.offer(command)) { //线程池处于RUNNING状态,将任务加入workQueue任务缓存队列            int recheck = ctl.get();                if (! isRunning(recheck) && remove(command))    //双重检查,若线程池状态关闭了,移除任务                reject(command);            else if (workerCountOf(recheck) == 0)       //线程池状态正常,但是没有线程了,创建worker                addWorker(null, false);        }        //第三步:创建临时线程        else if (!addWorker(command, false))            reject(command);    }
To summarize: the main functions of the execute() method:
  • If the number of core threads is insufficient, create core threads;
  • When the core thread is full, it is added to the cache queue;
  • When the cache queue is full, non-core threads are added;
  • If non-core threads are also full, the task will be rejected;
2.1.3.2. Create threads
private boolean addWorker(Runnable firstTask, boolean core) {        retry:        for (;;) {            int c = ctl.get();            int rs = runStateOf(c);
//等价于:rs>=SHUTDOWN && (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty()) //线程池已关闭,并且无需执行缓存队列中的任务,则不创建 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false;
for (;;) { int wc = workerCountOf(c); if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; if (compareAndIncrementWorkerCount(c)) //CAS增加线程数 break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } }
//上面的流程走完,就可以真实开始创建线程了 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { w = new Worker(firstTask); //这里创建了线程 final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); workers.add(w); //这里将线程加入到线程池中 int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { t.start(); //添加成功,启动线程 workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); //添加线程失败操作 } return workerStarted; }
Summary: The main functions of the addWorker() method;
  • Increase the number of threads;
  • Create a thread Worker instance and join the thread pool;
  • Join to complete the opening thread;
  • If the startup fails, the additional process will be rolled back;
2.1.3.3. Implementation of worker threads
    private final class Worker  //Worker类是ThreadPoolExecutor的内部类        extends AbstractQueuedSynchronizer          implements Runnable{                final Thread thread;    //持有实际线程        Runnable firstTask;     //worker所对应的第一个任务,可能为空        volatile long completedTasks;   //记录执行任务数
Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } public void run() { runWorker(this); //当前线程调用ThreadPoolExecutor中的runWorker方法,在这里实现的线程复用 }
...继承AQS,实现了不可重入锁... }

   
   
   
   
   
Summary: The main functions of the worker thread Worker class;
  • This class holds a working thread and continuously processes new tasks it obtains. The thread it holds is a reusable thread;
  • This class can be regarded as an adaptation class. In the run() method, the runWorker() method is actually called to continuously obtain new tasks and complete thread reuse;
2.1.3.4. Reuse of threads
final void runWorker(Worker w) {    //ThreadPoolExecutor中的runWorker方法,在这里实现的线程复用        Thread wt = Thread.currentThread();        Runnable task = w.firstTask;        w.firstTask = null;        w.unlock(); // allow interrupts        boolean completedAbruptly = true;   //标识线程是否异常终止        try {            while (task != null || (task = getTask()) != null) {    //这里会不断从任务队列获取任务并执行                w.lock();                                //线程是否需要中断                if ((runStateAtLeast(ctl.get(), STOP) ||                         (Thread.interrupted() &&                      runStateAtLeast(ctl.get(), STOP))) &&                    !wt.isInterrupted())                    wt.interrupt();                try {                    beforeExecute(wt, task);    //执行任务前的Hook方法,可自定义                    Throwable thrown = null;                    try {                        task.run();             //执行实际的任务                    } catch (RuntimeException x) {                        thrown = x; throw x;                    } catch (Error x) {                        thrown = x; throw x;                    } catch (Throwable x) {                        thrown = x; throw new Error(x);                    } finally {                        afterExecute(task, thrown); //执行任务后的Hook方法,可自定义                    }                } finally {                    task = null;    //执行完成后,将当前线程中的任务制空,准备执行下一个任务                    w.completedTasks++;                    w.unlock();                }            }            completedAbruptly = false;        } finally {            processWorkerExit(w, completedAbruptly);    //线程执行完成后的清理工作        }    }
Summary: The main functions of the runWorker() method;
  • 循环从缓存队列中获取新的任务,直到没有任务为止;
  • 使用worker持有的线程真实执行任务;
  • 任务都执行完成后的清理工作;
2.1.3.5、队列中获取待执行任务
  
  
  
  
  
private Runnable getTask() {        boolean timedOut = false;   //标识当前线程是否超时未能获取到task对象
for (;;) { int c = ctl.get(); int rs = runStateOf(c);
// Check if queue empty only if necessary. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; }
int wc = workerCountOf(c);
// Are workers subject to culling? boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) //若线程存活时间超时,则CAS减去线程数量 return null; continue; }
try { Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : //允许超时回收则阻塞等待 workQueue.take(); //不允许则直接获取,没有就返回null if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }
小结:getTask()方法主要功能;
实际在缓存队列中获取待执行的任务;
在这里管理线程是否要阻塞等待,控制线程的数量;
2.1.3.6、清理工作
private void processWorkerExit(Worker w, boolean completedAbruptly) {        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted            decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; workers.remove(w); //移除执行完成的线程 } finally { mainLock.unlock(); }
tryTerminate(); //每次回收完一个线程后都尝试终止线程池
int c = ctl.get(); if (runStateLessThan(c, STOP)) { //到这里说明线程池没有终止 if (!completedAbruptly) { int min = allowCoreThreadTimeOut ? 0 : corePoolSize; if (min == 0 && ! workQueue.isEmpty()) min = 1; if (workerCountOf(c) >= min) return; // replacement not needed } addWorker(null, false); //异常终止线程的话,需要在常见一个线程 } }
小结:processWorkerExit()方法主要功能;
  • 真实完成线程池线程的回收;
  • 调用尝试终止线程池;
  • 保证线程池正常运行;
2.1.3.7、尝试终止线程池
final void tryTerminate() {        for (;;) {            int c = ctl.get();                        //若线程池正在执行、线程池已终止、线程池还需要执行缓存队列中的任务时,返回            if (isRunning(c) ||                runStateAtLeast(c, TIDYING) ||                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))                return;                            //执行到这里,线程池为SHUTDOWN且无待执行任务 或 STOP 状态            if (workerCountOf(c) != 0) {                interruptIdleWorkers(ONLY_ONE);     //只中断一个线程                return;            }
//执行到这里,线程池已经没有可用线程了,可以终止了 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { //CAS设置线程池终止 try { terminated(); //执行钩子方法 } finally { ctl.set(ctlOf(TERMINATED, 0)); //这里将线程池设为终态 termination.signalAll(); } return; } } finally { mainLock.unlock(); } // else retry on failed CAS } }
小结:tryTerminate()方法主要功能;
  • 实际尝试终止线程池;
  • 终止成功则调用钩子方法,并且将线程池置为终态。

2.2、JAVA线程池总结

以上通过对JAVA线程池的具体分析我们可以看出,虽然流程看似复杂,但其实有很多内容都是状态重复校验、线程安全的保证等内容,其主要的功能与我们前面所提出的设计功能一致,只是额外增加了一些扩展,下面我们简单整理下线程池的功能;
2.2.1、主要功能
  • 线程数量及存活时间的管理;
  • 待处理任务的存储功能;
  • 线程复用机制功能;
  • 任务超量的拒绝功能;
2.2.2、扩展功能
  • 简单的执行结果统计功能;
  • 提供线程执行异常处理机制;
  • 执行前后处理流程自定义;
  • 提供线程创建方式的自定义;
2.2.3、流程总结
以上通过对JAVA线程池任务提交流程的分析我们可以看出,线程池执行的简单流程如下图所示;

2.3、JAVA线程池使用

线程池基本使用验证上述流程:
public static void main(String[] args) throws Exception {                //创建线程池       ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(               5, 10, 100, TimeUnit.SECONDS, new ArrayBlockingQueue(5));                //加入4个任务,小于核心线程,应该只有4个核心线程,队列为0        for (int i = 0; i < 4; i++) {            threadPoolExecutor.submit(new MyRunnable());        }        System.out.println("worker count = " + threadPoolExecutor.getPoolSize());   //worker count = 4        System.out.println("queue size = " + threadPoolExecutor.getQueue().size()); //queue size = 0                //再加4个任务,超过核心线程,但是没有超过核心线程 + 缓存队列容量,应该5个核心线程,队列为3        for (int i = 0; i < 4; i++) {            threadPoolExecutor.submit(new MyRunnable());        }        System.out.println("worker count = " + threadPoolExecutor.getPoolSize());   //worker count = 5        System.out.println("queue size = " + threadPoolExecutor.getQueue().size()); //queue size = 3                //再加4个任务,队列满了,应该5个热核心线程,队列5个,非核心线程2个        for (int i = 0; i < 4; i++) {            threadPoolExecutor.submit(new MyRunnable());        }        System.out.println("worker count = " + threadPoolExecutor.getPoolSize());   //worker count = 7        System.out.println("queue size = " + threadPoolExecutor.getQueue().size()); //queue size = 5                //再加4个任务,核心线程满了,应该5个热核心线程,队列5个,非核心线程5个,最后一个拒绝        for (int i = 0; i < 4; i++) {            try {                threadPoolExecutor.submit(new MyRunnable());            } catch (Exception e) {                e.printStackTrace();    //java.util.concurrent.RejectedExecutionException            }        }        System.out.println("worker count = " + threadPoolExecutor.getPoolSize());   //worker count = 10        System.out.println("queue size = " + threadPoolExecutor.getQueue().size()); //queue size = 5        System.out.println(threadPoolExecutor.getTaskCount());  //共执行15个任务                //执行完成,休眠15秒,非核心线程释放,应该5个核心线程,队列为0        Thread.sleep(1500);        System.out.println("worker count = " + threadPoolExecutor.getPoolSize());   //worker count = 5        System.out.println("queue size = " + threadPoolExecutor.getQueue().size()); //queue size = 0                //关闭线程池        threadPoolExecutor.shutdown();    }
-end-

本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

雷军公布小米澎湃 OS 完整系统架构,称底层全部重构 语雀 10 月 23 日故障原因及修复过程公告 微软 CEO 纳德拉:放弃 Windows Phone 和移动业务是错误决策 Java 11 和 Java 17 使用率均超 Java 8 Hugging Face 被限制访问 语雀网络故障持续大约 10 小时,现已恢复正常 国家数据局正式揭牌 Oracle 推出 Visual Studio Code 的 Java 开发扩展 马斯克:如果维基百科改名“维鸡百科”就捐款 10 亿美元 MySQL 8.2.0 GA
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10136833