不知道各位童鞋是否干过在程序主进程中嵌入IO操作这样“任性”的事情,笔者曾经干过,而且那时候还觉得自己很棒。没办法,那时候还年轻啊。后来随着学习的深入,知道了有nio这种神一样的东西。但如果有其他方案,至少笔者是不会使用nio这个类的。毕竟程序的易读性还是很重要滴。多数时候,我们完全可以使用多线程和IO阻塞来代替非阻塞IO。
最近读了下经典《Java并发编程实践》,大师的书的确很棒,好了不说废话了,来写下我的笔记吧。
并发编程主要需要解决安全性,活跃性和性能的问题。下面是个人的理解:
1 安全性:程序可以确定的按照我们希望的时序执行。
2 活跃性:程序最终会输出正确的结果
3 性能:程序多长时间能输出正确的结果。
对于程序设计人员,我们需要在上面的3点之间寻找一种平衡,一种在满足安全性基础之上的平衡。
首先,我觉得有一个问题需要解决,那就是什么时候使用并发编程:并发编程一定是在需要的时候使用,什么是需要的时候?比如有一天,你的leader和你说,系统太慢了,以至于用户都快跑光了(哈哈,有点夸张),这个时候你就需要考虑一下是否可以使用多线程来做点什么。如果你确实发现了代码中有太多可以用多线程提升的地方,那么,。just do it。否则,还是不要轻易改变了,毕竟“稳定压倒一切吗”。先保证稳定在考虑性能。
当决定使用多线程的时候,我们就需要考虑安全性,活跃性以及性能了。
所谓的安全性,就是要求我们能够编写
线程安全的代码,他的核心在于处理那些可以被多个线程共享的可变的变量。就像我在黑体部分说的,需要考虑线程安全的情况,一定是有下列3个条件:
1 一个可以被多个线程访问的变量
2 这个变量是共享的
3 这个变量的值是可以改变的
但你的程序中出现了这3个情况的时候,那么要小心了,需要考虑线程安全问题了。说白了,就是对于这个共享变量的访问时候的同步问题。就是常见的“读-改-写”和“核对后执行”情况。
比如我写了一个类:
@UnThreadSafe
class MyThread extends Thread
{
private int count=0;
public void run(){
count++;
System.out.println("counter="+count);
}
}
在这个例子中,MyThread类的变量count是可以被多个线程访问的,是共享的,是可变的(瞧,满足3个条件),我们需要考虑线程安全问题了。这是一个典型的“读-改-写”的问题,理论上我当然希望每个线程都能一口气执行完count++这条语句,但事实常常不是这样,可能thread-1刚刚读取到count为0,CPU就调度给了thread-2,这个时候thread-2读取到的count也是0,很显然,程序没有按照我们希望的时序执行,我们说他是不安全的。
我们可以使用线面的方式来让他变成线程安全的.
@ThreadSafe
class MyThread extends Thread
{
private final AtomicLong count=new AtomicLong(0);
public void run(){
int tmp=count.incrementAndGet();
System.out.println("counter="+tmp);
}
}
但是有些时候,并不总是有这样的第三方的类可以给我们使用的。
可能我们写了一个类,来根据一个请求的1-9的阿拉伯数字返回对应的大写汉字
public class MyServlet implements Servlet
{
public void service(ServletRequest req,ServletResponse res){
int i=getNum(req);
String str=getHanziByNum(i);
responseHanzi(res,i);
}
}
尽管service方法被多个线程执行,但不存在安全性的问题(没有多个线程共享的变量);这个时候,如果为了提高性能,需要将上一次请求的数据存储起来,我们来看:
@UnThreadSafe
public class MyServlet implements Servlet
{
private int num;
private String str;
public void service(ServletRequest req,ServletResponse res){
int i=getNum(req);
if(i==num)
responseHanzi(res,str);
else
String str1=getHanziByNum(i);
num=i;
str=str1;
responseHanzi(res,str1);
}
}
在上面的代码中,出现了多个线程可以访问可变的的共享变量num和str,这时候需要考虑线程安全的问题了。有个最简单的方案将他变为线程安全的:
@ThreadSafe
public class MyServlet implements Servlet
{
private int num;
private String str;
public synchronized void service(ServletRequest req,ServletResponse res){
int i=getNum(req);
if(i==num)
responseHanzi(res,str);
else
String str1=getHanziByNum(i);
num=i;
str=str1;
responseHanzi(res,str1);
}
}
程序设计人员不去具体分析哪些代码是需要同步的,而是同步一个大的区域,讲所有共享变量圈在里面,当然这样做实现了线程安全。但是没有考虑性能的问题。假设getHanziByNum()是一个非常耗时间的操作,那么同步方法service的执行时间将会非常慢,而整个方法有事同步的,很可能我们的并发代码 运行的性能比串行的还要差(创建和销毁线程需要时间,尽管很短)。因此,我们需要一些更加细粒度的同步,而不是像上面的粗粒度的同步。
@ThreadSafe
public class MyServlet implements Servlet
{
private int num;
private String str;
public void service(ServletRequest req,ServletResponse res){
int i=getNum(req);
String str1=null;
synchronized(this){
if(i==num)
str1=str;
}
if(str1=null){
str1=getHanziByNum(i);
synchronized(this){
num=i;
str=str1;
}
}
responseHanzi(res,str1);
}
}
上面的类是线程安全的,显然他比之前一个在性能上会更有优势。所以在解决多线程安全性的问题上,关于同步的粒度的大小需要我们仔细的研究,力度大一点可能实现起来简单,但是性能不一定太好。粒度太小的话,可能实现起来难度大,容易出错,怎么做到合适,需要我们自己权衡。
这个时候,如果我还想保存命中的变量
@ThreadSafe
public class MyServlet implements Servlet
{
private int num;
private String str;
private int hits;
private int totalHits;
public synchronized double getHitRadio{
return hits/totalHits;
}
public void service(ServletRequest req,ServletResponse res){
int i=getNum(req);
String str1=null;
synchronized(this){
totalHits++;
if(i==num){
str1=str;
hits++;
}
}
if(str1=null){
str1=getHanziByNum(i);
synchronized(this){
num=i;
str=str1;
}
}
responseHanzi(res,str1);
}
}
可以看到,所有共享变量出现的地方都被加了锁(即同步操作),只不过同步的力度不同罢了。
综上所述,关于并发,我们的思路大概是这样的:
1 是否使用并发
2 如果使用用并发,哪些共享变量需要考虑安全性
3 对于需要考虑安全性的那些共享变量,要记得为每一个加锁
4 加锁的粒度粗细的权衡