JAVA并发-并发编程概述

来源:互联网 发布:java 金额转换成大写 编辑:程序博客网 时间:2024/04/26 04:41
不知道各位童鞋是否干过在程序主进程中嵌入IO操作这样“任性”的事情,笔者曾经干过,而且那时候还觉得自己很棒。没办法,那时候还年轻啊。后来随着学习的深入,知道了有nio这种神一样的东西。但如果有其他方案,至少笔者是不会使用nio这个类的。毕竟程序的易读性还是很重要滴。多数时候,我们完全可以使用多线程和IO阻塞来代替非阻塞IO。
最近读了下经典《Java并发编程实践》,大师的书的确很棒,好了不说废话了,来写下我的笔记吧。
并发编程主要需要解决安全性,活跃性和性能的问题。下面是个人的理解:
1 安全性:程序可以确定的按照我们希望的时序执行。
2 活跃性:程序最终会输出正确的结果
3 性能:程序多长时间能输出正确的结果。
对于程序设计人员,我们需要在上面的3点之间寻找一种平衡,一种在满足安全性基础之上的平衡。
首先,我觉得有一个问题需要解决,那就是什么时候使用并发编程:并发编程一定是在需要的时候使用,什么是需要的时候?比如有一天,你的leader和你说,系统太慢了,以至于用户都快跑光了(哈哈,有点夸张),这个时候你就需要考虑一下是否可以使用多线程来做点什么。如果你确实发现了代码中有太多可以用多线程提升的地方,那么,。just do it。否则,还是不要轻易改变了,毕竟“稳定压倒一切吗”。先保证稳定在考虑性能。
当决定使用多线程的时候,我们就需要考虑安全性,活跃性以及性能了。
所谓的安全性,就是要求我们能够编写线程安全的代码,他的核心在于处理那些可以被多个线程共享的可变的变量。就像我在黑体部分说的,需要考虑线程安全的情况,一定是有下列3个条件:
1 一个可以被多个线程访问的变量
2 这个变量是共享的
3 这个变量的值是可以改变的
但你的程序中出现了这3个情况的时候,那么要小心了,需要考虑线程安全问题了。说白了,就是对于这个共享变量的访问时候的同步问题。就是常见的“读-改-写”和“核对后执行”情况。
比如我写了一个类:
@UnThreadSafeclass 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,很显然,程序没有按照我们希望的时序执行,我们说他是不安全的。
我们可以使用线面的方式来让他变成线程安全的.
@ThreadSafeclass 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方法被多个线程执行,但不存在安全性的问题(没有多个线程共享的变量);这个时候,如果为了提高性能,需要将上一次请求的数据存储起来,我们来看:
@UnThreadSafepublic 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,这时候需要考虑线程安全的问题了。有个最简单的方案将他变为线程安全的:
@ThreadSafepublic 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的执行时间将会非常慢,而整个方法有事同步的,很可能我们的并发代码 运行的性能比串行的还要差(创建和销毁线程需要时间,尽管很短)。因此,我们需要一些更加细粒度的同步,而不是像上面的粗粒度的同步。
@ThreadSafepublic 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);}}


上面的类是线程安全的,显然他比之前一个在性能上会更有优势。所以在解决多线程安全性的问题上,关于同步的粒度的大小需要我们仔细的研究,力度大一点可能实现起来简单,但是性能不一定太好。粒度太小的话,可能实现起来难度大,容易出错,怎么做到合适,需要我们自己权衡。

这个时候,如果我还想保存命中的变量
@ThreadSafepublic 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 加锁的粒度粗细的权衡

0 0