欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Java多线程编程实战指南(设计模式篇)》答疑总结(陆续更新,part1)

程序员文章站 2022-03-03 11:53:06
...

《Java多线程编程实战指南(设计模式篇)》答疑开展以来,不少网友提出的问题既有与本书有关的话题,也有Java多线程编程基础知识的相关话题。由于时间关系,对于重复的问题我不逐一回复。还请各位网友参考本总结。这里我将一些与本书相关以及具有代表性的问题提炼下,并附上的我的简要回复。其实,有些问题的回复如果要再深入或者详细,恐怕得写一篇文章,只是时间关系......

 

活动时间:(11月23日--11月30日)

http://www.iteye.com/topic/1142354 

http://bbs.csdn.net/topics/391863274

 

《Java多线程编程实战指南(设计模式篇)》中设计模式是如何总计出来的?为什么不是更多,或更少?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

这些模式是许多人总结出来的。书中收录的都是我使用和在实际项目中接触过的。 

所以,有些我了解但是没有具体用过模式,如Leader/Follower,并没有收录进来。以后我有了一定的这些模式的使用经验可能会把它们加进来。 

 

另外,尽管模式具有一定的语言(平台)中立性。但是,有些模式我认为在Java平台中能够发挥的作用有限,不使用这些模式可能反而使代码更加简洁。例如,POSA中收录的Thread Safe Interface模式,其主要意图在于在某些不用锁的情况下(如同一个线程内的组件调用)可以避免锁的开销,而在需要锁的情况下又能使用锁。在Java中基本上语言本身就支持这样的效果,且应用开发人员不需要做额外的事情:其一,Java中的锁是可重入的,这意味着同一个线程多次获取同一个锁并不会导致死锁;其二,Java自从1.6版开始对synchronized关键字的执行进行优化(包括偏向锁Biased Locking、锁粗化Lock Coarsening和锁去除Lock Elimination等),这使得非竞争条件下,锁的消耗大大降低了。也就是说,非竞争的锁的开销和无锁的开销之间的差距已经缩得很小心。 

 

还有的模式其实是我们已经所熟知,并且也无需在其基础上做一些其它的动作。运用其它都是直截了当的。比如Patterns In Java中收录的Single Threaded Execution,其意图就是使某些代码在任一时刻只有一个线程能够执行。大家可能立马想到synchronized。的确如此。所以,这样的模式我并没有收录。 

 

有没有一个比较通俗易懂的例子来解释多线程的概念?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

 

银行的营业厅开一个柜台的时候,所有客户只能被分配到这个柜台上。如果同时开几个柜台,那么所有客户可以被分配到不同的柜台上。这样,从办理业务的客户角度来看,他们等待的时间短了(响应性更好) 。从银行的角度来看,他们同样的时间能够接待的客户更多了(吞吐率变大了)。 

 

这里,单个柜台可以看做单个线程,它可能导致响应性和吞吐率低;而多个柜台可以看做多个线程,它可能使得响应性和吞吐率增加。 

 

正如我前面的帖子提到的,多线程未必就能提高处理效率,所以我在上面用了“可能”。

 

线程的优先级能否保证线程是按照优先级高低的顺序运行,使用时需要注意什么问题?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

 

线程优先级我感觉最好把它理解成应用代码给JVM线程调度器的一个提示信息。它只是JVM线程调度器进行线程调度时的一个参考信息(而不是全部),它并不能保证线程按照优先级高低的顺序进行运行。举个例子来说,假设有3个线程ThreadA、ThreadB和ThreadC,它们的优先级分别是高、普通(中)和低。假设某一个时刻,ThreadA和ThreadB处于I/O等待状态,而ThreadC处于Runnable状态,那么JVM线程调度器此时会选择ThreadC运行。可见,这里ThreadA的高优先级并没有起到任何作用。

 

滥用线程优先级的可能导致线程饥饿(Thread Starvation),即某些线程永远无法得以运行。这个好比老大同一天给你分配了好几个任务,当你询问这个几个任务的优先级时,他的回答都是”重要“。那么,你就会困惑:我到底该先完成哪个任务呢?可见,此时所谓的优先级,有等于没有。

 

因此,一般不建议设置线程的优先级,使用默认值就可以了。

 

什么是死锁,如何避免死锁?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

 

死锁类似于吝啬鬼落水的故事。一个吝啬鬼掉进河里了,有人准备给他施救,但是施救者的也不会游泳,而他的手使劲往河水处伸还是够不到吝啬鬼。于是他让吝啬鬼把手伸过来(Give me your hand!)。吝啬鬼一听到”给”字(Give)就神经紧张,硬是不肯伸手,觉得伸手给别人是吃亏了。于是吝啬鬼说“不,你伸手给我”(No!Give me your hand!)。于是,一个不能给,一个不肯给,你等我,我等你,救援一事无法进展。

 

死锁避免有两种方法:

1、不使用锁: 例如,Swing和Android都采用这种设计,使得它们的用户界面组件层使用单线程,也就避免了锁,自然也就避免了死锁。详情可参见《Java多线程编程实战指南(设计模式篇)》第14章。

2、对锁的访问顺序进行排序(Lock Ordering)。可参见这篇博文(英文):http://tutorials.jenkov.com/java-concurrency/deadlock-prevention.html

什么情况下使用多线程,什么情况下使用单线程?

《Java多线程编程实战指南(设计模式篇)》作者回复:

 

这实际上是一个收益与成本比的问题。因为多线程也有自身的开销和问题,如上下文切换、锁的开销以及由锁可能导致的死锁等问题,所以使用多线程编程不一定就比使用单线程的处理效率更高。这正如书中所打的一个比方---和尚打水的故事:一个和尚(单线程)挑水喝,两个和尚(多线程)担水喝,三个和尚(多线程)没水喝。可见,多线程的使用可能反而导致处理效率的降低。《Java多线程编程实战指南(设计模式篇)》第1章对这个问题有讲解。如何恰当地使用多线程编程,这点也正是《Java多线程编程实战指南(设计模式篇)》所能起到的一个作用。 

 

另一方面,在任务原始规模比较大(或者说不小)的情况下,恰当地使用多线程可以提高处理效率。例如,《Java多线程编程实战指南(设计模式篇)》第13章提到的一个实战案例:将数据库中的几十万条数据导出到文件中并发送到指定的FTP服务器上。这个实例如果不采用多线程编程,则可能使相应的计算显得非常慢。 

 

特意地使用单线程编程有时反而可能提高处理效率。这里,典型的使用场景是程序的处理过程涉及一些独占资源或者非线程安全的对象。例如,《Java多线程编程实战指南(设计模式篇)》第11章的实战案例:使用非线程安全的FTP客户端组件将一批本地文件FTP上传到指定的多个服务器。这个案例中,我们使用了单线程处理FTP文件上传,以减少多线程相关的开销。而这个线程的实际处理效率也能满足我们实际的需要。 

 

Java平台本身就是个多线程的平台,Java平台中线程无处不在:负责Java程序运行的main线程、垃圾回收GC线程、JIT编译器线程。因此,这里我们所说的单线程编程实际上是在多线程环境中特意使用单线程。 

 

另外,即使是在单CPU的机器上,多线程编程也是有适用场景的。例如,一个线程正在执行I/O操作(如读取文件),此时该线程并不占用CPU(因为它已经被Switch out了),那么其它线程,如执行加密/解密计算的线程此时可以占用CPU执行。这样,便提高了CPU的利用率,有利于提高系统的吞吐率。

 

如何合理设置线程池的大小(线程数)、使用线程时如何合理控制线程数?

《Java多线程编程实战指南(设计模式篇)》作者回复:

 

线程池的大小设置一般要考虑到主机的CPU个数(逻辑CPU个数,NCPU)。线程池大小设置过小会导致CPU资源浪费,而设置过大则可能导致消耗过多的内存以及产生更多的上下文切换(导致CPU额外消耗过多)。粗略地说,对于执行CPU密集型的任务(如加密/解密)的线程池其最大大小可以为2NCPU。对于执行I/O密集型的任务(如写日志文件)的线程池其最大大小可以设置为2NCPU+1。详情参见《Java多线程编程实战指南(设计模式篇)》第9章。 

 

Java中我们可以使用Runtime.getRuntime().availableProcessors()来获取主机的CPU个数。 

 

多个线程同时访问同一个资源(如变量、文件等)时,如何保证线程安全?

《Java多线程编程实战指南(设计模式篇)》作者回复:

我们知道,使用锁(如synchronized和ReetrantLock)是保证线程安全的一个常见方法。这种方法的本质是以互斥的方式保证一个时刻只有一个线程能够访问共享变量(资源)。这好比公路维修的时候,原本四车道路在维修路段被弄成了单车道使得车辆只能一辆一辆通行。所以这种方法的缺点非常明显。《Java多线程编程实战指南(设计模式篇)》第3章、第10章、第11章介绍的3个模式就可以用来保证线程安全。它们背后的思想是要么是共享状态不可变的变量、要么是不共享变量。

 

文件共享访问与访问共享变量是类似的。在Java中我们使用文件时并不是直接对文件进行操作,而是通过Stream和Writer进行。而这些接口本身可能已经对并发访问进行处理了,即它们本身保证了共享时的线程安全。但是,这只是其中一个方面(下面会讲到)。例如,java.io.Writer这个抽象类是我们经常使用的类(如PrintWriter和BufferedWrite)的父类。它在写文件的时候是加锁的,即通过锁去保证线程安全。如java.io.Writer中定义的write方法的代码所示:

 

 public void write(String str, int off, int len) throws IOException {

        synchronized (lock) {

           //省略其它代码

        }

  }

  

也就是说上面的write方法是一个原子操作。但是,我们知道“原子操作+原子操作!=原子操作“。所以,如果多个线程使用多个Writer实例写同一个文件,那么这个文件的内容就可能紊乱了。当然,按照上面的分析,多个线程用同一个Writer实例写同一个文件并不会有问题。

 volatile这个关键字究竟起到什么作用(2015.11.27)?

 

《Java多线程编程实战指南(设计模式篇)》作者回复:

保证赋值操作的原子性。

我们知道对Java中的64位数据类型(long和double)进行赋值的时候,JVM是不保证原子性的。例如:

 

private long count=0;

 

void someUpdate(){

   count=1;

}

 

上述代码中,一个线程调用 someUpdate更新count值的时候,另外一个线程读取到的该变量的值可能是0、也可能是1,甚至可能是其它数值。如果对上述变量采用voalitle进行修饰,那么上述代码对long型变量的赋值就具有了原子性。因此,其它线程读取到的该变量的值只可能是0或者1。

保证共享可变变量的可见性。

简单来说,就是一个线程对一个共享变量的更新对于另外一个线程而言不一定是可见的。比如,

 

private boolean isDone=false;

如果有个线程将上面的变量修改为true,那么其它线程可能读取到的值一直是false。如果将上述变量采用volatile修饰,那么一个线程将其值修改后,之后有其它线程来读取该变量的值,后面这个线程总是可以读取到跟新后的变量值。

禁止重排序。

比如下面的代码:

private int nonVoaltileVar=1;

private boolean isVarSet=false;

 

private void update(){

  nonVoaltileVar=2;

  isVarSet=true;

}

 

上述代码执行时,由于重排序的结果,一个线程执行update方法后(假设此时再也没有其它线程会去更新上述两个变量的值),其它线程读取到isVarSet的值为true的情况下,它所读取到nonVoaltileVar的值可能仍然是1。这是由于update方法中的两个语句的执行顺序可能被对调(重排序)。而如果我们用voalitle去修饰isVarSet,那么voaltile会禁止其修饰的变量的赋值操作前的操作被重排序到其之后。这样,就保证了其它线程读取到isVarSet的值为true的情况下,nonVoaltileVar的值总是为2。

 

《Java多线程编程实战指南(设计模式篇)》第3章的实战案例代码中有使用volatile关键字,可以参考下。如果要进一步或者更加详细的解释,那要不小的篇幅。深入的理解voaltile关键字涉及到CPU访问内存的机制以及JMM。

 

什么是Happens-before关系,如何能够更好地理解它(2015.11.29)?

《Java多线程编程实战指南(设计模式篇)》作者回复:

 

Happens-before关系是JMM中的一个比较容易误解的概念。我的理解是它其实是一个形式化(或者模型化)的概念,所以理解起来有些困难。但是,这种比较抽象的概念我们可以对其具体化,通过一些具体的例子来更好的理解它。

 

Happens-before的提出是为了解决多线程共享变量的可见性问题。我们知道,这个问题编译器要关心、我们作为应用开发人员也要关心。理解这点很重要,因为如果你从编译器的角度出发去理解Happens-before的概念,就会涉及一些Memory Barrier等与硬件相关的概念。所以,我建议先从应用开发人员的角度出发去理解这个概念,这样会比较容易。

 

下面,我们对几个具体的Happens-before规则从应用开发人员的角度进行“解读”,通过这个解读相信大家都能明白Happens-before是个什么东西,至少明白它对我们(应用开发人员)意味着什么。

 

线程启动规则。对一个线程进行的Thread.start调用happens‐before被启动的线程中的每个动作(Action)。

Thread start rule. A call to Thread.start on a thread happens‐before every action in the started thread.

 

所谓动作(Action),包括读变量、写变量、启动线程、等待线程停止(join)和锁的获取与释放。

 

上面的描述乍看起来很抽象,也显得像废话——因为线程只有在启动以后,相应线程的run方法中的代码才会被执行。所以,线程肯定是启动在先,运行在后。这大家都知道啊!不过,这里happens‐before要说明并不是我们刚才将的时间上先后关系。它要描述的是某种可见性的保证。以上面的规则为例,这个规则意味着父线程在启动一个子线程之前对任何一个变量的变更对于这个子线程而言都是可见的(这才是我们关心的话题!)。例如:

 

public class HappensBefore {

static int a;

static long b;

 

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

Thread childThread = new Thread() {

@Override

public void run() {

 

if (a == 1) {

System.out.println(b);

 

b = 900L;

a = 3;

}

}

};

 

a = 1;

b = 10000L;

a = 2;

b = 1L;

 

childThread.join();

 

System.out.println("a=" + a + ",b=" + b);

}

 

}

 

上述代码的输出为:

 

10000

a=3,b=900

 

这是因为,根据上面的对“线程启动规则”的解读,子线程childThread始终是可以看到其父线程(main线程)在启动其前对变量的写操作的结果(即a==1,b == 10000)。因此,childThread的run方法运行的时候看到a的值为1以及b的值为10000是有保证的。

 

再看另外一个具体的Happens-before的规则:

 

线程终止规则。一个线程中的任何一个动作都 happens‐before检测该线程终止的线程中的任何一个动作。这包括检测线程调用被检测线程的Thread.join或者Thread.isAlive。

Thread  termination  rule.  Any  action  in  a  thread  happens‐before  any  other  thread  detects  that  thread  has 

terminated, either by successfully return from Thread.join or by Thread.isAlive returning false. 

 

同样,这个规则的理解关键还在于可见性方面它对我们(应用开发人员)意味着什么。这条规则说明,当一个线程终止的时候,该线程所做的所有变量更新动作的结果对于等待其停止的线程而言都是可见的(当然,要等Thread.join/Thread.isAlive调用返回)。

 

还是以上面的代码为例,根据上面对“线程终止规则”的解读,当子线程终止的时候,调用其join方法的父线程(main线程)看到该线程更新过的变量值,即变量a的值为3,变量b的值为900,是有保证的。

 

此时,我们对Happens-before有了一定的认识。这时,可以考虑从编译器的角度(假设我们是编译器开发人员),去理解Happens-before这个概念了。

 

我们知道Thread.start这个方法是一个synchronized修饰的方法。上述“线程启动规则”的实现就是通过编译器对synchronized关键字的实现而实现的。编译器会在synchronized块进入和退出的时候分别插入恰当的Memory Barrier(指令)。这些指令的作用是保证一个线程对变量的更新得以刷新到主内存(而不是寄存器、写缓冲器等“工作内存”)中,并防止一些指令重排序。

 

接着,我们可以循着上述方法再去解读其它Happens-before规则,使得我们对Happens-before的理解更加深刻。

什么是ThreadLocal类,哪些场景下可以使用ThreadLocal类(2015-12-01)?

《Java多线程编程实战指南(设计模式篇)》作者回复:

 

ThreadLocal类是个什么东西的确不容易解释。要深入理解ThreadLocal类,还是得从为什么有这个类说起。

 

打个比方说。两个小孩玩一台遥控小汽车玩具,一个时刻只能有一个小孩操控遥控器,另外一个小孩只能等待,弄不好两个小孩还会为抢遥控器的控制权而打架!因此,共享是好的,但是有时也会产生一些问题。于是,我们容易想到一个解决由共享导致的麻烦,那就是不共享——给两个小孩给咱买一台同一型号的遥控小汽车,让它们各自玩各自的!

 

回到多线程编程领域,多线程编程*享变量(数据)往往导致要加锁,而锁又会导致等待以及上下文切换、死锁等开销和问题。因此,有时候不共享是最好的。这就是引入ThreadLocal的原因。

 

多数情况下,我们访问一个变量值是通过使用相应的变量名进行的。我们可以把ThreadLocal类的一个实例看做变量名,通过这个变量名我们可以获得一个变量值,这个变量值同时还与具体的线程相关联。也就是说,特定线程与特定这样的变量名的组合决定了一个特定的变量值。也就是说,假设Java中有这样一个关键字 thread_specific,它可以用来修饰某个变量。这样的变量一旦被多个线程访问,各个线程所得到的变量值总是属于该线程所特有的那一份,彼此之间互不干扰。这个假设的关键字所起到的作用正是ThreadLocal类所要实现的效果。

 

private thread_specific SimpleDateFormat threadSpecificSdf=new SimpleDateFormat("MMddHHmmss");

 

形象地说ThreadLocal类可以这样理解:每个线程都持有一个其特有(私有)的一个储物柜。一个储物柜可以有多个储物箱,每个储物箱中存放的东西就是变量值。每个线程只能访问自己的储物柜而不能访问别的线程的拥有储物柜。并且,每个储物箱都有一把钥匙(Key),一把钥匙只能开一个储物箱。一把钥匙就是一个ThreadLocal实例。因此,我们就可以看到下面的这种决定关系:

 

{线程对象(储物柜),ThreadLocal实例(储物箱钥匙)}→变量值(储物箱中存放的东西)

 

例如,

{thread1,threadLocalA}→String1

 

{thread1,threadLocalB}→String2

 

{thread2,threadLocalC}→String3

 

{thread2,threadLocalD}→String4

 

 

《Java多线程编程实战指南(设计模式篇)》第10章讲解的设计模式的实现使用了ThreadLocal类。这一章的“模式评价与实现考量”一节总结了ThreadLocal类的4种典型应用场景。书中有详细的结束和示例代码,这里我简单列举下。

 

场景一:需要使用非线程安全对象,但是又不希望引入锁。

      这个典型的例子就是在多线程环境中在不加锁的情况下保证对SimpleDateFormat类使用的线程安全。如下代码所示:

 

public class SimpleDateFormatExample {

// 注意这里!SimpleDateFormat是非线程安全,这意味着直接在多个线程间共享它是有问题的。

private static ThreadLocal<SimpleDateFormat> tlSdf = new ThreadLocal<SimpleDateFormat>() {

protected SimpleDateFormat initialValue() {

return new SimpleDateFormat("MMddHHmmss");

};

};

 

public void someOper(Date date) {

String ts = tlSdf.get().format(date);

System.out.println(ts);

}

 

}

 

场景二:需要使用线程安全对象,但是希望避免其使用的锁的开销和相关问题。

      比如,随机数生成器类Random是个线程安全的对象,这是因为它内部使用锁。虽然我们可以在多个线程间共享Random实例而不会导致线程安全问题,但是这涉及锁的开销。如果想避免这种开销,那么一个好的方法是每个线程只使用一个Random实例来生成随机数。在JDK7中引入的类java.util.concurrent.ThreadLocalRandom体现的正是这种思想。《Java多线程编程实战指南(设计模式篇)》第10章所举的实战案例就是这种应用场景,大家可以参考下。

场景三:隐式参数传递。

      一个ThreadLocal类的实例可以被同一个线程内的不同方法(可以跨类)使用。具体实现通常借助单例(Singleton)模式。

场景四:特定于线程的单例(Singleton)模式。

      传统的单例模式实际上是保证对于某个类,一个JVM下的一个ClassLoader下最多只有一个实例。而借助ThreadLocal我们可以实现对于某个类,每个线程可以拥有该类最多一个实例。

 

ThreadLocal类使用时需要特别注意以下两点:

1、ThreadLocal类的实例通常设置为某个类的静态变量

  即通常的使用格式是:

  private static ThreadLocal<XXX> tlVar=new ThreadLocal<XXX>() {

protected XXX initialValue() {

return new XXX();

};

};

 

 

 这是因为:一个ThreadLocal实例就对应一个线程特有的变量值,如果把ThreadLocal作为某个类的实例变量,由于一个类可以有多个实例,那么就会有多个ThreadLocal实例被创建。即便是对于一个线程而言,这多个ThreadLocal实例就对应了多个该线程特有的变量值。而这通常不是我们所需要。如果我们需要为同一线程创建不同的该线程特有的变量值,那应该创建不同名字的ThreadLocal实例。例如:

    private static ThreadLocal<XXX> tlVarA=new ThreadLocal<XXX>() {

protected XXX initialValue() {

return new XXX();

};

};

 

private static ThreadLocal<YYY> tlVarB=new ThreadLocal<YYY>() {

protected YYY initialValue() {

return new YYY();

};

};

 

2、在线程池环境下(如Web应用服务器下),使用ThreadLocal可能导致内存泄漏

  这种内存泄漏的原因分析可以从Class(也是一个对象)及负责加载其的ClassLoader之间的关系、JDK对ThreadLocal的具体实现以及Web应用服务器加载Web应用程序的原理入手。分析起来需要花费不是篇幅,《Java多线程编程实战指南(设计模式篇)》10章有详细的分析和配图。

在此基础上,我们可以给出相应的解决方案。详情参考《Java多线程编程实战指南(设计模式篇)》10章。

 

学习Java多线程编程/并发编程有哪些建议?

《Java多线程编程实战指南(设计模式篇)》作者回复:

你的问题提的很好,我个人也是比较注重的学习方法的。这点,我也尽可能地体现在我的书中。

 

我认为学一样东西,要从把握它的基本概念和原理入手。并在这个过程中注意概念和概念之间的关系,知其然而知其所以然,并主动去思考一些问题。当然,“觉知此事需躬行”,自己动手去实践是少不了的。

 

比如拿你上面的描述中涉及的几个概念来说。“互斥”,站在它背后的是共享可变数据(Shared Mutable Data/Variable),也就是说的出现或者之所以需要它完全是因为我们在多个线程间共享了可以改变的数据。换句话说,如果多个线程之间不共享数据(参见《Java多线程编程实战指南(设计模式篇)》第10章)或者共享的数据是不可变的(参见《Java多线程编程实战指南(设计模式篇)》第3章),那么我们就无需互斥,程序的计算效率也就提高了。因此,多线程并不一定就意味着“互斥”。互斥的结果是某些代码在任意一个时刻只有一个线程能够执行,那么我们可以思考下面这样一个问题:

 

synchronized块可以实现互斥,那么synchronized块保护的是临界区代码么?

 

这个问题如果回答“是”,我认为也没有错。但是,如果深入一步理解,我们会发现synchronized块正在要“保护”的是临界区代码所访问的共享可变变量,而不是代码本身。

 

在比如说“同步”(同步机制),为什么需要同步呢?一方面是要用它来“保护”共享可变数据。另外,也是通过它来保证多线程间共享的数据(不一定是可变的)之间的内存可见性(Visibility);并且,禁止指令重排序也是通过同步机制实现的。这里,又涉及了一个概念“”内存可见性“,站在它背后的是CPU通过缓存(Cache)却访问内存以提高其处理效率这个事实以及JIT编译器处于对代码执行效率的考虑可能对代码作出的优化。同样,指令重排序是个什么概念,什么情况下我们需要禁止(或者阻止)指令重排序也是我们要掌握的概念和原理。

 

再比如说,我们都知道Java中有两种方法可以创建线程,那么这两种方法有什么区别呢?当我们掌握了线程安全这一概念以后,思考这个问题的答案就有了方向了。

 

我们经常说“多线程编程”、“并发编程”,往往也不对二者进行区分。那么,二者有什么联系和区别呢?

 

线程(多线程)其实只是一种并发编程的模型,也就是说多线程可以实现并发编程。而并发编程却不一定就是多线程编程,有其它的方法,如Actor模型也能实现并发编程。当然,线程模型可以是其它并发编程模型的基础。这样把概念弄清楚,有助于我们扩展思路,开阔眼界。例如,《Java多线程编程实战指南(设计模式篇)》第8章介绍的Active Object模式其背后的思想和Actor模型非常相似。

 

反过来说,不能真正掌握基本概念和原理,就会导致表面上我们是学会了某些东西,但是在实际的工作过程中一遇到问题以自己的力量(不问别人、不搜索)就搞不定了。这好比驾校老师教学生如何发动小汽车、如何变道、如何换挡等等,学生也能自己操作起来。但是,学生考到驾照后,自己上路的时候就会遇到许多实际的问题,比如车子熄火了怎么办?路过积水的涵洞怎么办?这些自己能搞定么?再拿我们工作中的实际例子来说,如果我们不能理解到JSP就是一个Servlet的这个事实,不知道JSP经历从翻译、编译到运行的这样一个处理过程,那么在遇到JSP问题的时候我们自己可能就搞不定,比如一个JSP中include了另外一个JSP,被include的JSP内容更新了,而主JSP在运行的时候却始终没有出现更新后的效果。这样的问题,没有本质上把握JSP的概念和处理原理,仅凭自己是很难搞定的。

 

问题人人都会遇到,区别是老手能够很快找到问题所在,并给出简单有效的解决方法,而新手可能一直在原地踏步,甚至走进死胡同。究其原因,经验差别固然是一方面,我上面所讲应该也是重要的一方面。

 

至于大数据、云计算,我在工作过程中并没有接触。但是,我想方法是相似的。如果是我学它们,我会从这些技术或者理念的出现是要解决什么问题以及它们的一些基本概念和原理入手。另外,如果时间上允许的话,我建议是先学一样技术,深入的学习并掌握它,再次基础上再去学习其它知识、技术。这样,对于后学的东西而言,我们可以充分利用之前学习到知识、经验以及积累的能力,可以使学习后学的东西轻松一点。这种现象心理学上称之为”迁移“,类似我们所说的”触类旁通“。