《Java并发编程实战》学习笔记 - 第一部分

最近在复习Java的一些基础知识,重新刷了一下包括《Thinking in Java》、《Effective Java》、《Core Java》等经典书籍,发现自己对JVM以及并发这两块还不是很熟悉,于是先入手了这本《Java Concurrency in Practice》,原作者以JUC包核心成员为主,Doug Lea大神赫然在其中,不禁又想到读ConcurrentHashMap代码实现的阴影了。此外还有领导Java Collection框架设计Joshua Bloch(嗯,也是《Effective Java》的作者。。。),想必读完会有不少收获。

ok,让我们在Doug Lea大神的注目下开始本书的学习!

这本书一共分四部分,基础知识、结构化并发应用程序、活跃性、性能与测试、高级主题,本文将首先对第一部分-基础知识进行梳理总结。


第2章 线程安全性

我们平时写一个类,其中可能会有各种实例变量或者静态变量,这些变量的值决定了这个类或这个类对象所处的状态。

要编写线程安全的代码,其核心在于对状态访问操作的管理,特别是共享状态和可变状态的访问。

  • 共享:意味着变量可以由多个线程访问,例如一个全局递增的id生成器
  • 可变:意味着变量的值在其生命周期内可以变化,例如一个service的状态是running还是stopped

那么什么是线程安全性?

要清晰的定义线程安全性,首先需要弄明白所谓的“正确性”,通俗一点讲,正确性就是代码的行为跟我写的时候想要它做的事情是完全一样的(原文是:某个类的行为与其规范完全一致)。现在在来看线程安全的概念:

  • 一个类的线程安全性:当多个线程(调用方)访问某个类,且调用方没有采取任何同步措施时,不论采用何种线程调度方式或者这些线程以何种顺序交替执行,这个类总是能保持其正确性,那么这个类就是线程安全的

竞态条件(Race Condition)

在多线程并发执行的过程中,不同执行顺序可能出现不正确的结果,称之为竞态条件,简而言之就是,你的代码能不能work取决于运气。。。

最常见的竞态条件类型就是“先检查后执行”,例如,先检查是否为空,为空就初始化一下(想一想单例模式),再比如,先检查service是否ready,没有ready的话就分配资源设为ready,巴拉巴拉。。。

再一类就是“读取-修改-写入”操作,例如,i++,i--。

书里举了一个很有意思的例子,假设没有手机,没有大众点评、微信、百度地图,你跟朋友昨天下班后约了今天下午1点在某某大街的星巴克见面,结果你12:50分到了之后发现街的两头各有一个星巴克,你等到1点朋友还是没来,于是你就开始想,朋友会不会去了另外一家,你决定去寻找他,当你从前门出去的时候,跟你同样想法的你朋友,从后门进来了。。。于是,你们多久能遇到对方呢?

如何保证状态的一致性?

通俗来说就是:如果一个状态由多个变量一起定义,那么在一个原子操作中,这些变量都需要被更新到。其他线程只能在操作完成之前或者操作完成之后读取状态。

锁的重入

之前听过这个概念,但是没有仔细思考过应用场景。书里给出了一个很典型的场景:子类被synchronized修饰的方法中调用了父类被synchronized修饰的方法,如果没有锁重入机制,将发生死锁

(解释一下,在没有锁重入机制时,想象其调用过程:子类执行方法拿到锁,执行过程中调用父类方法,父类方法说,我要锁,子类说,我在执行,我不能给你,父类说,那我也不干,就死锁了。。。)

好,给出正规一点的定义:如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功,这一机制表明:获取锁的操作的粒度是线程,而不是方法调用。

锁重入的实现:

为每个锁关联一个计数器和所有者线程,例如(count,pid),当count为0,表示锁无人持有,每次持有锁之后count++,重入锁也是count++,退出重入的代码块之后,count--,退出最外层代码块之后count减到0,释放锁。

不要滥用synchronized

同步的代价是很高的,最坏情况下,把多个操作并行完全变成了挨个串行操作,必须要判断同步代码块的合理大小,当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O),尽量不要持有锁。


第3章 对象的共享

本章一开始就提到了“重排序”的概念,所以说这本说看上去的定位是给有经验的开发者看的。

重排序是指:在没有同步的情况下,编译器会对一些字节码的操作进行优化,使得其执行顺序跟代码中写的顺序不一定一致,且编译器认为这样执行起来更快且应该不会出错呢。

最低安全性:就算没有使用同步,你总能读到由其中一个线程设定的状态(值),尽管它可能不是正确的,但至少不是随机值。

而在64位的读写操作上,连这一点都不能保证,非volatile修饰的long和double变量的读写是会分解为两个32位操作的,也就是说,并发情况下,可能读到某个数的高32位和另外一个数的低32位,构造出一个不可预期的值。所以对long和double类型来说,多线程情况下要么使用volatile修饰(稍后会介绍),要么使用其对应的Atomic类型。

volatile变量

这是Java语言提供的一种较弱的同步机制,编译器和运行时都会知晓volatile变量是共享的,从而不会将volatile变量的操作参与到指令重排序之中,也不会被缓存在寄存器中,因此读取的volatile变量时候总能拿到最新的值。

volatile变量的读写操作不会加锁,所以也不会造成线程阻塞,所以说它是比synchronized关键字更轻量级的同步机制。

如何理解“volatile变量对可见性的影响比volatile变量本身更为重要”?

线程A首先写入一个volatile变量,线程B稍后读取了该变量,那么,在A写入之前,A能访问到的变量的值,对于B来说,在其读取了volatile变量后也是可见的。

这段听上去有点费脑子,其实就是说,由于volatile关键字避免了该变量参与指令重排序,如果A的写操作在B的读操作之前进行,那么在A的写操作之前看到的东西(比如其他变量被修改的值)B也能看到。

volatile使用场景

一般用于检查某个状态标记来判断是否做点啥事,比如生命周期事件的发生(初始化、关闭等)。

volatile只能保证可见性,并不能保证原子性,例如count++,而加锁两者都可以保证。

当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者确保单线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中(这翻译的,又说的不是人话了,我想了想,应该指的是该变量不会与其他变量进行比较操作之类,例如判断条件a<b)
  • 在访问变量时不需要加锁

安全的对象发布:不要在构造过程中使得this引用“逸出”。例如在构造函数中,创建线程,但不要启动它。

如果想要在构造函数中启动线程,可以使用,似有构造函数加上public工厂方法来避免不正确构造过程。

线程封闭:

一种避免使用同步的方法就是不共享数据,讲到这里是不是想到了threadlocal?没错就是它。

在讲threadlocal之前,书中还提到了一种叫栈封闭,其实就是方法中的局部变量不会被共享,且要注意防止引用对象逸出。

ThreadLocal类能使得线程中的某个值与保存值的对象关联起来,每一个使用ThreadLocal类变量的线程都有一份独立的副本。

举个例子,一个transactionmanager里面维护了一个threadlocal类的队列,存放着一个事务和其子事务,每一个调用方在执行事务操作时,其实是读取的自己线程set进去的事务队列,跟别的调用方法隔离开来。

类似的例子还包括全局的dbconnection等。

不可变对象

  • 对象创建之后不能修改
  • 对象的所有的域都是final类型
  • 对象通过正确调用构造函数创建,且过程中this引用没有逸出

Final域

final类型的域是不可修改的,基本类型的域是值不能修改,引用类型的域是引用不可修改但本身的内容是可变的,例如list set等。

除非需要某个域是可变的,否则应该将其声明为final域

安全的发布对象:

  • 使用静态初始化:public static XXX xxx = new XXX();
  • 将对象的引用保存到volatile域或者AtomicReference对象中
  • 将对象引用保存到某个正确构造对象的final类型域中
  • 到对象的引用保存到一个由锁保护的域中(vector,hashtable,concurrentmap等)

第4章 对象的组合

设计线程安全的类

  • 找出构成对象状态的所有变量
    • 分析对象的域,以及被引用对象的域
  • 找出约束状态变量的不变性条件
  • 建立对象状态的并发访问管理策略

实例封闭

将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

可以封闭在类的一个实例中(私有成员),或者作用域(局部变量),或者封闭在线程内部,在线程内部方法间传递。

也就是说,将不可变对象封装在内部,并对外部访问接口/方法进行合适的加锁处理

Java类库中的同步容器包装器

使用了decorator设计模式,将非线程安全的容器类封装在一个同步的容器包装器对象中,而包装器将每一个接口中的方法都实现为同步方法,并将调用请求转发到底层的容器对象上。

在现有的线程安全类中添加功能

  • 继承并添加新的同步方法,但是需要考虑到父类的同步策略如果改变,可能会导致子类的同步策略失效
  • 客户端加锁:对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户端代码。必须要了解对象X使用的是哪个锁。
    • 这里说的是,就算方法上用synchronized修饰了,并不能保证方法内部想要保护的引用对象的操作是原子的,需要确定引用对象的锁是哪一个,对该对象进行synchronize。例如,synchronized(list)
  • 组合:需要同步的对象上封装一层,将实际操作委托给底层的实际对象处理,同时实现各接口的同步方法

不要忽视文档

在文档中说明客户代码需要了解的安全性保证,以及代码维护人员需要了解的同步策略

对于使用类的开发人员来说,往往很难注意到使用的类可能存在的线程不安全隐患,例如:java.text.SimpleDateFormat类就不是线程安全的。

一种好的实践是,如果类没有显式说明是线程安全的,那么使用的时候就假设它是线程不安全的,并在需要的地方考虑使用同步策略。


                   

第5章 基础构建模块

同步容器类

Vector以及Hashtable: 其实现方式是,将状态都封装起来,对所有的公有方法进行同步。使得每次只有一个线程能访问容器的状态。所以,就性能很慢啊。

而且,在使用时,仍然需要通过客户端加锁来维护复合操作的原子性。

隐藏的迭代器

容器类的tostring、containsall、removeall等方法可能会触发迭代器的调用,如果此时内容被修改会抛出ConcurrentModificationException。

并发容器类

ConcurrentHashmap

用于替代同步容器的使用,可以极大的提高伸缩性并降低风险,该容器就是专门针对多线程并发而设计的。

使用Lock Striping分段锁机制,任意数量的读线程可以并发的访问map,读写操作可以同时访问map,一定数量的写入线程可以并发的修改map(取决于segment的数量)。

但是size和isempty的语义被相应的减弱了,其返回可能只是一个估计的值,随时失效,但是考虑到并发场景中,其实用处很小。

添加了原子操作的支持,包括:若没有则添加,若相等则移除,若相等则替换操作。

CopyOnWrite容器系列

CopyOnWriteList/CopyOnWriteSet

其安全性通过在修改时创建被重新发布一个新的容器副本来实现。适用于迭代操作比修改操作多的多的场景,毕竟复制的开销很大。

阻塞队列:生产者-消费者模式

BlockingQueue

支持任意数量的生产者和消费者,典型场景为线程池和工作队列。

在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具,它们能抑制并防止生产过多的工作项,使得应用程序在复合过载的情况下变得健壮。

双端队列(Deque,读作“deck”,一直以为叫DQ来着。。。)

适用于工作密取模式,也就是能者多劳,我从队头上取任务,做完之后没事干了,不行,我找个别的队列从队尾开始取任务继续干

Latch(闭锁)

相当于一扇门,在闭锁达到结束状态之前,门一直关着不让任何线程通过,一旦门打开,将永远打开。

适用场景:确保某些活动直到其他活动都完成后才继续执行。

例如,等待所有资源初始化完才开始计算;启动服务时的依赖链,被依赖的服务启动后才能启动当前服务;等待所有参与者就绪,比如游戏的准备。

实现:CountDownLatch

使用方法:设定等待的事件数量,对应的事件执行完之后调用CountDown方法,主线程调用Await方法一直阻塞,直到减到0为止,继续执行下面逻辑。

例如,可以用来进行简单的并发测试,n个线程先启动,然后集体await,主线程调用countDown,使得所有线程同时开始执行任务。

其他工具就不赘述了,都是JUC包里的

Futuretask(异步调用,callable返回)

Semaphore(用于实现某种资源池,或对容器大小施加边界):初始化一定数量的许可证,每次操作都需要获取一个许可证并释放一个许可证

CyclicBarrier 栅栏: 用于需要反复汇集再执行下一任务的场景。

在不涉及I/O操作和共享数据访问的计算问题中,当线程数量为cpu的个数或者cpu的个数+1时,将获得最优的吞吐量。更多的线程并不会带来任何帮助,甚至会降低性能。因为资源竞争会非常影响性能。

如何构建高效且可伸缩的结果缓存

并发容器+异步计算+原子操作

第一部分到这里基本就完了,全书看完之后我会再编辑整理一下。

                                                                                                   

猜你喜欢

转载自www.cnblogs.com/windyair/p/9094829.html
今日推荐