Java语言十五讲(第十二讲 Multi-Thread多线程12.2)

实例变量如balance在线程间是共享的。有的时候,我们真的需要线程级别的变量,不希望共享,也是有办法的。Java里面有ThreadLocal变量。
比如,我们的线程从inventory里面拿东西,上面的程序一次4个。我现在想不同的线程个数拿的个数不同,就可以用一个线程相关的变量。
我们改写一下:

class InventoryWorker implements Runnable{
    Inventory inv;
    private ThreadLocal<Integer> bag = new ThreadLocal<>();

    InventoryWorker(Inventory inv){
        this.inv=inv;        
    }

    @Override
    public void run() {
        Random r = new Random();
        bag.set(r.nextInt(10));

        for (int i=0; i<10; i++) {
            inv.get(bag.get());
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们增加了一个ThreadLocalbag,在线程的run()里面给它赋值。测试程序创建两个线程。

public class InventoryTest {
    public static void main(String[] args) {
        Inventory inv=new Inventory(1000);
        InventoryWorker worker = new InventoryWorker(inv);
        Thread t1=new Thread(worker);
        Thread t2=new Thread(worker);
        t1.start();
        t2.start();
    }
}

运行后,可以看到,不同的线程的bag变量是不一样的。

回到线程互动。下面我们再看一个例子,对仓库里的物品,有两类线程,一类是不停地往外搬,一类是不停地往里面放,我们看这种情况下如何同步。
先改写Inventory.java:

class Inventory {
    private int balance;

    public Inventory(int balance) {
         this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }
    public void setBalance(int balance) {
        this.balance = balance;
    }

    public void ship(int quantity) {
        synchronized (this) {
            if (balance>=quantity) {
                int newBalance = balance - quantity;
                this.setBalance(newBalance);
                System.out.println(Thread.currentThread().getName()+" Ship, balance : "+getBalance());
            }else {
                System.out.println(Thread.currentThread().getName()+" Stock is empty");
            }
        }
    }

    public void stockin(int quantity) {
        synchronized (this) {
            if (balance<1000) {
                int newBalance = balance + quantity;
                this.setBalance(newBalance);
                System.out.println(Thread.currentThread().getName()+" Stock in, balance : "+getBalance());
            }else {
                System.out.println(Thread.currentThread().getName()+" Stock is full");
            }
        }
    }
}

除了ship()往外面搬砖之外,增加了一个stockin()往里面放砖。程序代码没有大变化,只是增加了仓库存量的判断,如果存量不够就不执行了,如果满了也不执行了。
下面是生产者和消费者:

class InventoryConsumer implements Runnable{
    Inventory inv;
    InventoryConsumer(Inventory inv){
        this.inv=inv;
    }
    @Override
    public void run() {
        for (int i=0; i<10; i++) {
            inv.ship(5);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者循环十次,每次从仓库里面拿5块。

class InventoryProducer implements Runnable{
    Inventory inv;
    InventoryProducer(Inventory inv){
        this.inv=inv;
    }
    @Override
    public void run() {
        for (int i=0; i<10; i++) {
            inv.stockin(3);
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

生产者也是循环十次,每次往仓库里面放3个。
最后用一个客户程序进行试验:

public class InventoryTest {
    public static void main(String[] args) {
        Inventory inv=new Inventory(20);
        Thread t1=new Thread(new InventoryProducer(inv));
        Thread t3=new Thread(new InventoryConsumer(inv));
        t1.start();
        t3.start();
    }
}

给仓库一个初始库存为20,生产者和消费者各自运行。初看起来,也没有什么大问题,数量也不会弄乱。因为ship()和stockin()都加上了同步。运行结果如下:

Thread-0 Stock in, balance : 23
Thread-1 Ship, balance : 18
Thread-1 Ship, balance : 13
Thread-0 Stock in, balance : 16
Thread-1 Ship, balance : 11
Thread-0 Stock in, balance : 14
Thread-1 Ship, balance : 9
Thread-1 Ship, balance : 4
Thread-0 Stock in, balance : 7
Thread-1 Ship, balance : 2
Thread-0 Stock in, balance : 5
Thread-1 Ship, balance : 0
Thread-1 Stock is empty
Thread-0 Stock in, balance : 3
Thread-1 Stock is empty
Thread-0 Stock in, balance : 6
Thread-1 Ship, balance : 1
Thread-0 Stock in, balance : 4
Thread-0 Stock in, balance : 7
Thread-0 Stock in, balance : 10

最后的结果是10。不对呀。初始化数量为20,存了十次共30个,拿了十次共50个,最后的库存应该是0啊。我们仔细分析结果。有两次打印Thread-1 Stock is empty,说明当时检查库存不足了,就没有执行。
这当然解释了这个结果。不过我们的本意却不是这样的。我们是想让程序真的执行十次存放和外拿操作的。也就是说,一旦库存不足,不应该跳过不执行,而应该是等待生产者放足了再执行。我们为了实现这个效果,需要用到wait()和notify()。
Inventory程序修改如下:

class Inventory {
    private int balance;

    public Inventory(int balance) {
         this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }
    public void setBalance(int balance) {
        this.balance = balance;
    }

    public void ship(int quantity) {
        synchronized (this) {
            while (balance<quantity) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            int newBalance = balance - quantity;
            this.setBalance(newBalance);
            System.out.println(Thread.currentThread().getName()+" Ship, balance : "+getBalance());

            notify();
        }
    }

    public void stockin(int quantity) {
        synchronized (this) {
            while (balance>=1000) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            int newBalance = balance + quantity;
            this.setBalance(newBalance);
            System.out.println(Thread.currentThread().getName()+" Stock in, balance : "+getBalance());

            notify();
        }
    }
}

当库存数量不足的时候,就让线程死等,什么时候数量够了,就执行操作。
运行结果如下:

Thread-0 Stock in, balance : 23
Thread-1 Ship, balance : 18
Thread-1 Ship, balance : 13
Thread-0 Stock in, balance : 16
Thread-1 Ship, balance : 11
Thread-0 Stock in, balance : 14
Thread-1 Ship, balance : 9
Thread-1 Ship, balance : 4
Thread-0 Stock in, balance : 7
Thread-1 Ship, balance : 2
Thread-0 Stock in, balance : 5
Thread-1 Ship, balance : 0
Thread-0 Stock in, balance : 3
Thread-0 Stock in, balance : 6
Thread-1 Ship, balance : 1
Thread-0 Stock in, balance : 4
Thread-0 Stock in, balance : 7
Thread-1 Ship, balance : 2
Thread-0 Stock in, balance : 5
Thread-1 Ship, balance : 0

这是我们想要的结果。这个结果是用了wait()和notify()协调了生产者和消费者的行为才导致的。解释一下,当前消费者线程拿到锁运行到临界区的时候,遇到wait()就进行阻塞状态,不再往下执行,同时把锁交回去。另一个生产者线程拿到锁运行到临界区的时候,遇到notify()就会唤醒阻塞的线程,线程继续运行,判断库存是否足,不足继续wait(),足够了就往下执行。wait()和notify()作为一对方法,同时起作用,执行阻塞和唤醒,达到生产者消费者协调的目的。
程序里wait()外面包含的代码块由if()改成了while(),这点是必须的,否则可能会有虚假唤醒的情况。因为wait()阻塞被唤醒后线程是接着往下执行的,如果用if-else结构,那就直接跳过了,而我们的本意是要执行else,所以不能用else。那只是去掉else,而保留if行不行呢?也不行。因为多线程程序,我们可以创建多个消费者和生产者线程,不仅仅是上面代码里面的一个消费者和一个生产者,多个消费者线程可能会被同时唤醒,如果没头没脑执行操作就会出现错误数据。大家可以自己试一下。
线程有多个,运行次序不定,所以要限制使用场合,莫弄乱了。特别是在多个线程要共享一些数据的时候,更要小心翼翼,隔离和同步都要小心。最理想的情况是线程之间不共享任何东西,完全是独立的。但是这是不可能的,我们要被迫小心翼翼写程序,很累。仔细再仔细,避免混乱或者互相死锁。
我们看一个死锁的例子,跑两个线程,分别用o1和o2来互锁,代码如下:

public class DeadLockTest {
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(new DeadLock1());
        Thread t2=new Thread(new DeadLock2());
        t1.start();
        t2.start();
    }
}

public class DeadLock1 implements Runnable {
    @Override
    public void run() {
        while(true) {
            synchronized(DeadLockTest.o1) {
                System.out.println("thread 0 - lock 1");
                synchronized(DeadLockTest.o2) {
                    System.out.println("thread 0 - lock 2");
                }
            }
        }
    }
}

DeadLock1是线程0,先锁o1,再锁o2。

public class DeadLock2 implements Runnable {
    @Override
    public void run() {
        while(true) {
            synchronized(DeadLockTest.o2) {
                System.out.println("thread 1 - lock 2");
                synchronized(DeadLockTest.o1) {
                    System.out.println("thread 1 - lock 1");
                }
            }
        }
    }
}

DeadLock2是线程1,先锁o2,再锁o1。

运行一下,结果是:

thread 0 - lock 1
thread 1 - lock 2

但是程序没有结束,在死等。
我们可以观察一下。先用 jps命令找到进程号,然后用jstack看,结果是:

Java stack information for the threads listed above:

===================================================

"Thread-1":
        at DeadLock2.run(DeadLock2.java:9)
        - waiting to lock <0x00000000d60d64c8> (a java.lang.Object)
        - locked <0x00000000d60d64d8> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
"Thread-0":
        at DeadLock1.run(DeadLock1.java:9)
        - waiting to lock <0x00000000d60d64d8> (a java.lang.Object)
        - locked <0x00000000d60d64c8> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
Found 1 deadlock.

明显看出了Thread-0锁住了0x00000000d60d64c8,等着0x00000000d60d64d8,而与此同时Thread-1锁住了0x00000000d60d64d8,而等着0x00000000d60d64c8。陷入了死锁。

“有的线程活着,它已经死了。有的线程死了,它还活着。”Sun技术研发中心一个同事这么说过。多线程程序比较难以编写,不建议自己直接通过new Thread()创建线程,也不建议自己直接用wait()和notify(),而要用更保险的方法。答案就在java.util.concurrent中。
里面有一个BlockingQueue,能够让我们很方便地实现生产者消费者模型,不需要自己手工同步。我们用一个简单的例子说明。
先定义一个消费者,代码如下(BQConsumer.java):

import java.util.concurrent.BlockingQueue;

class BQConsumer extends Thread {
    private final BlockingQueue<String> queue;
    public BQConsumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                String str = this.queue.take();
                System.out.println("Consumer took: " + str);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}

消费者线程里面用到了一个BlockingQueue,要从里面拿东西就直接用queue.take(),别的不用管。
再定义生产者,代码如下(BQProducer.java):

import java.util.concurrent.BlockingQueue;

class BQProducer extends Thread {
    private final BlockingQueue<String> queue;
    public BQProducer(BlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            try {
                this.queue.put("goods");
                System.out.println("Stock in : goods");
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}

生产者线程里面用到了一个BlockingQueue,要往里面放东西就直接用queue.put(),别的不用管。
简化了好多。
使用的代码一样的,代码如下(BQTest.java):

public class BQTest {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>(20);

        new BQProducer(queue).start();
        new BQProducer(queue).start();
        new BQConsumer(queue).start();
        new BQConsumer(queue).start();
    }
}

这种写法,又好理解又好用。君子善假于器也。
BlockingQueue我们在讲collection集合类的时候再进一步介绍,这里主要就着多线程来讲。

上面说了线程之间同步的机制就是锁。我们到现在用的synchronized锁都是悲观锁,怕数据改乱了,拿到锁就独占,这种严防死守的方式保证数据正确。还有一种乐观锁,它认为大部分的时候不会都同时修改数据,只是有的读有的写,不用一上来就严防死守,而是可以考虑真正写的时候去试,有了冲突再解决。这样平均的吞吐量比较好,并发性高。
CAS是实现乐观锁的一种方式。Java8之后容器框架里面用了CAS,如ConcurrentHashMap。CAS意思是Compare And Swap,用了3个基本操作数:内存里的值V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址中的实际值V相同时,才将值修改为B。
举一个例子,设定初始内存里面的值V=10。现在线程1启动了,它想把值加1,所以对线程1来讲,旧的预期值就是10,要修改的新值是11。说时迟那时快,就在线程1要更新的时候,线程2来了,它把内存的值先变成了11。线程1要着手更新了,它检查一下这个时候的V(此时已经是11了),然后比较A(10),一看不相等,就提交失败。线程1就重新从内存拿值,得到V=11,并且A=11,再进行加1计算,于是B=12,这个时候线程1再次提交,幸运的是再次比较的时候V=A,所以就把新值12更新进去。
我们回顾两个线程从仓库里面搬砖的例子,以前我们用synchronized做的。现在我们改成CAS来实现。代码如下:

class Inventory {
    private AtomicInteger balance;

    public Inventory(int balance) {
         this.balance = new AtomicInteger(balance);
    }

    public void get(int quantity) {
        int oldValue = balance.get();
        int newBalance = oldValue - quantity;
        while (!balance.compareAndSet(oldValue, newBalance)) {
            oldValue = balance.get();
        }
        System.out.println(Thread.currentThread().getName()+" got :"+quantity +", balance : "+getBalance());
    }
}

class InventoryWorker implements Runnable{
    Inventory inv;
    InventoryWorker(Inventory inv){
        this.inv=inv;
    }
    @Override
    public void run() {
        for (int i=0; i<10; i++) {
            inv.get(4);
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class InventoryTest {
    public static void main(String[] args) {
        Inventory inv=new Inventory(1000);
        Thread t1=new Thread(new InventoryWorker(inv));
        Thread t2=new Thread(new InventoryWorker(inv));
        t1.start();
        t2.start();
    }
}

新程序里面,我们用了AtomicInteger,进行原子操作。实际更新的时候,用得下面的代码:

        while (!balance.compareAndSet(oldValue, newBalance)) {
            oldValue = balance.get();
        }

按照CAS的机制进行更新。

我们再看看java.util.concurrent里面还提供了什么工具。其中最重要的就是提供了Executor Service。通过它,能执行异步线程,还能管理线程池。具体的类就是Executors,这是负责线程的使用与调度的工具类,我们一般从它开始入手。
我们先看看怎么执行线程。它不仅仅能执行Runnable,还能执行Callable。
先看一个简单的例子,代码如下(ExecutorsTest1.java):

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsTest1 {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(() -> {
            System.out.println("Hello Thread!");
        });
    }
}

先用 Executors.newSingleThreadExecutor()创建了一个单线程的executor service,再直接提交执行一个线程。运行一下,打印出来了结果,但是这个程序不停止。是的,Executors必须显式的停止,否则它会一直等着新的任务。executor.shutdown()可以用来停止Java进程,并且是温柔地停止,所谓“温柔”,就是它会等待提交的线程执行完毕之后再停止。自然也有简单粗暴的办法直接停止,就是executor.shutdownNow()。
再看一个执行Callable读返回值的简单例子,代码如下(ExecutorsTest2.java):

public class ExecutorsTest2 {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> ft = executor.submit(() -> {
            System.out.println("Hello Thread!");
            return "sucess";
        });
        try {
            System.out.println("return : " + ft.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        executor.shutdown();        
    }
}

再看看线程池的概念。在有大量线程需要创建的时候,线程本身的创建销毁就成了一个负担,所以我们这个时候会使用线程池来复用。我们还是建议用系统提供的方法,不要手工自己创建,提高性能,并避免内存溢出等错误,简化管理。对服务器端的程序,大体上都要这么用。Executors提供了几个方法创建线程池,newCachedThreadPool()用来创建大小按需变化的线程池,newFixedThreadPool()创建固定大小的线程池(超出的线程进入等待队列),newScheduledThreadPool()创建可以定时执行的线程池。这几个方法按照常规情况创建线程池,一般情况下可以用,比较便利。但是有些情况下,还是要手工创建线程池比较合适,new ThreadPoolExecutor(),可以使用的参数比较多,自定义策略,下面会讲到。
先看一个简单的例子。

public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
        for (int i = 0; i < 20; i++) {  
            final int index = i;  
            fixedThreadPool.execute(()-> {  
                    try {  
                        System.out.println(index);  
                        Thread.sleep(3000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            );  
        }  
    }
}

用Executors.newFixedThreadPool(3)创建了大小为3的线程池,连续运行20个线程。我们可以明显看出是每三秒打印出三个数字,意味着三个线程同时在干活儿,别的线程等待。
没头没脑地增加线程数,有害无益,因为线程的管理和调度本身也是要耗费资源的。线程池过大,大量的线程将在相对更少的CPU和有限的内存资源上竞争,这不仅影响并发性能,还会因过高的内存消耗导致溢出;线程池过小,将导致处理器得不到充分利用,降低吞吐率。
经验值呢,我问过一些人,腾讯后台的研发人员曾经告诉过我,根据他们的经验,按照服务器上的CPU和任务特点来安排是合理的,比如对于计算密集型任务,应该将线程数设置为CPU的处理个数+1会比较优化。由于线程有执行栈等内存消耗,创建过多的线程不会加快计算速度,反而会消耗更多的内存空间;另一方面线程过多,频繁切换线程上下文也会影响线程池的性能。而CachedThreadPool能有效提高IO密集型任务的吞吐量。有个公式Threads= Ncpu * Ucpu * (1 + W/C)可以估算,Ncpu是CPU个数,Ucpu是预估的CPU的使用率,W是线程平均等待时间,C是任务执行时间。
我前面提到过,按照Executors new出来的线程池,用的默认参数设置,有的时候不合适,所以需要了解一下手工如何创建ThreadPoolExecutor。
构造函数为:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

参数说明:

corePoolSize - the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
maximumPoolSize - the maximum number of threads to allow in the pool
keepAliveTime - when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
unit - the time unit for the keepAliveTime argument
workQueue - the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
threadFactory - the factory to use when the executor creates a new thread
handler - the handler to use when execution is blocked because the thread bounds and queue capacities are reached

说明:
A ThreadPoolExecutor 自动调整池子大小 (see getPoolSize()),依据的是参数corePoolSize (see getCorePoolSize()) 和 maximumPoolSize (see getMaximumPoolSize())。
一个新的任务提交执行后execute(Runnable), 如果这时小于corePoolSize个线程在运行, 就会创建一个新的线程处理这个执行请求(即使这个时候有休眠的线程也不管)。
如果这是有大于corePoolSize但是小于maximumPoolSize个线程在运行,会先放到队列workQueue中,只有当队列workQueue满了之后才创建新的线程。
如果当前有超过corePoolSize个线程,超过的线程如果休眠时间达到了keepAliveTime,就会被终止掉。
提交任务的时候,如果线程数达到了maximumPoolSize 并且workQueue也满了,就会被拒绝掉。拒绝有不同的策略选择。
corePoolSize,maximumPoolSize,Queue的大小,需要根据情况进行设置。

我们最后用一个例子演示,代码如下(TestThreadPool.java):

public class TestThreadPool {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = 
       new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5));
    ExecutorCompletionService<String> executorCompletionService = 
       new ExecutorCompletionService<>(threadPoolExecutor);

        for (int i = 0; i < 20; i++) {
    try {
            executorCompletionService.submit(()-> {  
        try {  
            Thread.sleep(3000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        },"testtask"+i);

            System.out.print(" New task: testtask" + i);
            System.out.print(" ActiveCount: " + threadPoolExecutor.getActiveCount());
            System.out.print(" poolSize: " + threadPoolExecutor.getPoolSize());
            System.out.print(" queueSize: " + threadPoolExecutor.getQueue().size());
            System.out.println(" taskCount: " + threadPoolExecutor.getTaskCount());
        } catch (RejectedExecutionException e) {
            System.out.println("Reject:" + i);
        }

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        }

        threadPoolExecutor.shutdown();
    }
}

手工创建线程池,提交20个任务,观察线程池的变化。记录活动任务数,池子大小,队列大小,以及任务数。
运行结果如下:

New task: testtask0 ActiveCount: 1 poolSize: 1 queueSize: 0 taskCount: 1
New task: testtask1 ActiveCount: 2 poolSize: 2 queueSize: 0 taskCount: 2
New task: testtask2 ActiveCount: 3 poolSize: 3 queueSize: 0 taskCount: 3
New task: testtask3 ActiveCount: 3 poolSize: 3 queueSize: 1 taskCount: 4
New task: testtask4 ActiveCount: 3 poolSize: 3 queueSize: 2 taskCount: 5
New task: testtask5 ActiveCount: 3 poolSize: 3 queueSize: 3 taskCount: 6
New task: testtask6 ActiveCount: 3 poolSize: 3 queueSize: 4 taskCount: 7
New task: testtask7 ActiveCount: 3 poolSize: 3 queueSize: 5 taskCount: 8
New task: testtask8 ActiveCount: 4 poolSize: 4 queueSize: 5 taskCount: 9
New task: testtask9 ActiveCount: 5 poolSize: 5 queueSize: 5 taskCount: 10
New task: testtask10 ActiveCount: 6 poolSize: 6 queueSize: 5 taskCount: 11
Reject:11
Reject:12
Reject:13
Reject:14
New task: testtask15 ActiveCount: 6 poolSize: 6 queueSize: 5 taskCount: 12
New task: testtask16 ActiveCount: 6 poolSize: 6 queueSize: 5 taskCount: 13
New task: testtask17 ActiveCount: 6 poolSize: 6 queueSize: 5 taskCount: 14
Reject:18
Reject:19

关于Java并发编程,推荐大家好好看一本书《Java Concurrency in Practice》,这是Java并发编程的圣经。我们看看作者名单,这些如雷贯耳的名字,就能领略了,Brian Goetz / Tim Peierls / Joshua Bloch / Joseph Bowbeer / David Holmes / Doug Lea。它们都是Java Community Process JSR 166专家组(并发工具)的主要成员,并在其他很多JCP专家组里任职。Brian Goetz有20多年的软件咨询行业经验,并著有大量Java开发的文章。Tim Peierls是“现代多处理器”的典范人物。Joseph Bowbeer是一个Java ME专家,他对并发编程的兴趣始于Apollo计算机时代。David Holmes是《The Java Programming Language》一书的合著者,任职于Sun公司。Joshua Bloch是Google公司的首席Java架构师,《Effective Java》一书的作者。Doug Lea是《Concurrent Programming》一书的作者,纽约州立大学 Oswego分校的计算机科学教授,并发计算的权威。
读书就要读好书,读经典的书,是获益最大最快的方式,在市面上和百度上乱找一气,花费的时间与得到的收获不成比例。在阅读好书的过程中逐渐形成自己的品味和格,杨振宁说过,学者是先有taste,后有style。
读书乃人生一大快事。读这些名家经典,经常会有如沐春风的感觉,有时候不禁掩卷赞叹:春风大雅能容物,秋水文章不染尘!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值