查缺补漏

来源:互联网 发布:ubuntu iptables启用 编辑:程序博客网 时间:2024/05/01 13:41

前言

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 the10  *             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框架本身与具体事件处理逻辑无关

0 0
原创粉丝点击