Java线程池运行原理,线程池源码解读【Java线程池学习二】

news2024/9/21 8:40:49

一、前奏

有了上一篇博文的学习,相信你对于线程池的使用这块已经不在存在什么问题了,日常开发和面试也都足够了。 线程池最优使用策略【Java线程池学习一】

但随着时间的推移在闲下来的时候我突然想,当任务进入了队列之后是怎么取出来的呢?然后列举了几个问题

  1. 添加的一个任务是怎么运行的?
  2. 任务丢到了队列,怎么取出来呢?
  3. 过了时间怎么销毁线程?
  4. 怎么拒绝的?
  5. 线程池,这个池是什么? 线程怎么放进去?

毫无疑问想要解决上面的问题,那只有研究源码,下面我们就来看下 ThreadPoolExecutor 的源码,此次目的就是解决上面的问题,先对线程池的核心工作原理进行理解,后面我们再来对线程池来一个全面的解读。


二、开始

public static void main(String[] args) throws InterruptedException {

    ThreadPoolExecutor executor = new ThreadPoolExecutor(1,2, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));

    executor.execute(()->{
        try {
            Thread.sleep(1000 * 6);
            System.out.println("task-1 " + Thread.currentThread().getName()+ " " + Thread.currentThread().hashCode());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    executor.execute(()->{
        System.out.println("task-2 " + Thread.currentThread().getName()+ " " + Thread.currentThread().hashCode());
    });

    executor.execute(()->{
        System.out.println("task-3 " + Thread.currentThread().getName()+ " " + Thread.currentThread().hashCode());
    });

    Thread.sleep(1000 * 10);

    executor.execute(()->{
        System.out.println("task-4 " + Thread.currentThread().getName()+ " " + Thread.currentThread().hashCode());
    });
}

上面的代码案例很简单,现在我们用上一篇博文的知识来解读一下这段代码:

  1. task-1 进入线程池,这时候线程池里面还没有线程,所以会启一个线程去执行【thread-1】
  2. task-2 进入线程池,虽然线程池里面有一个【thread-1】,但是它被上一个任务阻塞着,所以 task-2进入队列
  3. task-3 进入线程池,这时候队列满了,【thread-1】还在阻塞中,所以会开启一个新的线程去执行【thread-2】
  4. task-3 执行完毕后,队列中还有一个 task-2,所以【thread-2】会去执行 task-2
  5. 执行完 task-2 之后,我们暂停添加任务,【thread-2】会在等待 1s 之后就销毁了
  6. 这时候 task-1执行完毕, 过了一会我们又添加了一个 task-4 到线程池,【thread-1】是长期存活的它会去执行 task-4

打印结果如下:

task-3 pool-1-thread-2 448689266
task-2 pool-1-thread-2 448689266
task-1 pool-1-thread-1 1721383753
task-4 pool-1-thread-1 1721383753

三、分析

注:ThreadPoolExecutor 的源码里面用到了 与或非 左移右移 运算,理解起来比较困难,但又不是业务的主流程,所以我在后面进行简化。


3-1、execute(Runnable command)

上面的案例代码我们只是调用了线程池的 execute 方法 ,所以我们直接来看看这个方法。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
        
   /**
    * workerCountOf(c) 就是返回的当前线程池存在的线程数
    * 
    * 如果小于核心线程数就进行 addWorker ,把任务添加进去,添加成功就结束(注这个addWorker并不是加入队列)
    */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    
   /**
    * 当前线程池没有在跑,并成功把当前任务放入队列中
    *
    * 插入成功后会检查一下当前线程池的运行情况,如果isRunning返回false说明线程池已经超负荷运行了 就删除任务 并拒绝任务
    *
    * 如果上一步没有问题,就再判断一下当然线程池是否有在运行的线程,没有就开启一个,下面我们会详细说明【addWorker】
    */
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    
   /**
    * 如果插入队列失败了(以超过核心线程数来工作),会尝试直接运行该任务,如果还是失败,就拒绝
    */
    else if (!addWorker(command, false))
        reject(command);
}
  1. offer 方法是在队列允许的范围内把数据插入进去,如果容量不足就返回false
  2. 线程池的实时性很高,所以可以看到它的源码里面做了很多补偿的机制,比如任务插入到队列中去后还会再判断线程池的状态是否正常。(如果在插入成功的一瞬间线程池已经终止了就会出问题)

3-2、 addWorker(Runnable firstTask, boolean core)

  • firstTask 就是当前任务,当然它可以为null,如果是null就是让当前线程去执行队列中其它任务(后面会看到)
  • core 判断线程的数量是在 核心线程数量还是最大线程数量的范围 (true 就是核心线程数量范围内)
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 检查特殊情况下队列是否为空
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            // wc 为当前线程池的线程数
            int wc = workerCountOf(c);
            // 如果线程数大于最大容量 或 大于规定容量就返回 false
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 使用CAS去让当前线程数+1 成功则【整个退出】    
            if (compareAndIncrementWorkerCount(c))
                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 {
        // Worker 是private final修饰的内部类,实现了 Runnable接口,里面有【当前任务和当前线程】下面详解
        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());
               /**
                * 判断当前任务是否符合加入到任务中的条件
                *
                * rs 是一个很大的负数 可以理解规定能最大创建的线程数【536870911】 所以不用考虑,不会创建那么多
                * SHUTDOWN 是个常数 0
                */
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    // 判断当前线程是否可以被 开启/start
                    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;
}
  1. 关于 t.isAlive() 可以参看 https://www.javatpoint.com/java-thread-isalive-method

3-3、Worker

现在我们来看一下Worker这个内部类,上面我们也说了Worker 实现了Runnable,并且在最后的一步中调用了 start方法。

它继承了AbstractQueuedSynchronizer 实现了 Runnable,但代码很简单,定义了三个参数,代码比较简单直接看。

  • thread 执行当前任务的线程
  • firstTask 当前任务
  • completedTasks 该线程执行成功的任务数 (会统计整个线程池执行成功的任务数)
private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable {
 
    private static final long serialVersionUID = 6138294804551838833L;

    final Thread thread;

    Runnable firstTask;

    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);
    }

    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.

    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

我们单独来看一下它的构造方法,任务是由外面传递过来的,线程是每次创建一个新的,上面我们在创建的时候没有指定ThreadFactory,则用的默认的 Executors.DefaultThreadFactory

Worker(Runnable firstTask) {
      setState(-1); // inhibit interrupts until runWorker
      this.firstTask = firstTask;
      this.thread = getThreadFactory().newThread(this);
  }

3-4、runWorker(Worker w)

3-2 里面我们调用了 worker 的 start方法,后续肯定就会调用 run 方法了,也就是 runWorker 方法

public void run() {
    runWorker(this);
}
final void runWorker(Worker w) {
    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 pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                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);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}
  1. 它在执行之前和之后留了两个钩子 beforeExecute、afterExecute 里面没有任何实现是空方法,可以留给后续扩展。
  2. 我们以为当前线程只是去执行当前的 任务/task, 但其实不是 task != null || (task = getTask()) != null 它先去执行自己的任务,如果自己的任务执行完了,就会执行 getTask 从队列中拿任务直到队列中没有任务为止。
  3. 我们在上面也看到调用 addWorker方法的时候 任务传递的是 null,其实就是让它去执行队列中的任务。

3-5、getTask()

该方法如同它自己的名【获取任务】,它的作用就是从队列中获取任务。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 特殊情况下校验队列是不是为空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

       /**
        * allowCoreThreadTimeOut 是否允许核心线程超时 默认 false
        * wc > corePoolSize  当前活跃线程是否大于核心线程数
        * 
        * 其实就是相当于只有超过核心线程数的线程才设置超时时间
        */
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        
        /**
         * 如果当前线程数 > 最大线程数 就退出(销毁当前线程)
         * 或 当前线程是设置了超时的,并且已经超时了 就退出(销毁当前线程)
         */
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
           /**
            * 从队列中获取数据
            * 
            * poll 等待固定时间
            * take 一直阻塞,直到获取到了任务数据
            */
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

take方法 一直阻塞,直到获取到了任务数据


四、问题剖析

4-1、添加的一个任务是怎么运行的?

  1. 黑色的线是正常添加一个任务的流程
  2. 红色的线是线程组里面的线程一直阻塞获取队列中的数据

在这里插入图片描述


4-2、任务丢到队列,怎么取出来呢?

下面是截取 getTask() 里面的一段代码

 Runnable r = timed ?
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();

简单理解就是 非核心线程调用 poll 方法,核心线程调用 take 方法,take方法是阻塞的,获取不到数据会一直等着。


4-3、过了时间怎么销毁线程?

咱们把 getTask 方法复制过来,删掉多余的部分

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

我们知道超过核心线程的部分会调用 poll 方法,如果超过时间没有获取到对应的任务: timedOut = true

如果 timedOut = true,那上面的if判断就会进去,当前死循环就结束了,线程也就正常结束。


4-4、怎么拒绝的?

拒绝这个就很简单了,在excute方法的讲解里面已经说了。


4-5、线程池,这个池是什么? 线程怎么放进去?

再来回顾一下是怎么创建线程的,在进入 addWorker 方法的时候会进行一系列的判断,当判断通过后就会创建一个 w = new Worker(firstTask);

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

找到这个 Worker 的构造方法再来看一下,前面我们也说了默认的线程工厂是Executors.DefaultThreadFactory,来看看它的 newThread 方法

DefaultThreadFactory

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

可以看到其实每次都是 new Thread,但是它的 ThreadGroup 是同一个。

线程启动的时候是调用的 Thread.start() 方法,隐藏其它方法,里面有一个 把当前线程添加到线程组里面。

public synchronized void start() {
    // ...
    
    group.add(this);

   // ...
}

ThreadGroup.add
int nthreads;
Thread threads[];

void add(Thread t) {
    synchronized (this) {
        if (destroyed) {
            throw new IllegalThreadStateException();
        }
        if (threads == null) {
            threads = new Thread[4];
        } else if (nthreads == threads.length) {
            threads = Arrays.copyOf(threads, nthreads * 2);
        }
        threads[nthreads] = t;
        nthreads++;
        nUnstartedThreads--;
    }
}

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

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

相关文章

linux系统根文件系统构建

根文件系统构建 一、根文件系统简介 根文件系统是 Linux 内核启动以后挂载(mount)的第一个文件系统&#xff0c;从根文件系统中读取初始化脚本&#xff0c;比如 rcS&#xff0c;inittab 等。根文件系统和 Linux 内核是分开的&#xff0c;单独的 Linux 内核是没法正常工作的&a…

快捷获取GDI+绘图参数的两种经验方案

文章目录一、使用系统的枚举二、专用枚举1、颜色Color2、字体Font3、字体名称4、笔刷Brush5、笔Pen6、矩形Rectangle7、点Point8、大小Size文章出处&#xff1a; https://blog.csdn.net/haigear/article/details/129085403在绘图中&#xff0c;常常需要给出颜色&#xff0c;字体…

目标检测各常见评价指标详解

注&#xff1a;本文仅供学习&#xff0c;未经同意请勿转载 说明&#xff1a;该博客来源于xiaobai_Ry:2020年3月笔记 对应的PDF下载链接在&#xff1a;待上传 目录 常见的评价指标 准确率 &#xff08;Accuracy&#xff09; 混淆矩阵 &#xff08;Confusion Matrix&#xff…

SpringBoot实现统一返回接口(除AOP)

起因 关于使用AOP去实现统一返回接口在之前的博客中我们已经实现了&#xff0c;但我突然突发奇想&#xff0c;SpringBoot中异常类的统一返回好像是通过RestControllerAdvice 这个注解去完成的&#xff0c;那我是否也可以通过这个注解去实现统一返回接口。 正文 这个方法主要…

Django框架之模型视图--HttpResponse对象

HttpResponse对象 视图在接收请求并处理后&#xff0c;必须返回HttpResponse对象或子对象。HttpRequest对象由Django创建&#xff0c;HttpResponse对象由开发人员创建。 1 HttpResponse 可以使用django.http.HttpResponse来构造响应对象。 HttpResponse(content响应体, con…

【opencv源码解析0.2】opencv库源码编译

如何编译opencv库源码 大家好&#xff0c;我是周旋&#xff0c;感谢大家学习【opencv源码解析】系列&#xff0c;本系列首发于公众号【周旋机器视觉】。 上篇文章我们介绍了如何配置opencv环境&#xff0c;搞清了opencv的包含目录include、静态库链接以及动态库链接的作用。 【…

(考研湖科大教书匠计算机网络)第五章传输层-第四节:TCP流量控制

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;流量控制概述二&#xff1a;流量控制举例三&#xff1a;拓展阅读&#xff08;可不看&#xff09;&#xff08;1&#xff09;TCP流量控制完整例子&a…

马上卸载这个恶心的软件!

大家好&#xff0c;我是良许。 春节已经过完了&#xff0c;但在这喜庆的日子里&#xff0c;又有一个小丑在上窜下跳了。 没错&#xff0c;这个不要脸的小丑依然还是 Notepad 的作者。 好好的一个开发者&#xff0c;为何老喜欢整一些有得没得的东西&#xff1f;好好搬砖写代码…

pygame8 扫雷游戏

一、游戏规则&#xff1a; 1、点击方格&#xff0c;如果是地雷&#xff0c;游戏失败&#xff0c;找到所有地雷游戏胜利 2、如果方块上出现数字&#xff0c;则表示在其周围的八个方块中共有多少颗地雷 二、游戏主逻辑&#xff1a; 主要逻辑即调用run_game, 然后循环检测事件…

云计算|OpenStack|社区版OpenStack---基本概念科普(kvm的驱动类别和安装)

前言&#xff1a; 云计算里基本都是基于kvm技术作为底层支撑&#xff0c;但&#xff0c;该技术是比较复杂的&#xff0c;首先&#xff0c;需要硬件的 支撑&#xff0c;表现在物理机上&#xff0c;就是需要在BIOS中调整设置虚拟化功能&#xff0c;这个虚拟机功能通常是interVT或…

Fastjson2基础使用以及底层序列化/反序列化实现探究

1 Fastjson2简介 Fastjson2是Fastjson的升级版&#xff0c;特征&#xff1a; 协议支持&#xff1a;支持JSON/JSONB两种协议部分解析&#xff1a;可以使用JSONPath进行部分解析获取需要的值语言支持&#xff1a;Java/Kotlin场景支持&#xff1a;Android8/服务端其他特性支持&a…

python基础知识有哪些需要背(记住是基础知识)我是初学者

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;一个有趣的事情&#xff0c;一个有趣的事情&#xff0c;今天让我们一起来看看吧&#xff01; 1、python基础知识有哪些需要背&#xff08;记住是基础知识&#xff09;我是初学者 或看好Python的广阔前景&#xff0c;或…

RabbitMQ 入门到应用 ( 五 ) 应用

6.更多应用 6.1.AmqpAdmin 工具类 可以通过Spring的Autowired 注入 AmqpAdmin 工具类 , 通过这个工具类创建 队列, 交换机及绑定 import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Di…

python基于django微信小程序的适老化老人健康预警小程序

随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理系统应运而生,各行各业相继进入信息管理时代, 适老化老人健康预警微信小程序就是信息时代变革中的产物之一。 任何系统都要遵…

Spring国际化实现

Java国际化 Java使用Unicode来处理所有字符。 Locales 国际化主要涉及的是数字、日期、金额等。 有若干个专门负责格式处理的类。为了对格式进行控制&#xff0c;可以使用Locale类。它描述了&#xff1a; 一种语言一个位置(通常包含)一段脚本(可选&#xff0c;自Java SE7开…

CMMI之需求开发流程

需求开发&#xff08;Requirement Development, RD&#xff09;的目的是通过调查与分析&#xff0c;获取用户需求并定义产品需求。需求开发过程域是SPP模型的重要组成部分。本规范阐述了需求开发过程域的两个主要规程&#xff1a; 需求调查 [SPP-PROC-RM-SURVEY] 需求定义 [SPP…

消失的数字【C语言】

题目&#xff1a; 数组nums包含从0到n的所有整数&#xff0c;但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗&#xff1f; 解法&#xff1a; int missingNumber(int* nums, int numsSize){int val0;for(int i0;i<numsSize;i){val^nums[i];}fo…

Vue3之条件渲染

1.何为条件渲染 条件渲染就是在指定的条件下&#xff0c;渲染出指定的UI。比如当我们显示主页的时候&#xff0c;应该隐藏掉登录等一系列不相干的UI元素。即UI元素只在特定条件下进行显示。而在VUE3中&#xff0c;这种UI元素的显示和隐藏可以通过两个关键字&#xff0c;v-if 和…

Qt动画框架详解

目录1.前言2.原理3.属性动画4.并行执行的动画5.顺序执行的动画6.扩展属性动画支持的数据类型1.前言 为软件适当的添加一些动画&#xff0c;能够提高软件的用户体验。在使用Qt框架开发软件时&#xff0c;我们可以用Qt提供的动画框架来为QWidget等UI元素添加动画效果。本文从动画…

程序员和他的女朋友一起创建了价值 150,000,000 美元的网站

本篇文章讲述了Otis和Elizabeth Chandler创办Goodreads.com的故事。他们从小就爱读书&#xff0c;创办网站前他们的困惑是没有很多人在线分享书评。Otis和Elizabeth觉得如果有一个地方把所有人的评论和评价收集起来&#xff0c;那将会很有价值。奥蒂斯和伊丽莎白从小就喜欢读书…