体会Java并发编程

来源:互联网 发布:java文件夹的复制 编辑:程序博客网 时间:2024/06/08 06:10

体会Java并发编程

在做互联网系统的过程中,经常会对系统提出高并发,高性能等要求。那么什么是高并发呢?什么是高性能呢?怎么满足高并发,高性能呢?

高并发?

在一定的计算资源下,系统能够同时处理的任务越多,那么代表系统具有越高的并发。

高性能?

性能的体现在两个方面:第一,某个指定的任务单元需要“多快”才能够处理完成。例如:服务时间,延迟时间等指标描述。第二,在计算资源一定的情况下,能完成“多少”工作。例如:吞吐率,效率,可伸缩性,容量等这些指标。

满足高并发,高性能;有许多的系统架构方法。比如:LAMPLinux+Apache+

MySQL+PHP)。Linux+nginx+java.+Oracle但是底层的实现都是居于并发技术,所以必须需要熟悉并发技术。闲暇之时,对Java并发编程这块进行了思考和总结

什么是并发?为什么要并发?并发能够解决哪些实际的问题?

并发由来

计算机中加入操作系统实现多个程序同时执行,为了提高计算机资源的利用率,提高资源使用的公平性,提高实现多任务工作的便利性。这些因素也促使了进程的产生,操作系统同时运行多个程序,每个程序都运行在单独的进程中;操作系统为每个进程分配各种资源(内存,文件句柄,安全认证等等),甚至进程之间可以通讯,交换数据。

进程的产生,同时也促进了线程的出现,线程允许在同一个进程中同时存在多个程序控制流,线程会共享进程范围内的资源(内存,文件句柄,安全认证等等)。一个进程包含了多个线程。线程也被称为轻量级进程,在大多数的现代操作系统中,都是以线程为基本的调度单位。线程还提供了一种直观的分解模式来充分利用多处理器的并行性,言外之意,一个线程能够运行到多个CPU当中。而且一个程序的多线程也可以同时调度到多个CPU上运行。也就是说,多个线程能够分别单独的运行到不同的CPU上。

并发解决的问题

前面介绍并发是为了提高计算机资源的利用率,提高资源使用的公平性,提高实现多任务的便利性。传统的串行模式,模拟了人类的工作方式,每次只是做一件事情,做完了之后再做另外一件事情。比如:起床,穿衣,下楼,喝茶。但是现实的世界里,如果将事情的粒度分的更细一些,比如:喝茶的动作可以进一步的细化,打开柜子,选择茶叶,将茶叶导入水壶中,水壶可以去烧水。在烧水当中,需要等待,所以这里可以空出时间去完成别的任务。这就是任务异步特性,同理在程序运行当中也是如此。

苏宁易购,日均IP访问量50万左右,年交易额达到100亿。如此规模的网站,如果只是串行模式运行,那么显然毫无意义,完全满足不了现实的业务要求。所以支持并发,而且是高并发是基本要求。

 

 

什么是线程?线程能够解决什么问题?线程存在什么问题?现实有哪些实际的运用?怎么实现线程技术?Java的线程技术有哪些?这些分别都是为了解决什么问题?现实系统当中怎么处理多线程?

线程的问题

线程就是操作系统提供的基本调度单元。一个程序包含多个线程,这些线程可以分别运行在不同的处理器当中,充分利用了多处理器的能力。同时多线程可以同时处理多种不同类型的任务,给每个任务分配一个线程,能够产生同步运行的能力。比如:在下载一个数据量比较大的报表时候,可以使用多线程同步下载,提高速度。另外,当某个任务运行时间过长或者需要等待资源时间过长,如果在单线程的环境下,就会产生堵塞,其他任务没有办法继续进行。但是,如果采用多线程,就可以解决这类问题。这类问题在实际的开发中经常的发生。比如:在写一个任务的时候,需要记录日志,日志的记录需要的时间比较长,而且记录日志和主要业务关系不紧密,所以可以启动一个新的线程来完成日志的记录,提高性能。

还有,处理灵敏度较高的GUI,在现代的GUI框架中,都是采用事件线程分发机制。当触发一个‘按钮’,就会分发一个线程去单独的执行该事情功能。其他的界面操作‘按钮’将不受到影响,可以继续触发。比如:SwingAWTAndroidGUI框架。

多线程技术存在一些方便之处,同样也存在一些问题。是和单线程完全不同的问题。

一个程序可以包含多个线程,这多个线程是为了完成共同的目标,显然就会存在线程之间的数据共享需要;线程之间的同步,通信需要。

线程之间共享数据,会带来线程安全性问题,

线程之间同步,互斥,会带来线程活跃性问题。

线程之间同步,会带来线程阻塞,那么就需要线程的唤醒,也就是线程的通信。

线程本身运行需要一定资源开销,过度的线程,会带来系统性能问题。

线程安全性

什么是线程安全性呢?请看下图:


线程不安全情况图1

1中显示初始情况下,银行账号余额Balance=100,A线程过来取出Balance的值为100,然后进行修改Balance=100+100=200;还未进行更新Balance的值;这时候B线程过来取出Balance的值Balance=100,然后进行修改Balance=100-20=80;最终结果A线程的计算结果被覆盖了,导致了银行账户的余额发生了错误。

 

有状态类图2

1,图2的情况显然出现线程安全的问题。多线程共享可变数据,并共同操作可变数据(竞态条件)导致线程安全问题。线性安全的定义【来源《Java并发编程实战》】:当多线程访问某个类(数据),这个类始终都能够表现出正确的行为,那么这就称为这个类似线程安全。那么怎么解决线程安全问题呢?

 

方法1:不共享数据,既然是共享数据导致的,最简单就是不要共享数据

如果仅仅是在单线程下访问数据,就永远是安全的了。这种技术被称为线程封闭。线程封闭的方式有三种:Ad-hoc(不介绍), 栈封闭,ThreadLocal类。

栈封闭:

无状态的类,一定是线程安全的,因为无状态的类,没有共享数据。比如图1

 

无状态类图3

3中显示的类,没有任何的属性域,也没有和任何的类有相关的域。每个线程都是调用Calander方法,计算使用的临时状态(balance),都是保存在线程栈的局部变量中,而且只能由正在执行的线程访问。线程间没有任何的共享状态。

ThreadLocal类:

这种方式其实就是将需要共享的数据,进行复制,当多线程进入访问的时候,给每个线程拷贝一份共享的数据。那么这个时候,每个线程只是在操作自己的数据。

 

ThreadLocal4

4的运行结果如下:

 

结果可以看出,产生了三个线程来完成Thread-0,Thread-1,Thread-2,分别的到了自己的数据Data值,同时在后期的使用当中都是使用自己产生的数据。

 

方法2:原子变量或者同步技术,有时候多线程必须共享数据,那么就必须多线程保持同步

一个进程包含了多个线程,多线程共享了该进程的相关的资源,那么就难免不了共享数据,存在‘竞态条件’,这种情况就可能导致数据的不一致性,所以必须考虑避免线程安全。

原子变量和同步技术,都是在某种情况下让多个线程按照顺序操作某块‘数据’。原子变量的粒度更小,只是建立共享变量上面,同步技术可以适用于代码块中。

 

原子变量图4

 

同步技术图5

原子变量的实现类在Java5 内提供了一些。图4中是列出一个AtomicLong,同步技术除了synchronized的方式之外还有更多的Lock,栅栏,阻塞队列,信号灯,同步容器等

当你需要设计一个线程安全的类,也可以使用对象组合的方式来完成,利用现有的线程安全的类组合,把安全性的问题委托给其他合适的类。

除了AtomicLong之外还有:

 

线程活跃性问题

需要解决线程安全问题,那么采用了同步锁机制,就可能出现死锁,饥饿,糟糕的响应,活锁等线程活跃性问题。

死锁,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

死锁图6

从图6可以看出A线程和B线程互相获取对方需要释放的锁,进行永久等待状态。在预防死锁的技术一般采用,第一,支持定时锁,当给锁加入定时设置,如果获取锁时候超时了,那么可以释放这个锁,避免了死锁的发生;第二,支持线程转信息来分析死锁,相当于日志追踪方式,一旦分析发现死锁,那么采取强制释放锁方法,避免了死锁的发生。

饥饿,和死锁的情况类似,当线程由于无法访问它所需要的资源而不能够继续执行时,这时候就发生了“饥饿”。导致饥饿的情况很多,可能是对线程的优先级使用不当,可能是代码结构的死循环。

糟糕的响应,一般发生在GUI应用程序当中,如果某个线程长期的占用一个锁,那么导致其他想要访问这个容器的线程就必须等待很长时间。

活锁,类似于网络访问处理过程中的多次尝试,虽然一定程度上能够保证不出现阻塞线程,但是仍然不会继续执行。它错误的将一个不可能完成的任务当做可以完成的任务。所以解决该问题可以使用规定尝试次数。

线程通信

往往同一任务会出现多个线程协作来完成,所以现在直接就需要进行通信。

在传统的Java技术中,线程之间的同步通信使用技术是Notify()Wait()。比如:当两个线程ABA运行完成了,需要通知BB运行完成了需要通知A,如果没有到自己运行就等待。这种通信技术能够满足两个线程之间的交互,但是如果是3个以上的线程通信就变得复杂了。

 

线程性能问题

有些任务,资源越多,那么就越完成的快。但是有些任务本质上是串行的,即使增加再多的资源也未必能够提高速度。

性能的问题,由Amdahl定律描述【来源《java并发编程实战》】,在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件和串行组件所占用的比重。


F是必须被串行执行的部分,N为处理器个数,speedup为加速比。当N趋近无穷大时,最大的加速比趋近于1/F,如果F50%,最大的加速比为2.

所以,在实际的开发工作当中,需要考虑的是不是增加的线程越多,那么性能就会越好。这个是错误的理解。需要综合考虑串行任务数量和线程开启数量。让系统的性能达到最优化。

线程的启动都是需要消耗CPU,内存资源的。

实际运用

Timer :调度任务,任务需要周期性的,或者按照固定时刻被调度。任务的调度不能够影响其他业务的正常运行,显然需要启动线程操作,多个线程执行同一代码块,显然会导致线程安全问题。

 

Servlet/Jsp:Servlet:应用框架用于部署网页应用程序以及分发来自Http客户端的请求,在高吞吐量的网站(苏宁网站)多个客户端可能同时请求用一个Servlet的服务,在Servlet规范中,Servlet同样需要满足被多个线程同时调用,换句话说Servlet是需要线程安全的。类似于Servlet/Jsp的还有Php,.Asp等。

 

数据库的访问:现代数据库已经被大量的使用于生产系统,大量的数据需要在前端页面展示,而这些数据基本上都来源于数据库(也可以存于缓存),所以数据库访问相当频繁。大量的线程读取,更新数据库信息,显然会导致线性安全的问题,所以数据库需要解决线性安全的问题。

 

提高系统的性能问题:比如,为了导入一个数据超过1万行的EXCEL表格数据,如果是适用单线来处理,那么效率方面会显得非常的低。这时候可以采用多线程技术,让多个线程同时处理,各自处理不同的数据段。这样导入数据效率就变得很高。

 

竞争类系统,比如:抢购,竞价。这些都是常见的互联网应用,那么在这种情况下,你必须要考虑线程安全的问题,因为它们都存在共享数据“价格”。

多线程实战

传统技术

Java线程技术中,通过ThreadRunnable来实现线程创建。创建的过程个人比较喜欢使用new Thread(new Runnable(){….}),这种方式更面向对象。

在传统的Java线程技术中,实现线程同步的技术是synchronized(Object){};它实现同步的原理是借助于唯一的Object。多个线程都需要获取这个唯一的Object,才能够执行下面的代码块。所以Object 控制范围越大,那么同步影响的线程数量就越多。比如:

Synchronized范围图5

从图中可以看出,各种情况的同步范围,一旦多线程超出了唯一Object的范围,那么同步就会失去效果。这种传统的同步互斥技术,不是那么的面向对象。

在传统的Java技术中,线程之间的同步通信使用技术是Notify()Wait()。比如:当两个线程ABA运行完成了,需要通知BB运行完成了需要通知A,如果没有到自己运行就等待。这种通信技术能够满足两个线程之间的交互,但是如果是3个以上的线程通信就变得复杂了。

在传统的Java技术中,定时任务功能是有Timer完成的。但是它的局限是只能指定一个间隔时间处理任务,不能够指定日历完成任务。一般在项目开发当中都使用框架:Quartz 

Java5技术

这三个代码包内包含了新的Java5多线程技术。首先要介绍的是Exector框架,这个框架有两方面的作用:创建线程,提交任务并且两者分开;线程池运用。我们可以通过它为“每个任务指定一个线程来完成”。前面提到为提高系统性能,需要多个线程协同完成任务,多个线程的创建就可以使用Exector框架。它提供了如下图的方法。


接着就要说到原子变量,同步容器,在具体的开发的过程中,为了提高开发速度和质量。一般都是采用原子变量,同步容器。通过组合这些同步容器可以开发出线程安全的代码。比如前面提到的ActomicLong,为原子变量。同步容器有Vector,Hashtable..它们的get,set方法都进行了同步策略,保证了读取数据的一致性。因为同步容器

除了同步容器外,Java.util.concurrent 里面还提供了并发容器,比如: ConcurrentHashMap。和Hashtable不同,它采用了更细粒度的加锁机制来实现更大程度的共享,这种锁机制是分段锁。还有CopyOnWriteList用于替代同步List,它的实现原理是“写入时复制”,当需要的修改,都会发布并创建一个新的容器副本。以下列出了其他并发容器。

 

接着是Java提供的同步工具,第一,锁技术,Lock,相当于synchronized。如下:

 

Lock6

从图6中可以看出,Lock的使用过程更加的面向对象。同时Lock还支持读写锁。如下:

 

读写锁可以适用于生产者和消费者模型中。多个生产者对资源进行放入,多个消费者对资源进行获取。是阻塞队列的基本实现方法。

JavaSemaphore,信号量,它提供一定数量的信号灯,只有当获取该信号灯的线程方能访问其中的数据或者执行方法。处理完成后,线程将会把信号灯归还回去,让其他的线程能够获取到。

JavaCyclicBarrier,栅栏,类似于平常,几个朋友约定到一个集合地点,然后一起出发旅行,只有朋友们都到齐了,才出发。

Java CountdownLatch,在一些应用场合中,需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,以便进行后面的操作。 

Java的 Exchanger,可以在对中对元素进行配对和交换的线程的同步点。每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象。Exchanger 可能被视为 SynchronousQueue 的双向形式。Exchanger 可能在应用程序(比如遗传算法和管道设计)中很有用。 

多线程测试

首先,我们在做代码的线程测试的时候,写单元测试代码,可以通过Exector框架创建多个线程运行代码,检查结果是否符合要求;同时通过system.currentTimeMillis()来测试代码的运行性能情况。

其次,如果是开发web项目的话,可以通过Apache ab 工具进行测试。它可以开启规定数目的线程数,对web功能访问,得出想要的结果。

笔者在广告平台项目的开发过程中,就采用了Apache ab 工具做性能测试,首先准备了测试数据,预先保留开始的数据原样;接着使用 ab -n 1000 -c 10 "http://xxx" 命令启动10个并发线程,对功能进行了1000的访问,检查最终的测试结果;然后继续增加并发线程,增加对功能的访问次数,来测试功能的性能极限和并发情况。

 

 

 

 

0 0