《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的概念和处理原理,仅凭自己是很难搞定的。
问题人人都会遇到,区别是老手能够很快找到问题所在,并给出简单有效的解决方法,而新手可能一直在原地踏步,甚至走进死胡同。究其原因,经验差别固然是一方面,我上面所讲应该也是重要的一方面。
至于大数据、云计算,我在工作过程中并没有接触。但是,我想方法是相似的。如果是我学它们,我会从这些技术或者理念的出现是要解决什么问题以及它们的一些基本概念和原理入手。另外,如果时间上允许的话,我建议是先学一样技术,深入的学习并掌握它,再次基础上再去学习其它知识、技术。这样,对于后学的东西而言,我们可以充分利用之前学习到知识、经验以及积累的能力,可以使学习后学的东西轻松一点。这种现象心理学上称之为”迁移“,类似我们所说的”触类旁通“。
相关推荐
11 内置函数part1 第17章 01 课前吹牛 02 zip方法 03 max和min高级使用 04 其他内置函数 05 文件操作的其他模式 第18章 01 上节课复习 02 文件处理b模式 03 文件操作的其他方法 04 文件seek方法补充 05 迭代器...
2023年通信施工应急预案.doc
234751_43.m3u8
内容概要:本文详细介绍了基于ARM单片机(LM3S6911)和FPGA(ALTERA EP1C3)的运动控制卡设计方案。ARM负责复杂的插补算法和以太网通信,FPGA则承担实时脉冲生成和IO扩展任务。文中展示了具体的硬件连接、通信协议以及关键代码片段,如以太网通信的LWIP协议栈实现、FPGA的Verilog代码用于脉冲生成和IO消抖处理。此外,还讨论了硬件设计细节,如PCB布局和电源管理,强调了系统的实时性和抗干扰能力。 适合人群:对嵌入式系统和运动控制系统感兴趣的工程师和技术爱好者,尤其是有一定ARM和FPGA开发经验的人群。 使用场景及目标:适用于工业自动化领域的运动控制应用,旨在提高运动控制的精度和实时性,减少机械抖动,增强系统的可靠性和灵活性。 其他说明:本文不仅提供了详细的硬件和软件设计思路,还附带了原理图、PCB图及源代码,方便读者进行进一步的研究和开发。
Paddle Serving-部署一个自己的OCR识别服务器
内容概要:本文详细介绍了如何利用COMSOL的等离子体模块构建针-针电极间的空气流注放电模型。主要内容涵盖了几何结构的定义、物理场配置(如电子、正负离子的载流子选择)、化学反应的设定(包括21组带电粒子反应)以及Helmholtz光电离过程的具体实现方法。文中还提供了多个代码片段用于解释各个步骤的操作方式,并强调了求解器配置和边界条件处理的关键点。此外,作者分享了一些实用的小技巧,如初始步长设置、网格细化等,以确保模型能够稳定收敛并得到合理的仿真结果。 适合人群:从事等离子体物理研究的专业人士,特别是那些对高压放电现象感兴趣的科研工作者和技术人员。 使用场景及目标:适用于希望深入了解和模拟针-针电极间空气流注放电行为的研究项目。通过该模型可以更好地理解电场分布、粒子密度变化等微观物理过程,从而为实际工程应用提供理论支持。 阅读建议:由于涉及较多的技术细节和数学公式,建议读者具备一定的电磁学、流体力学基础知识,并且最好有一定的COMSOL软件使用经验。同时,在实践中可以根据自己的研究方向调整模型参数进行探索。
内容概要:本文详细介绍了基于西门子S7-200 PLC的恒压供水系统的设计与实现。系统采用一拖二或一拖三模式,确保供水系统的可靠性。核心组件包括PLC、富士PID控制模块和ABB变频器,通过精确的压力控制和快速响应,实现了稳定的恒压供水。文中提供了详细的PLC程序示例,涵盖水泵启停控制、PID算法调用以及变频器频率调节等功能。此外,还展示了触摸屏界面设计,用于实时监控和操作。硬件配置方面,强调了柜体制作图纸的重要性和规范性,确保电气接线正确无误。调试过程中,作者分享了许多实用技巧,如PID参数整定、变频器设置和故障切换逻辑等。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对PLC编程和恒压供水系统感兴趣的读者。 使用场景及目标:适用于工业现场的恒压供水系统设计与实施,旨在提高供水系统的稳定性和可靠性,减少因设备故障导致的生产中断。 其他说明:本文不仅提供了完整的硬件配置和软件编程指导,还分享了丰富的实战经验和调试技巧,帮助读者更好地理解和应用相关技术。
内容概要:本文详细介绍了基于MATLAB的三电平整流器输入不平衡控制的仿真模型。该模型采用模型预测控制(MPC)和正负序分离技术,通过Clarke变换和Park变换将三相电压转换到αβ坐标系,并进一步分离出正序和负序分量。随后,通过模型预测控制计算网侧参考电流,确保系统在网侧电压不平衡情况下仍能稳定运行。文中还讨论了仿真过程中的一些关键技术细节,如代价函数设计、LCL滤波器的谐振频率设置以及动态调整预测步长的方法。最终,仿真结果显示该方法在电网电压不平衡时表现出色,直流侧电压纹波控制在2%以内,电流波形质量良好。 适合人群:从事电力电子、电力系统控制领域的研究人员和技术人员,特别是对三电平整流器及其控制策略感兴趣的读者。 使用场景及目标:适用于研究和开发三电平整流器在非理想电网条件下的控制策略,旨在提高系统的稳定性和效率,减少谐波和直流侧电压波动。 其他说明:文中提供了详细的MATLAB代码片段和调试技巧,有助于读者理解和实现该仿真模型。此外,作者还分享了一些实际调试中的经验和注意事项,如坐标变换的时序对齐问题和代价函数权重的选择。
内容概要:本文详细介绍了Asp.Net CRM客户关系管理系统的功能和技术实现。该系统不仅涵盖了客户信息管理、日程安排等功能,还展示了如何通过C#代码实现这些功能的具体细节。此外,文章强调了系统的二次开发能力和扩展性,如通过创建新的数据库表和编写相应代码来满足特定行业需求。系统采用三层架构,将客户生命周期、销售流程、团队协同等功能模块有机结合,形成一个完整的业务协同平台。文中还探讨了销售流程引擎、数据关联、实时消息推送等方面的技术实现,突出了系统的灵活性和高效性。 适合人群:对CRM系统开发感兴趣的软件工程师、企业IT管理人员、有一定编程基础的研发人员。 使用场景及目标:适用于希望提升客户管理水平、优化销售流程、增强内部协作的企业。通过实施该系统,企业可以更好地管理客户资源,提高工作效率,降低成本,最终提升销售业绩。 其他说明:文章提供了丰富的代码示例,帮助读者深入理解系统的工作原理和技术实现。同时,强调了系统在实际应用中的灵活性和扩展性,使其能够适应不同企业的具体需求。
PowerShell7.5.1
内容概要:本文详细介绍了三菱FX5U PLC的以太网通讯配置和编程技巧。首先讲解了基本的硬件配置和参数设置,如IP地址、子网掩码、端口号等。接着展示了如何使用ST语言和Python进行TCP客户端编程,包括创建Socket、连接服务器、发送和接收数据的具体步骤。文中还提到了一些常见的陷阱和解决方案,如隐藏寄存器的设置、十六进制地址转换、连接超时等问题。此外,提供了高级玩法,如批量读写寄存器、使用MC协议读取数据以及通过Python脚本测试通讯稳定性。最后强调了调试工具Wireshark的使用和注意事项。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对三菱PLC以太网通讯感兴趣的读者。 使用场景及目标:帮助读者掌握三菱FX5U PLC以太网通讯的配置和编程方法,提高设备联机和数据采集效率,解决实际项目中可能遇到的各种问题。 其他说明:文章不仅提供了详细的代码示例,还分享了许多实用的经验和技巧,有助于读者快速上手并避免常见错误。
内容概要:本文详细介绍了永磁同步电机(PMSM)的双环矢量控制理论及其在MATLAB/Simulink中的具体实现方法。首先解释了电流环和速度环的工作原理,特别是电流环中的Clarke变换和Park变换的具体实现。文中提供了详细的代码示例,如Clarke变换、Park变换以及电流环PI控制器的实现。同时,针对常见的调试问题给出了具体的解决方案,如速度环PI参数的整定、SVPWM模块的配置等。此外,还强调了仿真过程中需要注意的关键点,如PWM载波频率、死区时间补偿、速度观测器的选择等。 适合人群:从事电机控制研究和技术开发的专业人士,尤其是有一定MATLAB/Simulink基础的研发人员。 使用场景及目标:适用于希望深入了解PMSM双环矢量控制原理并掌握其仿真的技术人员。目标是在MATLAB/Simulink环境中搭建稳定的PMSM双环控制系统,确保系统在不同负载条件下的稳定性和鲁棒性。 其他说明:文章不仅提供了理论分析,还包括了大量的实践经验分享,帮助读者避免常见错误,提高仿真效率。
内容概要:本文深入介绍了FX3U PLC控制器的硬件架构和嵌入式软件设计。硬件方面,详细描述了控制器的尺寸、主控芯片STM32F103VCT6及其外围电路设计,如电源管理、继电器输出、光耦隔离、模拟量处理和通讯接口(CAN总线、RS485)。软件部分则展示了关键的GPIO配置、ADC校准、中断处理、状态指示灯控制以及多种通讯协议的实现方法。文中还特别强调了抗干扰设计和工业应用场景中的优化措施。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对PLC控制器硬件设计和嵌入式编程感兴趣的读者。 使用场景及目标:适用于希望深入了解PLC控制器内部工作原理的技术人员,帮助他们掌握如何设计和优化工业控制系统,特别是在小型产线控制器或智能仓储系统的开发中。 其他说明:文章不仅提供了详细的硬件和软件设计方案,还分享了许多实践经验,如光耦隔离的应用、抗干扰措施、以及针对特定工业环境的优化技巧。
2025年软件实施工程师笔试面试题及答案.docx
CAD技术在水利水电工程中的应用.docx
内容概要:本文介绍了一种低成本、高精度的模拟量正负电压输出模块方案,适用于工业控制和实验室设备。该模块支持±10V、±5V、±3V可编程设置,具备断电参数保存、自定义校准等功能。硬件设计包括电源模块、DAC芯片、EEPROM和控制芯片,确保输出电压稳定性和高精度。软件部分实现了电压设置、参数保存和自定义校准功能。材料成本控制在15元左右,性价比极高。 适合人群:电子工程师、硬件开发者、工业控制系统设计师以及对高精度电压输出有需求的研究人员。 使用场景及目标:① 工业控制系统的精密电压调节;② 实验室设备的高精度电压输出;③ 自动化项目的批量部署。 其他说明:该方案提供了完整的硬件设计和软件实现,包括详细的代码示例和原理图,方便用户进行二次开发和定制。
内容概要:本文深入剖析了一个全开源淘客系统的实现细节,涵盖了多个关键技术点。首先介绍了用Go语言重构的淘宝联盟API网关,展示了商品搜索接口的核心代码,强调了参数签名、HTTP请求处理以及数据转换的重要性。接着讨论了数据库设计,特别是佣金结算表的结构,突出了JSON类型的使用和乐观锁机制的应用。前端方面,展示了基于Vue.js的佣金日历组件,体现了数据驱动UI和事件冒泡处理的设计理念。部署环节则分享了一些实用技巧,如手动更新淘宝API的SSL证书链。此外,还探讨了PHP实现的订单同步服务、SDK封装、热更新通道等特性,以及系统的技术售后支持措施。最后,文章总结了该系统的优点,如清晰的MVC分层结构、灵活的分佣规则设计、完善的部署方案和技术售后支持。 适合人群:对淘客系统开发感兴趣的开发者,尤其是希望深入了解系统架构设计、前后端开发、数据库设计和部署优化的技术人员。 使用场景及目标:适用于想要搭建或改进淘客系统的团队和个人开发者。目标是帮助读者理解淘客系统的各个组成部分及其工作原理,从而能够快速上手并进行二次开发。 其他说明:文中提供了大量实际代码片段和配置示例,便于读者理解和实践。同时,作者还分享了许多实用的经验和技巧,有助于提高开发效率和系统稳定性。
内容概要:本文详细介绍了如何使用 LabVIEW 2018 版制作动态启动界面,涵盖三大核心技术:图片加载、控件移动和动态调用。首先,通过创建透明图像和使用相对路径确保跨平台兼容性,实现了高质量的图片展示。其次,利用 While 循环和属性节点精确控制控件的移动轨迹,如 Logo 飞入、文字滑入等效果。最后,采用 VI 动态调用技术,使主程序在启动界面前台动画播放完毕后无缝加载,提升了用户体验。此外,文中还提供了许多优化建议,如双缓冲模式、渐隐效果、状态机架构等,帮助开发者打造更加专业的启动界面。 适合人群:具有一定 LabVIEW 基础的开发者,尤其是希望提升用户界面美观度和交互体验的技术人员。 使用场景及目标:适用于需要开发带有动态启动界面的 LabVIEW 应用程序,旨在提高软件的专业性和视觉吸引力,同时确保启动过程的流畅性和稳定性。 其他说明:文中提供的代码片段和技巧可以直接应用于实际项目中,建议初学者逐步尝试各个功能模块,熟悉后再进行综合应用。
内容概要:本文详细介绍了如何利用STM32单片机和Matlab协作构建一个高效的信号发生器。首先,通过Matlab生成不同类型的波形数据并将其转化为适用于DAC的12位格式,然后将这些数据存储为C语言数组以便嵌入STM32程序中。接着,在STM32端配置DAC通道并通过DMA进行高速数据传输,确保波形能够稳定地输出。此外,文中还探讨了串口通信协议的设计用于远程控制波形参数,如频率、幅度等;实现了按键消抖功能以实现波形类型的手动切换;并且优化了LCD显示屏的内容刷新机制。最终测试表明该系统可以在1Hz-50kHz范围内提供高质量的波形输出,总谐波失真控制在1%以内。 适合人群:具有一定嵌入式开发经验的技术人员,尤其是对STM32、Matlab有一定了解的人群。 使用场景及目标:适用于需要定制化波形输出的应用场合,如实验教学、产品研发测试等。主要目的是为了帮助开发者更好地理解和掌握STM32与外部设备交互的方式,特别是DAC与DMA配合使用的技巧。 其他说明:随附有完整的项目资料包,包括Matlab脚本、Keil工程项目文件以及硬件原理图等,方便读者实际操作验证。
内容概要:本文详细介绍了台达触摸屏通过MODBUS RTU协议直接与台达变频器通讯的技术实现及其应用场景。主要内容涵盖通讯的基础理论、具体的代码实现方法(如连接、启动、停止、正反转控制、频率设定等功能的具体代码示例)、界面设计要点(如实时数据显示、状态指示灯的颜色变化等),以及针对常见问题的解决方案。此外,文中还强调了该方案的应用范围不仅限于台达产品,还可以推广到其他品牌的变频器、触摸屏甚至是温控表等设备,展示了其广泛的适应性和灵活性。 适用人群:从事工业自动化领域的工程师和技术人员,尤其是那些希望深入了解MODBUS RTU协议及其在实际项目中应用的人群。 使用场景及目标:适用于需要简化控制系统架构、降低成本的企业或个人开发者。主要目标是在不需要额外PLC的情况下,实现触摸屏对变频器的有效控制,提高系统的稳定性和效率。 其他说明:文中提供了大量实用的代码片段和配置参数,帮助读者快速理解和实施相关技术。同时,作者还分享了许多实践经验,如避免常见的配置错误、优化用户体验的设计思路等,有助于读者少走弯路。