Java并发编程随笔

来源:互联网 发布:淘宝微淘需要收费吗 编辑:程序博客网 时间:2024/06/10 19:21
一.什么是线程
操作系统在运行一个程序时,会为其创建一个进程。现代操作系统调度的最小单元是线程,也叫轻量级进程,一个进程里可以创建多个线程,这些线程都拥有各自的计数器,堆,栈和局部变量等属性,并且能够访问共享的内存变量。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。
二.为什么使用多线程
1.更多的处理器核心。
一个线程在一个时刻智能运行在一个处理器核心上,所以一个单线程程序在运行时智能使用一个处理器核心。使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间。
2.更快的响应时间
一些场景可以将数据一致性不强操作派发给其他线程处理,这样做的好处是相应用户请求的现成能够尽可能快的处理完成,缩短响应时间,提升用户体验。
3.更好的编程模型
三.线程的状态
1.NEW,初始状态,线程被构建,但是还美欧调用start()方法
2.RUNNABLE,运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行中“。
3.BLOCKED,阻塞状态,表示线程阻塞与锁。
4.WAITING,等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)。
5.TIME_WAITING,超时等待状态,该状态不同于WATING,它是可以在指定的时间自行返回的。
6.TREMINATED,终止状态,表示当前线程已经执行完毕。
四.volatile关键字
1.在多线程并发编程中,synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多线程开发中保证了共享变量的可见性。可见性是指当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它不会引起线程上下文的切换和调度,所以执行成本更低。
2.volatile一个重要特性是可以避免重排序,典型场景是单例模式中,变量用volatile修饰,如下所示:
public class DoubleCheckLocking{    private static Instance instance;                         //1                                                                             //2    public static Instance getInstance(){                  //3        if(instance == null){                                      //4            synchronized(DoubleCheckLocking.class){ //5                if(instance == null){                             //6                    instance = new Instance();              //7                }            }        }        return instance;    }}


第七行 instance = new Instance();创建一个对象可以分解为如下三步:
memory = allocate();//分配对象的内存空间
ctorInstance(memory)//初始化对象
instance = memory;//设置Instance指向刚分配的内存地址
上面2,3之间可能会被重排序,会导致在多线程的情况下第二个线程会访问到一个还未初始化的对象。解决方法是不允许2,3重排序,加上volatile修饰。
注意,这里加volatile和可见性没有关系。因为synchronized已经可以保证可见性。
五.Java并发容器和工具类
1.ConcurrentHashMap
ConcurrentHashMap是线程安全且高效的HashMap。在并发编程中使用HashMap可能导致程序死循环,而使用线程安全的HashTable效率又非常低下。
HashMap在多线程情况下put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表行程环形数据结构,一旦行程环形数据结构,Entry的next节点永远不为空,会产生死循环获取Entry。
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下效率非常低下,因为当一个线程访问HashTable的同步方法时,其他线程无法访问HashTable的同步方法。如线程1使用put添加元素,线程2不但不能put添加元素,也不能get元素,所以竞争越激烈效率越低。
ConcurrentHashMap使用锁分段技术提升并发访问率,HashTable效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,加入容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程就不会存在锁竞争,这就是ConcurrentHashMap所使用的锁分段技术。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须先获得对应的Segment锁。
ConcurrentHashMap的get操作不需要获取锁,除非读到的值是空才会加锁重读。原因是它的get方法里将要使用的共享变量都定义成volatile类型,如用于统计当前Segment大小的count字段和用于存储值的HashEntry的value,能够在线程之间保持可见性,在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁
ConcurrentHashMap的put操作需要加锁。put操作首先定位到Segment,然后再Segment里进行插入操作,插入操作需经过两个步骤,一判断是否需要对Segment里的HashEntry数组进行扩容,二定位添加元素的位置,将其放入HashEntry数组里。
ConcurrentHashMap不会对整个容器进行扩容,而只对某个Segment进行扩容。
2.CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
CountDownLatch相当于一个闸门,使用时通过构造器接受一个int型参数作为计数器,当我们调用CountDownLatch的countDown方法是,计数器减一,CountDownLatch的await方法会阻塞当前线程,知道计数器变为0,。CountDownLatch不会重新初始化或修改计数器的值。
3.CyclicBarrier
CyclicBarrier的意思是可循环使用的屏障。它要做的是让一组线程到达一个屏障是阻塞,知道最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续执行。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我到达了屏障,然后当前线程被阻塞。
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
4.CountDownLatch和CyclicBarrier的区别:
书上介绍说,CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,所以CyclicBarrier能处理更复杂的业务场景。看起来CyclicBarrier的功能更强大,那CountDownLatch岂不是没有意义了吗?
查了一些资料,个人理解是:CountDownLatch是一个线程等一组线程完成后,再做自己的事,明确了一个线程等一组线程。
而CyclicBarrier是这一组线程等最后一个完成之后一起做事,没有明确的谁先等谁。(语言有点乱,大家见谅。)
六.java中的线程池
1.使用线程池的好处:
(1)降低资源消耗。通过重复利用已创建的线程减低线程创建和销毁造成的消耗。
(2)提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立刻执行。
(3)提高线程管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可进行统一分配,调优和监控。
2.线程池的实现原理:
线程池中有几个重要的参数,
(1)corePoolSize,核心线程数,当提交任务时,如果当前运行的线程少于corePoolSize,则创建新线程来执行任务。
(2)BlockingQueue,阻塞队列,当运行的线程大于等于corePoolSize时,会把任务添加到BlockingQueue中。
(3)当无法添加任务到BlockingQueue时,会继续创建线程处理任务,最大不能超过maximumPoolSize线程数。如果超过了,任务将被拒绝,调用RejectedExecutionHandler.rejectedExecution()方法。
举个例子,将线程池看做一个工厂,工厂开始会一点点招到corePoolSize个工人,也可能直接招corePoolSize人,这些工人满足正常的工作需求,当受节日双十一影响时,工作量加大,这时候corePoolSize个工人无法及时处理这么多工作,就将任务放到仓库BlockingQueue中,工人做完工作就到仓库里取任务继续做,有时候工作量会非常大,仓库也放满了,这时候老板会继续招人,招到maximumPoolSize个工人来处理任务,这时候如果还处理不完任务,就会将任务抛弃掉了。一段时间之后工厂没这么多任务了,老板为节省开销,会裁掉一部分人,只留corePoolSize个工人就可以完成日常任务了。
3.常用线程池:
(1)FixedThreadPool,被称为可重用固定线程数的线程池,他的corePoolSize和maximumPoolSize都被设置为创建线程时指定的参数nThreads。它的BlockingQueue使用的是LinkedBlockingQueue,无界队列,当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过nThreads。
(2)SingleThreadExecutor是使用单个worker线程的线程池,它的corePoolSize和maximumPoolSize都被设置为1,队列使用无界队列LinkedBlockingQueue。所以SingleThreadExecutor其实是这有一个线程工作的线程池。
(3)CachedThreadPool是一个会根据需要创建新线程的线程池,它的corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的,CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,所以如果主线程提交任务的速度高于线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。
(4)ScheduledThreadPoolExecutor主要用来在给定的延迟之后执行任务或者定期还行任务,功能和Timer类似,但是功能更强大灵活,Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以指定多个后台线程数。
ScheduledThreadPoolExecutor执行过程:
线程1从DelayQueue中获取已到期的任务,到期任务只任务的time大于当前时间。
线程1执行这个任务
线程1修改这个任务的time变量到下次要被执行的时间。
线程1把这个修改time之后的任务放到DelayQueue中。

参考:Java并发编程的艺术
0 0
原创粉丝点击