CountDownLatch和CyclicBarrier介绍

概述

JDK中提供了一些用于线程之间协同等待的工具类,CountDownLatchCyclicBarrier就是最典型的两个线程同步辅助类。下面分别详细介绍这两个类,以及他们之间的异同点。

CountDownLatch类

CountDownLatch顾名思义:倒计数锁存器。没错,他就是一个计数器,并且是倒着计数的。他的应用场景如下:

一个任务A,他需要等待其他的一些任务都执行完毕之后它才能执行。就比如说赛跑的时候,发令员需要等待所有运动员都准备好了才能发令,否则不被K才怪嘞!

此时CountDownLatch就可以大展身手了。

常用操作

本节介绍CountDownLatch的基本操作函数。

构造函数

函数签名如下:

public CountDownLatch(int count)

用一个给定的数值初始化CountDownLatch,之后计数器就从这个值开始倒计数,直到计数值达到零。

await函数

await函数用两种形式,签名分别如下:

public void await() throws InterruptedException
public boolean await(long timeout, TimeUnit unit)

这两个函数的作用都是让线程阻塞等待其他线程,直到CountDownLatch的计数值变为0才继续执行之后的操作。区别在于第一个函数没有等待时间限制,可以等到天荒地老,海枯石烂,第二个函数给定一个等待超时时间,超过该时间就直接放弃了,并且第二个函数具有返回值,超时时间之内CountDownLatch的值达到0就返回true,等待时间结束计数值都还没达到0就返回false。这两个操作在等待过程中如果等待的线程被中断,则会抛出InterruptedException异常。

countDown函数

这个函数用来将CountDownLatch的计数值减一,函数签名如下:

public void countDown()

需要说明的是,如果调用这个函数的时候CountDownLatch的计数值已经为0,那么这个函数什么也不会做。

getCount函数

该函数用来获取当前CountDownLatch的计数值,函数签名如下:

public void countDown()

模拟实验

理论知识讲完了,需要真枪实战地来演示一下这个类的作用,我们就以下面这个场景为例子,用CountDownLatch来实现这个需求:

有5个运动员赛跑,开跑之前,裁判需要等待5个运动员都准备好才能发令,并且5个运动员准备好之后也都需要等待裁判发令才能开跑。

首先分析一下依赖关系:

裁判发令 -> 5个运动员都准备好;
5个运动员开跑 -> 裁判发令。

好,依赖关系已经出来了,代码实现:

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * @author qifuguang
 * @date 15/8/24 23:35
 */
public class TestCountDownLatch {
    private static final int RUNNER_NUMBER = 5; // 运动员个数
    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
        // 用于判断发令之前运动员是否已经完全进入准备状态,需要等待5个运动员,所以参数为5
        CountDownLatch readyLatch = new CountDownLatch(RUNNER_NUMBER);
        // 用于判断裁判是否已经发令,只需要等待一个裁判,所以参数为1
        CountDownLatch startLatch = new CountDownLatch(1);
        for (int i = 0; i < RUNNER_NUMBER; i++) {
            Thread t = new Thread(new Runner((i + 1) + "号运动员", readyLatch, startLatch));
            t.start();
        }
        try {
            readyLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        startLatch.countDown();
        System.out.println("裁判:所有运动员准备完毕,开始...");
    }

    static class Runner implements Runnable {
        private CountDownLatch readyLatch;
        private CountDownLatch startLatch;
        private String name;

        public Runner(String name, CountDownLatch readyLatch, CountDownLatch startLatch) {
            this.name = name;
            this.readyLatch = readyLatch;
            this.startLatch = startLatch;
        }

        public void run() {
            int readyTime = RANDOM.nextInt(1000);
            System.out.println(name + ":我需要" + readyTime + "秒时间准备.");
            try {
                Thread.sleep(readyTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + ":我已经准备完毕.");
            readyLatch.countDown();
            try {
                startLatch.await();  // 等待裁判发开始命令
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + ":开跑...");
        }
    }
}

运行结果如下:

1号运动员:我需要389秒时间准备.
2号运动员:我需要449秒时间准备.
3号运动员:我需要160秒时间准备.
4号运动员:我需要325秒时间准备.
5号运动员:我需要978秒时间准备.
3号运动员:我已经准备完毕.
4号运动员:我已经准备完毕.
1号运动员:我已经准备完毕.
2号运动员:我已经准备完毕.
5号运动员:我已经准备完毕.
裁判:所有运动员准备完毕,开始...
1号运动员:开跑...
5号运动员:开跑...
2号运动员:开跑...
4号运动员:开跑...
3号运动员:开跑...

可以看到,一切都是如此地完美,运动员准备好了之后裁判才发令,裁判发令之后运动员才开跑。

CyclicBarrier类

CyclicBarrier翻译过来就是:循环的屏障。什么是循环?可以重复利用呗,对这个类就是一个可以重复利用的屏障类。CyclicBarrier主要用于一组固定大小的线程之间,各个线程之间相互等待,当所有线程都完成某项任务之后,才能执行之后的任务。
如下场景:

有若干个线程都需要向一个数据库写数据,但是必须要所有的线程都讲数据写入完毕他们才能继续做之后的事情。

分析一下这个场景的特征:

  • 各个线程都必须完成某项任务(写数据)才能继续做后续的任务;
  • 各个线程需要相互等待,不能独善其身。

这种场景便可以利用CyclicBarrier来完美解决。

常用函数

本节介绍CyclicBarrier的基本操作函数。

构造函数

有两种类型的构造函数,函数签名分别如下:

public CyclicBarrier(int parties, Runnable barrierAction)
public CyclicBarrier(int parties)

参数parties表示一共有多少线程参与这次“活动”,参数barrierAction是可选的,用来指定当所有线程都完成这些必须的“神秘任务”之后需要干的事情,所以barrierAction这里的动作在一个相互等待的循环内只会执行一次。

getParties函数

getParties用来获取当前的CyclicBarrier一共有多少线程参数与,函数签名如下:

public int getParties()

返回参与“活动”的线程个数。

await函数

await函数用来执行等待操作,有两种类型的函数签名:

public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException 

第一个函数是一个无参函数,第二个函数可以指定等待的超时时间。它们的作用是:一直等待知道所有参与“活动”的线程都调用过await函数,如果当前线程不是即将调用await函数的的最后一个线程,当前线程将会被挂起,直到下列某一种情况发生:

  • 最后一个线程调用了await函数;
  • 某个线程打断了当前线程;
  • 某个线程打断了其他某个正在等待的线程;
  • 其他某个线程等待时间超过给定的超时时间;
  • 其他某个线程调用了reset函数。

如果等待过程中线程被打断了,则会抛出InterruptedException异常;
如果等待过程中出现下列情况中的某一种情况,则会抛出BrokenBarrierException异常:

  • 其他线程被打断了;
  • 当前线程等待超时了;
  • 当前CyclicBarrier被reset了;
  • 等待过程中CyclicBarrier损坏了;
  • 构造函数中指定的barrierAction在执行过程中发生了异常。

如果等待时间超过给定的最大等待时间,则会抛出TimeoutException异常,并且这个时候其他已经嗲用过await函数的线程将会继续后续的动作。

返回值:返回当前线程在调用过await函数的所以线程中的编号,编号为parties-1的表示第一个调用await函数,编号为0表示是最后一个调用await函数。

isBroken函数

给函数用来判断barrier是否已经损坏,函数签名如下:

public boolean isBroken()

如果因为任何原因被损坏返回true,否则返回false

reset函数

顾名思义,这个函数用来重置barrier,函数签名如下:

public void reset()

如果调用了该函数,则在等待的线程将会抛出BrokenBarrierException异常。

getNumberWaiting函数

该函数用来获取当前正在等待该barrier的线程数,函数签名如下:

public int getNumberWaiting()

模拟实验

下面用代码实现下面的场景:

有5个线程都需要向一个数据库写数据,但是必须要所有的线程都讲数据写入完毕他们才能继续做之后的事情。

一般情况

代码:

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author qifuguang
 * @date 15/8/25 00:34
 */
public class TestCyclicBarrier {
    private static final int THREAD_NUMBER = 5;
    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(THREAD_NUMBER, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴写入数据完毕");
            }
        });
        for (int i = 0; i < THREAD_NUMBER; i++) {
            Thread t = new Thread(new Worker(barrier));
            t.start();
        }
    }

    static class Worker implements Runnable {
        private CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        public void run() {
            int time = RANDOM.nextInt(1000);
            System.out.println(Thread.currentThread().getId() + ":我需要" + time + "毫秒时间写入数据.");
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":写入数据完毕,等待其他小伙伴...");
            try {
                barrier.await(); // 等待所有线程都调用过此函数才能进行后续动作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":所有线程都写入数据完毕,继续干活...");
        }
    }
}

运行结果如下:

10:我需要16毫秒时间写入数据.
11:我需要353毫秒时间写入数据.
12:我需要101毫秒时间写入数据.
13:我需要744毫秒时间写入数据.
14:我需要51毫秒时间写入数据.
10:写入数据完毕,等待其他小伙伴...
14:写入数据完毕,等待其他小伙伴...
12:写入数据完毕,等待其他小伙伴...
11:写入数据完毕,等待其他小伙伴...
13:写入数据完毕,等待其他小伙伴...
13:我宣布,所有小伙伴写入数据完毕
13:所有线程都写入数据完毕,继续干活...
10:所有线程都写入数据完毕,继续干活...
12:所有线程都写入数据完毕,继续干活...
14:所有线程都写入数据完毕,继续干活...
11:所有线程都写入数据完毕,继续干活...

可以看到,线程小伙伴们非常团结,写完自己的数据之后都在等待其他小伙伴,所有小伙伴都完成之后才继续后续的动作。

重复使用

上面的例子并没有体现CyclicBarrier可以循环使用的特点,所以有如下代码:

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author qifuguang
 * @date 15/8/25 00:34
 */
public class TestCyclicBarrier {
    private static final int THREAD_NUMBER = 5;
    private static final Random RANDOM = new Random();

    public static void main(String[] args) throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(THREAD_NUMBER, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴写入数据完毕");
            }
        });
        for (int i = 0; i < THREAD_NUMBER; i++) {
            Thread t = new Thread(new Worker(barrier));
            t.start();
        }
        Thread.sleep(10000);
        System.out.println("================barrier重用==========================");
        for (int i = 0; i < THREAD_NUMBER; i++) {
            Thread t = new Thread(new Worker(barrier));
            t.start();
        }
    }

    static class Worker implements Runnable {
        private CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        public void run() {
            int time = RANDOM.nextInt(1000);
            System.out.println(Thread.currentThread().getId() + ":我需要" + time + "毫秒时间写入数据.");
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":写入数据完毕,等待其他小伙伴...");
            try {
                barrier.await(); // 等待所有线程都调用过此函数才能进行后续动作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":所有线程都写入数据完毕,继续干活...");
        }
    }
}

运行结果:

10:我需要228毫秒时间写入数据.
11:我需要312毫秒时间写入数据.
12:我需要521毫秒时间写入数据.
13:我需要720毫秒时间写入数据.
14:我需要377毫秒时间写入数据.
10:写入数据完毕,等待其他小伙伴...
11:写入数据完毕,等待其他小伙伴...
14:写入数据完毕,等待其他小伙伴...
12:写入数据完毕,等待其他小伙伴...
13:写入数据完毕,等待其他小伙伴...
13:我宣布,所有小伙伴写入数据完毕
13:所有线程都写入数据完毕,继续干活...
10:所有线程都写入数据完毕,继续干活...
11:所有线程都写入数据完毕,继续干活...
14:所有线程都写入数据完毕,继续干活...
12:所有线程都写入数据完毕,继续干活...
================barrier重用==========================
15:我需要212毫秒时间写入数据.
16:我需要691毫秒时间写入数据.
17:我需要530毫秒时间写入数据.
18:我需要758毫秒时间写入数据.
19:我需要604毫秒时间写入数据.
15:写入数据完毕,等待其他小伙伴...
17:写入数据完毕,等待其他小伙伴...
19:写入数据完毕,等待其他小伙伴...
16:写入数据完毕,等待其他小伙伴...
18:写入数据完毕,等待其他小伙伴...
18:我宣布,所有小伙伴写入数据完毕
18:所有线程都写入数据完毕,继续干活...
15:所有线程都写入数据完毕,继续干活...
19:所有线程都写入数据完毕,继续干活...
16:所有线程都写入数据完毕,继续干活...
17:所有线程都写入数据完毕,继续干活...

可以看到,barrier的确是重用了。

等待超时

如果await的时候设置了一个最长等待时间,并且等待超时,则会怎么样呢?下面的例子故意让一个线程延迟一段时间才开始写数据,这样就会出现先等待的线程等待最后一个线程抛出等待超时异常的情况。

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author qifuguang
 * @date 15/8/25 00:34
 */
public class TestCyclicBarrier {
    private static final int THREAD_NUMBER = 5;
    private static final Random RANDOM = new Random();

    public static void main(String[] args) throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(THREAD_NUMBER, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴写入数据完毕");
            }
        });
        for (int i = 0; i < THREAD_NUMBER; i++) {
            if (i < THREAD_NUMBER - 1) {
                Thread t = new Thread(new Worker(barrier));
                t.start();
            } else {  //最后一个线程故意延迟3s创建。
                Thread.sleep(3000);
                Thread t = new Thread(new Worker(barrier));
                t.start();
            }
        }
    }

    static class Worker implements Runnable {
        private CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        public void run() {
            int time = RANDOM.nextInt(1000);
            System.out.println(Thread.currentThread().getId() + ":我需要" + time + "毫秒时间写入数据.");
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":写入数据完毕,等待其他小伙伴...");
            try {
                barrier.await(2000, TimeUnit.MILLISECONDS); // 只等待2s,必然会等待最后一个线程超时
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":所有线程都写入数据完毕,继续干活...");
        }
    }
}

运行结果:

10:我需要820毫秒时间写入数据.
11:我需要140毫秒时间写入数据.
12:我需要640毫秒时间写入数据.
13:我需要460毫秒时间写入数据.
11:写入数据完毕,等待其他小伙伴...
13:写入数据完毕,等待其他小伙伴...
12:写入数据完毕,等待其他小伙伴...
10:写入数据完毕,等待其他小伙伴...
java.util.concurrent.BrokenBarrierException
12:所有线程都写入数据完毕,继续干活...
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
13:所有线程都写入数据完毕,继续干活...
11:所有线程都写入数据完毕,继续干活...
10:所有线程都写入数据完毕,继续干活...
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
java.util.concurrent.TimeoutException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
14:我需要850毫秒时间写入数据.
java.util.concurrent.BrokenBarrierException
14:写入数据完毕,等待其他小伙伴...
14:所有线程都写入数据完毕,继续干活...
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)

可以看到,前面四个线程等待最后一个线程超时了,这个时候他们不再等待最后这个小伙伴了,而是抛出异常并都继续后续的动作。最后这个线程屁颠屁颠地完成写入数据操作之后也继续了后续的动作。需要说明的是,发生了超时异常时候,还没有完成“神秘任务”的线程在完成任务之后不会做任何等待,而是会直接执行后续的操作。

总结

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
  • CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  • CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

声明

本文为作者原创,也纯属个人见解,如理解有误,请留言相告。转载请注明出处: http://qifuguang.me/2015/08/25/[Java并发包学习五]CountDownLatch和CyclicBarrier介绍
如果你喜欢我的文章,请关注我的微信订阅号:“机智的程序猿”,更多精彩,尽在其中:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 一、多线程 说明下线程的状态 java中的线程一共有 5 种状态。 NEW:这种情况指的是,通过 New 关键字创...
    Java旅行者阅读 4,656评论 0 44
  • Java-Review-Note——4.多线程 标签: JavaStudy PS:本来是分开三篇的,后来想想还是整...
    coder_pig阅读 1,629评论 2 17
  • 相关概念 面向对象的三个特征 封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象. 多态的好处 允许不同类对...
    东经315度阅读 1,925评论 0 8
  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,798评论 1 19
  • 星期四/晴 近日阳光大好,无限春光将长清惯有的妖风也融化了,卸去八分凛冽的微风拂在面颊上,教人好生欢喜。 早上出门...
    酒久里个丸子阅读 127评论 0 0