JAVA并发编程实战读书笔记

今天第一天读这个书吧,感觉翻译确实有些问题,有些地方读起来并不是很通顺,感觉这本书对于我这种并发编程菜鸡不是很实用,要更深一点。

原子性:如count++这种操作其实是多步的操作,当并发执行它时一定注意加锁的操作

原子变量类:当我们遇到这类操作时,java的并发包中提供了一个原子变量类,用于实现数值和对象引用的原子状态转换。如通过AutomicLon个替代long类型的计数器,能过确保所有对计数器状态的访问操作都是原子的。

 

重入:一个线程能再次获取它已经持有的锁。

可见性:JVM内存中是有工作内存和主内存的区分的,假如我们在工作内存完成了写操作,但是并没有刷新到主内存中去,这是另一个线程进行了读取,那么读取的是过期的数据。

当我们加上内置锁synchronized可以保证可见性,假设当线程A执行获取x锁的代码块,线程B执行这个同步代码块时可以看到线程A中的所有操作

实际上synchronized实现了下面的步骤

1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

 

并且还有重排序问题,这方面理解不是很深,因此后面来补

注意:在线程没同步的情况读取变量时,可能会得到一个有效值,但至少这个是由之前的某个线程设置的值,并不是随机值,这中安全性保证被称为最低安全性。最低安全性适用于绝大多数变量,JVM规定对于变量的读取操作和写入操作必须是原子操作。但是存在一个例外,非volatile类型的64位数值变量(long和double),JVM允许将64位的读操作或者写操作分解成两个32位的操作,当读取一个非volatile类型的long变量时,当读和写在不同线程时,很可能会读取到某个值得高32位和另一个值的低32位。

 

volatile:Java语言提供了一种稍弱的同步机制,即volatile变量,保证了可见性,不保证原子性。用来确保将变量的跟新操作通知到其他线程, 这个也与重排序有关,看完回来补充。

同步容器类:包括Vector和HashTable

     实现线程方法:将它们的状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态,但是在迭代过程中如果更改容器类可能会导致长度改变,那么它们表现的行为是快速失败的,这种方式中通过一个计数器的变化和容器类相关联,如果迭代期间计数器被修改,那么抛出异常。但是这种计数器检查并没有再同步情况下进行,因此可能看到失效的计数值,而迭代器可能还没有意识到已经发生修改,这是设计上的权衡,从而降低并发修改操作的监测代码对程序性能的影响。

并发容器:同步容器将所有容器状态都串行化实现它们的线程安全性。这种做法降低了并发性。

并发容器是为了针对多个线程并发访问设计的,在Java5.0增加了ConcurrentHashMap,用来替代同步且基于散列的map,以及CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步的List。

ConcurrentHashMap:同步容器类在执行每个操作期间都持有一个锁,但是ConcurrentHashMap使用了不同的加锁策略来提供更高发行和伸缩性,使用了粒度更细的锁:分段锁

并发容器提供的迭代器不会抛出异常,因此迭代过程中不需要加锁,

Executor:

Executor是一个接口,为了灵活且强大的异步任务执行框架提供了基础,它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,它基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。

使用execute方法来执行任务

看下它的api关系图。

我们看看它的子接口

ExecutorService:

 它扩展了Executor接口,添加了一些用于生命周期管理的方法。

有两种关闭线程池的方法。

shutdown:平缓的关闭,不接受任何新任务,等待提交的任务执行完成

shutdownNow:粗暴的关闭,它将尝试取消所有运行中的任务。

接下来看下ThreadPoolExecutor:

作为executor的实现类,也就是具体的线程池,扩展了一些方法,主要是构造函数,它的构造函数有这么几个参数

  • corePoolSize 为线程池的基本大小。
  • maximumPoolSize 为线程池最大线程大小。
  • keepAliveTime 线程空闲的存活时间
  • unit 线程空闲的存活时间单位
  • workQueue 用于存放任务的阻塞队列。
  • handler 当队列和最大线:
  • threadFactory:使用threadFactory创建新线程,通过这个线程工厂能为线程设置一些状态
  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。 
    如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。 
    如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。 
    如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。 
    也就是处理任务的优先级为: 
    核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。 
    当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。 
    handler有四个选择: 
    * ThreadPoolExecutor.AbortPolicy() 
    抛出java.util.concurrent.RejectedExecutionException异常 
    * ThreadPoolExecutor.CallerRunsPolicy() 
    重试添加当前的任务,他会自动重复调用execute()方法 
    * ThreadPoolExecutor.DiscardOldestPolicy() 
    抛弃旧的任务 
    * ThreadPoolExecutor.DiscardPolicy() 
    抛弃当前的任务 

再看另一个很重要的类:

Executors:

提供了一系列的静态工厂方法用于创建各种线程池。

我们看下它里面的方法:

我们发现他的返回都是Executor的子接口,说明它的这些方法是用来创建线程池的。

源码中其实返回的是ExcutorThreadPool类。

我们详解四种定义好的线程池及其源码

先看cacheeThreadPool:

我们看它的源码:

看FixedThreadPool

这个是定长线程池,我们看下它的源码

它接受一个数字,这个数字就是线程的个数,它的存活时间为0,队列为LinkedBlockingQueue这个队列基于链表。并且是阻塞型的,那么也就是说,根据接受的线程的个数,只能创建这么多,当线程达到corePoolSize那么就进入阻塞队列,如果阻塞队列也满了,那么无法再创建新的线程

SingleThreadExecutor:

单线程池

它固定了线程的数量,只能是一个线程,处理密集型任务将会很适合。

 

CachedThreadPool:

缓存线程池

 它的corePoolSize为0,但是maximumPoolSize为最大(这里可认为不限制),保存时间为60秒,那么也就是说,当新来一个任务就要创建一个线程去执行它,这个线程池创建的所有的线程都是过时将会被回收的,如果在回收之前来了新的线程,那么我们就会使用之前空闲的的线程。

我们这个线程池的处理原则是来了新的任务有线程直接用,没有就创建新的线程,但是我们设置的corePoolSize为0,那么如果线程池中没有可用线程,都会直接进队列等待,

这里要说下SynchronousQueue的队列,这个队列其实没有容量,就像是餐厅里,厨师做好饭会放到台子上等待服务员去端走,这个台子就是我们常说的一般的缓冲队列,但是这里没有了这个台子,当我们厨师做完饭他会端着饭一直等到服务员从他手里接走。

这里我们必须结合源码说一下这里为什么这个线程池会这样

在executor中,会先判断你当前线程池中的线程的总数是否大于corePoolSize,如果大于,那么会加入到这个缓冲队列中去,

这里加入缓冲队列的方法是offer,我们看下这个队列的offer方法

如果说有另一个线程等待接受此队列则返回true,否则返回false,因为我们没有线程接受这个元素,那么我们将线程加入队列时始终返回false,因此无法加入队列,所以创建新的线程。

 

 newScheduledThreadPool;

,作为可以 定时和周期性执行任务 的线程池。

 

如何减少锁的竞争:

当我们对由某个独占锁保护的资源进行访问时,将采用串行方式--每次只有一个线程能访问它

有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有锁的时间,如果两者的乘积很小那么大多数获取锁的操作都不会发生竞争。

解决方案:

缩小锁的范围:尽可能缩短锁的持有时间,可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作,以及可能被阻塞的操作。例如I/O操作。当把一个代码块分解为多个代码块时,反而会对性能产生印象(如果JVM执行锁粒度粗话,那么可能将分解的同步块又重新合起来)。

降低线程请求锁的频率:通过锁分解和锁分段技术。

锁分解:我们将一个锁分解为多个锁,例如我们一个类对象中增加商品和增加订单两个方法用了同一个对象锁,我们将锁分解,给增加商品和增加订单两个方法都设上了不同的锁,那么降低了每个锁被访问的概率

锁分段:我们将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这成为锁分段。

例如:ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,正式这样concurrentHashMap最多能够支持多达16个的并发写入,

锁分段的劣势:要获取多个实现独占访问将更加困难开销更大,有时候有些操作需要加锁整个容器,比如ConcurrentHashMap需要扩展映射范围,以及重新计算散列值映射到更大的范围,就要获取所有锁

避免热点域:

当每个操作请求多个变量时,锁的粒度将很难降低,常见的优化措施是将一些反复计算的结果缓存起来,但是这会引入一些热点域问题

如:当实现HashMap时,需要考虑如何在size方法中计算Map中的元素数量,最简单的方法就是,在每次调用时都统计一次元素的数量,一种常见的优化措施是,在插入和移除元素的时候,都需要更新一个计数器,虽然在put和remove中增加了一些开销,确保了size方法的开销降低了

但是在单线程或完全同步的实现中,虽然能提升速度,但是,每个修改map的操作都需要更新这个共享的计数器,那么我们在加入和删除方法中有需要完成全局同步这个变量的更新,造成了问题,因此ConcurrentHashMap中的size将对每个分段进行枚举并将每个分段中的元素数量相加,而不是维护一个全局计数,为了避免枚举每个元素,ConcurrentHashMap给每个分段都维护了一个独立的计数,通过每个分段的锁来维护这个值。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值