前言
Java的部分有基础、设计模式、IO、NIO、多线程,之后有时间还会把集合这 部分补上去,这么多内容里面,难免有一些知识点遗漏,本文主要是讲解这些遗漏的知识点。这些知识点,不是特别大的难点,所以没有必要专门写一篇文章讲解; 但是这些知识点,也不是一两句话就说得清楚的,所以放在这里。查漏补缺系列文章,每篇5个知识点,只要有值得研究的问题就会写上来。
Thread.sleep(XXX)方法消耗CPU吗?
这个知识点是我之前认识一直有错误的一个知识点,在我以前的认识里面,我一直认为 Thread.sleep(1000)的这一秒钟的时间内,线程的休眠是一直占用着CPU的时间片休眠的,查看了资料和仔细思考之后发现不是。 Thread.sleep(1000)的意思是:代码执行到这儿,1秒钟之内我休息一下,就不参与CPU竞争了,1秒钟之后我再过来参与CPU竞争。
说到这儿,就要顺便再提sleep和wait的区别了,JDK源码提供给我们的注释是非常严谨的:
1 /** 2 * Causes the currently executing thread to sleep (temporarily cease 3 * execution) for the specified number of milliseconds, subject to 4 * the precision and accuracy of system timers and schedulers. The thread 5 * does not lose ownership of any monitors. 6 * 7 * @param millis the length of time to sleep in milliseconds. 8 * @exception InterruptedException if any thread has interrupted 9 * the current thread. The <i>interrupted status</i> of the 10 * current thread is cleared when this exception is thrown. 11 * @see Object#notify() 12 */ 13 public static native void sleep(long millis) throws InterruptedException;
1 /** 2 * Causes the current thread to wait until another thread invokes the 3 * {@link java.lang.Object#notify()} method or the 4 * {@link java.lang.Object#notifyAll()} method for this object. 5 * In other words, this method behaves exactly as if it simply 6 * performs the call <tt>wait(0)</tt>. 7 * <p> 8 * The current thread must own this object‘s monitor. The thread 9 * releases ownership of this monitor and waits until another thread 10 ... 11 */ 12 public final void wait() throws InterruptedException { 13 wait(0); 14 }
看sleep方法的第4、第5行,"The thread does not lose ownership of any monitors"
看wait方法的第8、第9行,"The thread releases ownership of this monitor"
所以二者的差别就来了,差别就在"monitor"也就是监视器上,sleep和wait方法的执行都会释放CPU资源,但是sleep方法不会释放掉监视器的所有权,而wait方法会释放掉监视器的所有权。所谓监视器,就是假如sleep方法和wait方法处于同步方法/同步方法块中,它们所持有的对象锁
Thread.sleep(0)的作用
讲这个问题,要先讲一下两种线程调度的方法,周志明老师的《深入理解Java虚拟机:JVM高级特性与最佳实践》第12章第4节对这块内容有比较清楚的解释。
线程调度指的是系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度与抢占式线程调度。
1、协同式线程调度
使用协同式线 程调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。这种调度方式最大的好处就是 实现简单,而且由于线程要把自己的事情干完了才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步问题。不过协同式线程调度的坏处也很明 显:线程执行时间不受控制。如果一个线程编写有问题,一直不告诉系统进行线程切换,那么程序便会一直阻塞在那儿。所以这种方式非常不稳定,一个线程坚持不 让出CPU执行时间就可能会导致整个系统崩溃。
2、抢占式线程调度
抢占式线程调度方式是由系统来分配执行时间的,线程切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。
很明显Java采取的是抢占式调度方式。Java的线程是通过映射到系统的原生线程上实现的,在某个线程挂起或者分给它的CPU时间片到了之后,操作系统会根据线程优先级、线程饥饿程度等算出一个总的优先级出来,然后再挑选一个线程,分给它时间片。
讲了这么多,回到我们的主题上,我总结两点:
1、CPU分出来的时间片,可以竞争的线程都是会去竞争获取的
2、调用了Thread.sleep(XXX)方法的线程,意味着在XXX毫秒的时间内,该线程不参与CPU时间片的竞争
那么Thread.sleep(0)是干什么用的呢?它的作用就是:强制操作系统触发一次CPU计算优先级并分配时间片的动作。比如线程A获得了5毫秒的CPU执行时间,如果在执行了2毫秒的时候遇到了Thread.sleep(0)语句,那么后面的3毫秒的时间片就不运行了,操作系统重新计算一次优先级,并分配下一个CPU时间片给哪个线程。
这个小细节对于系统运行是有好处的:
1、避免了某一线程长时间占用CPU资源,我们知道在Java中比如开了两个非守护线程,线程优先级为10的线程A与线程优先级为5的线程B同时执行,这意味着操作系统基本上绝大多数时间都在运行线程A,基本不会把CPU控制权交给线程B
2、避免了系统假死
3、让线程有比较平均的机会获得CPU资源
不过Thread.sleep(0)虽然好,但是不要去滥用它。一个系统CPU占用率高是好事情,这意味着CPU在做事情,没有闲着。但是CPU占用率高还得保证CPU做的事情是应该做的事情, 比如CPU占用率高,但是在死循环,有意义吗?这就是代码写得有问题。Thread.sleep(0)也一样,这句语句触发了操作系统计算优先级、分配时 间片的动作,势必占用CPU的时间,如果在很多线程里面都滥用这个方法的话,CPU使用率是上去了,但大多数时间做的都是无意义的事情。我认为这个动作的目的更多是为了优化系统,而不是代码必须执行的一部分。
可变对象(immutable)和不可变对象(mutable)
这个是之前一直忽略的一个知识点,比方说说起String为什么是一个不可变对象,只知道因为它是被final修饰的所以不可变,而没有抓住不可变三个字的重点:
1、不可变对象就是那些一旦被创建,它们的状态就不能被改变的对象,每次对它们的改变都是产生了新的对象
2、可变对象就是那些创建后,状态依然可以被改变的对象
举个例子:String和StringBuilder,String是不可变的, 因为每次对String对象的修改都将产生一个新的String对象,而原来的对象保持不变;StringBuilder是可变的,因为每次对 StringBuilder的修改都作用于该对象本身,并没有新的对象产生。如果这么说还不够清楚,截取两段源码,首先是String的concat方 法,用户向已有的字符串后面拼接新的字符串:
1 public String concat(String str) { 2 int otherLen = str.length(); 3 if (otherLen == 0) { 4 return this; 5 } 6 char buf[] = new char[count + otherLen]; 7 getChars(0, count, buf, 0); 8 str.getChars(0, otherLen, buf, count); 9 return new String(0, count + otherLen, buf); 10 }
看到第9行,new了一个新的String出来。然后看一下 StringBuilder,StringBuilder最常用的应该就是append方法了,append一个字符串的时候会调用 StringBuilder的父类AbstractStringBuilder的append方法:
1 public AbstractStringBuilder append(String str) { 2 if (str == null) str = "null"; 3 int len = str.length(); 4 ensureCapacityInternal(count + len); 5 str.getChars(0, len, value, count); 6 count += len; 7 return this; 8 }
第5行的这个value就是一个char型数组"char[] value;",每次对StringBuilder的操作都是对value的改变。
不可变的对象对比可变对象有两点优势:
1、保证对象的状态不被改变
2、不使用锁机制就能被其他线程共享
实际上JDK本身就自带了一些不可变类,比如String、Integer、Float以及其他的包装类,判断的方式就是看它们真正的那个对象是不是final的就好了。
我们自己也可以创建不可变对象,创建不可变对象应该遵循几个原则:
1、不可变对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象
2、不可变对象的所有属性应该都是final的
3、对象必须被正确地创建,比如对象引用在创建过程中不能泄露
4、对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的不可变特性
使用不可变类的好处:
1、不可变类是线程安全的,可以不被synchronized修饰就在并发环境中共享
2、不可变对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程之间共享
3、不可变对象提高了程序的性能,因为它减少了synchronized的使用
4、不可变对象时可以被重复利用的,你可以将它们缓存起来,就像字符串字面量和整型数值一样,可以使用静态工厂方法来提供类似于valueOf这样的方法,它可以从缓存中返回一个已经存在的不可变对象,而不是重新创建一个
不可变对象虽然好,但是它有一个很大的缺点就是会制造出大量的垃圾,给垃圾收集带来很大的麻烦,由于它们不能被重用而且,所以不可变对象的使用依赖于开发人员合理的使用。另外,不可变对象也有一些安全问题,比如密码就建议不要用String,因为:
如果密码是以明文的形式保存成字符串 ,那么它将一直留在内存中,直到垃圾收集器把它清除。而由于字符创被放在字符串缓存池中以方便重用,所以它就可以在内存中被保留很长时间,而这将导致安全 隐患,因为任何能够访问内存的人都可以清晰地看到文本中的密码,这也是为什么总是应该用加密的形式而不是明文来保存密码。由于字符串是不可变的,所以没有 任何方式可以修改字符串的值,因为每次修改都将产生新的字符串,而如果使用char[]来保存密码,就可以将其中所有元素都设置为空或者是零。所以将密码 保存到字符数组中很明显地降低了密码被窃的风险。
计算密集型任务和IO密集型任务
在Java并发编程方面,计算密集型和IO密集型是两个非常典型的例子,讲解一下这方面的内容:
1、计算密集型
计算密集型,顾名思义就是应用程序需要非常多的CPU计算资源,在多核CPU时 代,我们要让每一个CPU核心都参与计算,将CPU性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将 是多么大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作的,所以为了让它的优势完全发挥出来,避免过多的上下文切换,比较理想的方案是两种:
(1)线程数 = CPU核数 + 1
(2)线程数 = CPU核数 * 2
2、IO密集型
对于IO密集型的应用,就很好理解了,我们现在做的大部分开发都是WEB应用,涉 及到大量的网络传输,不仅如此,与数据库、与缓存之间的交互也涉及IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好之后,线程才会继续 执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中的线程数量,这样就能让在等待IO的这段时间内,线程可以去做其他事情,提 供并发处理效率。但是这个线程池的线程数量也不是可以随意增大,因为线程上下文切换是有代价的,对于IO密集型的应用,线程数的计算有一个公式:
线程数 = CPU核心数 / (1 - 阻塞系数)
这个阻塞系数需要根据实际业务来调整,并不是绝对的。关于计算密集型,我原来理解 得并不深,现在想来,似乎有些入门,我的笔记本是双核的CPU,举个例子,我定义了一个线程,这明显是一个计算密集型的任务,因为线程无限循环做i++和 i--两个操作,这两个操作都是要不断消耗CPU的:
private static class T extends Thread { public void run() { int i = 0; while (true) { i++; i--; } } }
测试结果为:
(1)开一条线程,整个CPU占用率50%左右,其中35%都在执行我们的Java代码
(2)开两条线程,整个CPU占用率75%左右,其中65%都在执行我们的Java代码
(3)开三条线程,整个CPU占用率99%左右,其中90%都在执行我们的Java代码
线程继续增多,和第三条差不多,所以计算密集型的任务有结论是"线程数 = CPU核数 + 1",这样才能真正发挥出多核CPU的性能来,让CPU充分运行起来,做计算操作。
我原先不明白的一点是,操作系统是多进程的,为什么我写了while死循环之后,操作系统90%以上都会执行我们的Java代码,难道操作系统很少切换到别的进程去操作呢?即便是死循环,到时间了,还是会放弃CPU控制权,让CPU执行别的进程的,不是吗?
现 在想想可以这么理解这个问题。操作系统确实是管理着多个进程没错、也会在多个进程间切换没错,但是在分配CPU时间的时候,操作系统会根据进程执行的情况 计算出一个优先级来,如果操作系统发现某个进程执行地特别勤快,那么会优先分给它时间片。Java进程死循环就是这样,操作系统每次时间片切过来,都发现 在做CPU操作,于是就会给它一个比较高的优先级。
没法直接证明这一点,但是可以间接证明,运行死循环代码,用任务管理器看CPU占用率,一开始Java进程的CPU占用率还是慢慢上去的,然后在80%~90%之间浮动一两秒,之后就一直在90%以上了,这种现象似乎印证了我的说法。
Reactor模式
这也是之前学习的时候一直没有注意到的一个知识点。
Reactor模式即反应器模式,是并发系统常用的多线程处理方式,用以节省系统的资源,提高系统的吞吐量。举一个餐厅吃饭的例子,可能会更好理解。
对于一个餐厅而言,每一个人来就餐是一个事件,客人会先看一下菜单,然后点餐,这就像一个网站会有很多的请求,要求服务器做一些事情,处理这些就餐事件的就需要我们的服务人员了。多线程处理的方式是这样的:
1、来了一个人,一个服务员去服务,然后客人会看菜单、点菜,服务员将菜单给后厨
2、再来一个人,一个服务员去服务。。。
3、再来一个人,一个服务员去服务。。。
这就是多线程的处理方式,一个事件到来,就会有一个线程服务,很显然这种方式在人少的情况下会有很好的用户体验,每个客人都觉得自己是VIP,专人服务的,如果餐厅一直这样同一时间最多来5个客人,这家餐厅是客户很好地服务下去的。
来了一个好消息,因为这家店服务好,吃饭的人多起来了,同一时间会来10个客人, 但是只有5个服务员,这样就不能一对一服务了,所以老板又请了5个服务员,每个人又能享受VIP待遇了。然后人又多起来了,一时间会来20个客人,老板不 想请人了,再请人就赚不到钱了,还要给服务员开工钱呢,这时候怎么办?老板想了想,10个服务员对付20个客人吧,服务员勤快点就好了,伺候完一个马上伺 候另外一个,应该还是可以的。这样做是一个办法,但是一个明显的缺点就是,如果正在接受服务员服务的客人点菜很慢,其他客人可能要等待好长时间,有些脾气 火爆的客人可能等待不下去了。
Reactor模式怎么处理这个问题呢:
老板发现,客人点菜比较慢,大部分服务员都在等待客人点菜,其实干的活不是太多。 所以,老板决定,客人点菜的时候,服务员去招呼其他客人,等客人点好了菜直接招呼一声服务员,马上就有一个服务员过去服务。有了这个新的方法之后,老板就 进行了一次裁员,只留了一个服务员!这就是利用单线程做多线程的事情。
其实Java很多东西也是来自于生活,就像上面说的Reactor模式。Reactor的中心思想是:将所有要处理的IO时间注册到一个中心IO多路复用器上,同时主线程阻塞在多路复用器上,一旦有IO时间到来或是准备就绪,多路复用器返回并将相应IO时间分发到对应的处理器当中,这就是一种典型的事件驱动机制。Reactor模式是编写高性能网络服务器的必备技术之一,它有如下优点:
1、响应快,不必为同步单个时间所阻塞,虽然Reactor本身依然是同步的
2、编程相对简单,可以最大程度地避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
3、可扩展性好,可以方便地通过增加Reactor实例个数来充分利用CPU资源
4、可复用性好,Reactor框架本身与具体事件处理逻辑无关