Java并发简介

年轻的时候学会了“使用”Servlet后,感觉自己什么都会做了,之后就不停的写所谓的业务逻辑,框架(这里说的不是structs,spring等,就是说servlet)给人们屏蔽了很多复杂性(更别说构建在servlet上面的那些了),极易容易上手,上手之后就一直那样了...... 随着需求的变化和复杂性的演进,开始则是在已有的知识层面上想解决办法,结果代码很复杂,毫无设计之美。绕了很多弯路才发现原来基础的东西最重要,也最难懂,学起来最没有立竿见影的效果。框架如同工具一样,是希望开发人员以最小的学习成本投入生产,但是对于一个有追求的人讲,仅仅停留在使用工具上还是比较low,我们还需要了解工具是如何工作的,如和扬长避短,甚至改造工具以达到我们特定的需求。有很多基础的东西,我们这里主要讲讲JAVA并发,同时作为自己学习的总结。

我们为什么要并发?目前很难找到单核CPU的机器了吧,同时也很少有绝对串行化的程序了吧(自己写的Hello World之类的程序除外)。 那为什么不把不同的任务(Task)同时放在不同的CPU上执行呢? 这就是要并发的原因。JAVA并发是靠线程(Thread)实现的,线程是调度的基本单元。

线程的优势

  1. 充分利用多CPU的能力,提高系统吞吐量:在蒸煮(任务1)的过程中,同时可以炒菜(任务2)。当然你的煤气灶是单灶就没法了。。。即使在单CPU的机器上,多线程也可能提高程序的吞吐量,假如某个操作被阻塞(同步I/O操作),后续的逻辑不得不等待其完成。如果将其分开,放入不同的线程中,则在I/O上被阻塞的线程不会影响另一个线程被调度执行。比如你等烧水的时间可以刷朋友圈。 这样做的前提是,程序首先是可以被分开,另一个线程的执行并不依赖I/O操作的结果。
  2. 简化建模:你有三个任务要处理,改BUG,给老板写报告,关注股市行情等。当你正在绞尽脑汁地改bug,老板忽然发消息让你尽快交报告,还同时高频观察股价以便出手解套。有时候你要兼顾几个任务,常常让人倍感疲惫。程序也一样,有很多task要处理,如何以优雅的方式进行是一个设计问题。JAVA允许将复杂应用分解为不同的任务,每个任务分配给独立的线程运行。从而使编程逻辑清晰。很多框架如Servlet,RMI都是利用此模型开发,框架来管理请求,创建线程,平衡负载,分发(dispatch)请求给相应的业务处理组件。屏蔽了底层的细节,开发人员只需要关注业务逻辑。
  3. 提高用户界面响应:用Eclipse IDE都知道,如果触发一个长时间的任务(编译工程,搜索文件,更新等),界面依然可以接受用户的其他操作。对于长时间的任务,会放入单独的线程里面完成,从而不影响Event Thread处理用户别的很快完成的请求。
  4. 异步事件处理的简单化: Java NIO提供了一种非阻塞IO机制,在超大量请求来时,可能会触到多线程的性能瓶颈,通过NIO实现单线程内的异步IO,从而减少线程的创建数量,并且不阻塞其他逻辑。但是NIO相对比较复杂也容易出错。而通过简单的多线程,同步IO已经可以满足大部分需求。

线程的风险    

  1. 安全问题:如果你很放心地将这段代码(对象)放入多线程的环境并发调用,那就是二哥。这个getValue方法实现了自增操作,但是此操作不是原子性的(这和数据库的ACID里面的Atomicity原子性是两码事,ACID的原子性不是描述并发的!),此操作包含了读取变量,增加1,写回变量三个操作。
    public class IDGenerator{ 
    	private int value;
    	public int getValue() {
    		return value++;
    	}
    }
    


    以下是两个线程交替运行的可能的一种情况。A,B两个线程都读取了初始值9,都增加1,所以变量的结果是10而不是期望的增加两次为11的情况,这种现象也叫丢失更新(lost update)。 如果这个值是被作为数据库的主键的话,那就问题严重了。虽然多线程可以共享内存地址空间,为线程通讯提供了极大便利,但这种读写共享数据的不确定性会带来很大麻烦,我们无法预测运行结果(因为有多种可能性),很难找到问题原因。所以,必须使用JAVA的同步机制来协同多线程串行化访问共享变量。 
     

    
    
    //使用synchronized 来协同多线程访问,每次调用都会返回唯一值
    public class IDGenerator{
        private int value;
        public synchronized int getNext() {
            return value++;
        }
    }

    如果不使用同步机制,编译器,运行时,硬件都可以根据需要对代码执行顺序,时间进行优化。比如把变量缓存在CPU寄存器中,并且只对当前线程可见,这种方法优化了程序性能。而在多线程环境下,程序员需要了解如何利用这些优势并且不会破坏安全性是一项挑战

  2. Liveness 活跃性问题: 死锁,活锁,饥饿问题。

  3. 性能问题:设计良好的并发应用程序中,线程能提高程序的整体性能。但是,无论如何,线程总会带来某种程度上的运行时开销。 比如频繁调度时候的上下文切换,保存和恢复(程序计数器等),造成CPU时间更多花在调度而不是执行上。对于共享的数据,同步机制往往迫使编译器放弃某些优化,使缓冲区的数据无效,增加共享内存总线的同步流量。这些因素带来额外性能开销。

线程无处不在,所以安全性无处不在

当你通过java命令启动JVM的那一刻起,线程就已经创建了,JVM会创建守护线程比如垃圾回收器和finalization,main函数的执行则是在的主线程中。许多框架(GUI,Timer, Servlet等)也在管理线程或者线程池,来调用应用程序代码,访问应用程序状态/变量。所以,几乎所有JAVA程序都是多线程的,安全性至关重要。框架通过创建管理线程、线程池回调应用代码,应用代码访问应用数据和状态。所以,别天真以为框架会take care所有安全性相关的问题,保证程序thread-safe,事实上它们做不到. 相反,安全性问题会随着这种调用模式延伸到每一个访问数据的代码路径上。下面举几个简单的例子详细说明。

  1. Timer: 推迟或者周期运行某个TimerTask任务。应用定义TimerTask,它会在Timer管理的线程中被调用。如果TimerTask访问的数据也被其他线程同时访问,那么,TimerTask和其他线程都需要采用线程安全的方式访问这些数据。 最简单的方法则是,将线程安全封装在被访问对象中。
  2. Servlet/JSP,每一个Servlet封装了一个业务处理逻辑,web程序很可能出现大量相同的请求,所以,这个Servlet将在多个线程中被同时运行,另外,我们有时候需要多个业务逻辑同时访问一些共享的变量如application(存储在ServletContext中)和session(HttpSession存储)范围中的对象(多个Servlet在多个线程中访问应用数据),所以Servelt/jsp,filters,以及这些对象都必须线程安全。

  3. RM框架,GUI框架都有类似的调用模式和相应的安全性问题。
posted @ 2018-07-10 20:56  Ricky~~  阅读(271)  评论(0编辑  收藏  举报