您好,欢迎访问一九零五行业门户网

改善Java程序的151个建议

建议123:volatile不能保证数据同步
volatile关键字比较少用,原因无外乎两点,一是在java1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题就是移植性较差;而且比较难设计,而且误用较多,这也导致它的名誉 受损。
我们知道,每个线程都运行在栈内存中,每个线程都有自己的工作内存(working memory,比如寄存器register、高速缓冲存储器cache等),线程的计算一般是通过工作内存进行交互的,其示意图如下图所示:
从示意图上我们可以看到,线程在初始化时从主内存中加载所需的变量值到工作内存中,然后在线程运行时,如果是读取,则直接从工作内存中读取,若是写入则先写到工作内存中,之后刷新到主内存中,这是jvm的一个简答的内存模型,但是这样的结构在多线程的情况下有可能会出现问题,比如:a线程修改变量的值,也刷新到了主内存,但b、c线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最新鲜的值,此时就出现了不同线程持有的公共资源不同步的情况。
对于此类问题有很多解决办法,比如使用synchronized同步代码块,或者使用lock锁来解决该问题,不过,java可以使用volatile更简单地解决此类问题,比如在一个变量前加上volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与内存交互的,而不是与本线程的工作内存交互的,保证每个线程都能获得最新鲜的变量值,其示意图如下:
明白了volatile变量的原理,那我们思考一下:volatile变量是否能够保证数据的同步性呢?两个线程同时修改一个volatile是否会产生脏数据呢?我们看看下面代码:
class unsafethread implements runnable { // 共享资源 private volatile int count = 0; @override public void run() { // 增加cpu的繁忙程度,不必关心其逻辑含义 for (int i = 0; i < 1000; i++) { math.hypot(math.pow(92456789, i), math.cos(i)); } count++; } public int getcount() { return count; } }
上面的代码定义了一个多线程类,run方法的主要逻辑是共享资源count的自加运算,而且我们还为count变量加上了volatile关键字,确保是从内存中读取和写入的,如果有多个线程运行,也就是多个线程执行count变量的自加操作,count变量会产生脏数据吗?想想看,我们已经为count加上了volatile关键字呀!模拟多线程的代码如下:
public static void main(string[] args) throws interruptedexception { // 理想值,并作为最大循环次数 int value = 1000; // 循环次数,防止造成无限循环或者死循环 int loops = 0; // 主线程组,用于估计活动线程数 threadgroup tg = thread.currentthread().getthreadgroup(); while (loops++ < value) { // 共享资源清零 unsafethread ut = new unsafethread(); for (int i = 0; i < value; i++) { new thread(ut).start(); } // 先等15毫秒,等待活动线程为1 do { thread.sleep(15); } while (tg.activecount() != 1); // 检查实际值与理论值是否一致 if (ut.getcount() != value) { // 出现线程不安全的情况 system.out.println(循环到: + loops + 遍,出现线程不安全的情况); system.out.println(此时,count= + ut.getcount()); system.exit(0); } } }
想让volatite变量出点丑,还是需要花点功夫的。此段程序的运行逻辑如下:
启动100个线程,修改共享资源count的值
暂停15秒,观察活动线程数是否为1(即只剩下主线程再运行),若不为1,则再等待15秒。
判断共享资源是否是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。
如果没有找到,继续循环,直到达到最大循环为止。
运行结果如下:
循环到:40 遍,出现线程不安全的情况
此时,count= 999
这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,在多个线程修改的情况下,count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程的安全。
在解释原因之前,我们先说一下自加操作。count++表示的是先取出count的值然后再加1,也就是count=count+1,所以,在某个紧邻时间片段内会发生如下神奇的事情:
(1)、第一个时间片段
a线程获得执行机会,因为有关键字volatile修饰,所以它从主内存中获得count的最新值为998,接下来的事情又分为两种类型:
如果是单cpu,此时调度器暂停a线程执行,让出执行机会给b线程,于是b线程也获得了count的最新值998.
如果是多cpu,此时线程a继续执行,而线程b也同时获得了count的最新值998.
(2)、第二个片段
如果是单cpu,b线程执行完+1操作(这是一个原子处理),count的值为999,由于是volatile类型的变量,所以直接写入主内存,然后a线程继续执行,计算的结果也是999,重新写入主内存中。
如果是多cpu,a线程执行完加1动作后修改主内存的变量count为999,线程b执行完毕后也修改主内存中的变量为999
这两个时间片段执行完毕后,原本期望的结果为1000,单运行后的值为999,这表示出现了线程不安全的情况。这也是我们要说明的:volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证线程修改的安全性。
顺便说一下,在上面的代码中,unsafethread类的消耗cpu计算是必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个cpu资源的情景,否则很难模拟出volatile线程不安全的情况,大家可以自行模拟测试。
回到顶部
建议124:异步运算考虑使用callable接口
多线程应用有两种实现方式,一种是实现runnable接口,另一种是继承thread类,这两个方法都有缺点:run方法没有返回值,不能抛出异常(这两个缺点归根到底是runnable接口的缺陷,thread类也实现了runnable接口),如果需要知道一个线程的运行结果就需要用户自行设计,线程类本身也不能提供返回值和异常。但是从java1.5开始引入了一个新的接口callable,它类似于runnable接口,实现它就可以实现多线程任务,callable的接口定义如下:
public interface callable { /** * computes a result, or throws an exception if unable to do so. * * @return computed result * @throws exception if unable to compute a result */ v call() throws exception; }
实现callable接口的类,只是表明它是一个可调用的任务,并不表示它具有多线程运算能力,还是需要执行器来执行的,我们先编写一个任务类,代码如下: 
//税款计算器 class taxcalculator implements callable { // 本金 private int seedmoney; // 接收主线程提供的参数 public taxcalculator(int _seedmoney) { seedmoney = _seedmoney; } @override public integer call() throws exception { // 复杂计算,运行一次需要2秒 timeunit.milliseconds.sleep(2000); return seedmoney / 10; } }
这里模拟了一个复杂运算:税款计算器,该运算可能要花费10秒钟的时间,此时不能让用户一直等着吧,需要给用户输出点什么,让用户知道系统还在运行,这也是系统友好性的体现:用户输入即有输出,若耗时较长,则显示运算进度。如果我们直接计算,就只有一个main线程,是不可能有友好提示的,如果税金不计算完毕,也不会执行后续动作,所以此时最好的办法就是重启一个线程来运算,让main线程做进度提示,代码如下:
public static void main(string[] args) throws interruptedexception, executionexception { // 生成一个单线程的异步执行器 executorservice es = executors.newsinglethreadexecutor(); // 线程执行后的期望值 future future = es.submit(new taxcalculator(100)); while (!future.isdone()) { // 还没有运算完成,等待200毫秒 timeunit.microseconds.sleep(200); // 输出进度符号 system.out.print(*); } system.out.println(\n计算完成,税金是: + future.get() + 元 ); es.shutdown(); }
在这段代码中,executors是一个静态工具类,提供了异步执行器的创建能力,如单线程异步执行器newsinglethreadexecutor、固定线程数量的执行器newfixedthreadpool等,一般它是异步计算的入口类。future关注的是线程执行后的结果,比如没有运行完毕,执行结果是多少等。此段代码的运行结果如下所示:
**********************************************......
计算完成,税金是:10 元
执行时,*会依次递增,表示系统正在运算,为用户提供了运算进度,此类异步计算的好处是:
尽可能多的占用系统资源,提供快速运算
可以监控线程的执行情况,比如是否执行完毕、是否有返回值、是否有异常等。
可以为用户提供更好的支持,比如例子中的运算进度等。
回到顶部
建议125:优先选择线程池
在java1.5之前,实现多线程比较麻烦,需要自己启动线程,并关注同步资源,防止出现线程死锁等问题,在1.5版本之后引入了并行计算框架,大大简化了多线程开发。我们知道一个线程有五个状态:新建状态(new)、可运行状态(runnable,也叫作运行状态)、阻塞状态(blocked)、等待状态(waiting)、结束状态(terminated),线程的状态只能由新建转变为了运行状态后才能被阻塞或等待,最后终结,不可能产生本末倒置的情况,比如把一个结束状态的线程转变为新建状态,则会出现异常,例如如下代码会抛出异常:
public static void main(string[] args) throws interruptedexception { // 创建一个线程,新建状态 thread t = new thread(new runnable() { @override public void run() { system.out.println(线程正在运行); } }); // 运行状态 t.start(); // 是否是运行状态,若不是则等待10毫秒 while (!t.getstate().equals(thread.state.terminated)) { timeunit.microseconds.sleep(10); } // 直接由结束转变为云心态 t.start(); }
此段程序运行时会报java.lang.illegalthreadstateexception异常,原因就是不能从结束状态直接转变为运行状态,我们知道一个线程的运行时间分为3部分:t1为线程启动时间,t2为线程的运行时间,t3为线程销毁时间,如果一个线程不能被重复使用,每次创建一个线程都需要经过启动、运行、销毁时间,这势必增大系统的响应时间,有没有更好的办法降低线程的运行时间呢?
t2是无法避免的,只有通过优化代码来实现降低运行时间。t1和t2都可以通过线程池(thread pool)来缩减时间,比如在容器(或系统)启动时,创建足够多的线程,当容器(或系统)需要时直接从线程池中获得线程,运算出结果,再把线程返回到线程池中___executorservice就是实现了线程池的执行器,我们来看一个示例代码:
public static void main(string[] args) throws interruptedexception { // 2个线程的线程池 executorservice es = executors.newfixedthreadpool(2); // 多次执行线程体 for (int i = 0; i 1) notempty.signal(); } finally { takelock.unlock(); } if (c == capacity) signalnotfull(); // 返回头元素 return x; }
分析到这里,我们就明白了线程池的创建过程:创建一个阻塞队列以容纳任务,在第一次执行任务时创建做够多的线程(不超过许可线程数),并处理任务,之后每个工作线程自行从任务对列中获得任务,直到任务队列中的任务数量为0为止,此时,线程将处于等待状态,一旦有任务再加入到队列中,即召唤醒工作线程进行处理,实现线程的可复用性。
使用线程池减少的是线程的创建和销毁时间,这对于多线程应用来说非常有帮助,比如我们常用的servlet容器,每次请求处理的都是一个线程,如果不采用线程池技术,每次请求都会重新创建一个新的线程,这会导致系统的性能符合加大,响应效率下降,降低了系统的友好性。
其它类似信息

推荐信息