读《java并发编程实战》做的一些笔记

2017-12-19 22:32:15

1. 可能造成线程不安全的情况:
1.1 竞态条件:当某个计算的正确性取决于多个线程的执行时序时,就会发生竞态条件,
竞态条件并不总是会发生错误
例如: 用多线程来访问一个公共变量,并对它加1,如果不同步就会出现竞态条件
例如2:懒汉式单例模式,如果用在多线程环境也可能出现错误
1.2 重入: 如果一个线程已经持有锁,另一个线程想要再持有这个锁时就会陷入等待,
如果一个线程已经持有锁,该线程又发出请求想要持有自己的锁,那么是可以成功的
也就是说已经持有锁的线程可以再次请求持有锁
扩展:在多线程中如果一个锁没有被占用,那么会标记为0,如果一个锁被一个线程连续持有多次那么标记会递增,
重入的作用:例如在父类和子类有两个都有synchronized方法,如果在子类的synchronized方法
中用super来调用父类的synchronized方法,那么就会出现重入这种情况,当前线程会请求再持有锁,
如果不支持重入,那么就会进入死锁,当前子类的synchronized方法持有了锁,而该方法又调用了
一个获得当前锁的同步方法
1.3 发布与逸出:"发布"使一个对象能够在其他地方被使用,当某个对象不应该被发布时而发布就被称为逸出,例如如果一个对象在
构造过程中被发布就会出现错误,一个对象的构造要么只能在单线程环境中或者被同步
如果一个对象一旦被发布,那么在多线程环境中就会存在风险
2. 比较好的一种设计同步的方法:把线程安全封装到对象中,对该对象需要同步的的方法才同步
而不需要同步的方法不同步,并且只同步那些需要同步的代码以提高性能,例如修改操作需要同步,获取值操作不同步(但是可能会造成脏读情况,是否需要取决于具体情况)
注意:当一个代码块需要执行较长时间时,不要用持有锁,也就是同步,这样性能会大大下降(我认为出现这种情况通常是设计问题) 例如:IO
3. volatile关键字用来修饰long和double,变量的读取和修改都会成为原子操作
volatile变量只能保证可见性(总是返回最新的写入值),但是不能保证原子性操作,而加锁可以保证这两个
只有在以下情况才使用该关键字: 对变量的写入操作不依赖当前的变量值或者只有单线程在更新变量的值,
在访问该变量时不需要加锁,该变量不是常值
4. ThreadLocal 在ThreadLocal中可以保存当前线程的对象,可以完美的在一个线程中来共享一个对象
5. 不变形来保证线程安全:例如用final关键字来修饰
6. 怎么发布一个线程安全的对象?如果该对象运行在多线程环境中,那么需要确认该对象的哪些方法是需要线程安全的
7. 如何设计一个线程安全的类? 
1):找出构成对象的所有字段
2):找出不变形的那些字段
3):建立对象状态的并发访问管理策略
8. jdk中一些并发容器:ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet
CopyOnWriteArrayList,如果要使用并发容器不要去用Collections中的方法把容器同步后使用,而是直接使用Concurrent类的容器,这时java中性能最好的并发容器 
9. 结构化并发应用程序: 围绕任务执行来编写并发程序,重要的是划分任务的边界,比如:大多数服务器应用,比如web服务器,邮件服务器都是以
客户的请求为任务边界,一个请求就是一个任务,在一个线程中执行
10.不要无限制的创建线程,线程的执行和销毁都会带来计算机资源的消耗,过多的线程会带来性能的损耗,这也是为什么需要一个线程池
围绕任务来设计线程:
11. executor:基于生产者和消费者的一个任务执行模型,它可以构造一个线程池,解决了之前的无限制的创建线程
可以用此模型构建一个简单的线程池的web服务器,
executor的生命周期:ExecutorService扩展了Executor,封装了一些新的管理Executor生命周期(状态:运行,关闭,已终止)的方法,包括关闭,判断状态等
12.延迟任务和周期任务Timer:Timer存在一些缺陷,它的执行依赖于绝对时间(如果系统时间有错误那么可能造成错误),应该用ScheduledThreadPool代替它
Timer只会用一个线程(如果要用Timer只能new多个Timer才能是多线程的)来执行定时任务,比如一个TimerTask1的执行周期是40ms,另一个TimerTask的执行任务是10ms,那么会在执行了TimerTask1后在连续执行4次10ms的TimerTask2
另一个缺陷如果TimerTask抛出异常,那么该定时线程被终止

13.callable和Runnable描述的都是抽象的计算机任务,callable中call方法(也就是任务)可以返回一个值或者抛出异常
executors框架中的submit方法会返回一个Future对象,该对象是描述一个任务的生命周期,提供了相应的方法来判断任务是否取消或者完成以及获取任务执行的结果(callable中任务执行完成会有返回值)以及取消任务
14.任务超时后终止任务不再继续,这可以用Future来实现,其中带参数的get方法提供了该功能,get方法就是取得任务执行的返回值,如果在指定时间里没有取得任务的返回值,那么结束任务
取消与关闭:线程容易被启动但是正常的关闭却是难,jdk提供的stop和suspend存在缺陷,
15.停止线程的一种方式:自定义一个cancelled变量来保存是否取消,见例子E13,缺陷如果线程遭遇堵塞那么就不会检查是否取消变量,终止不了直到没有堵塞
16.对interrupt()方法的正确理解:interrupt并不会真正的终止线程它只是把是是否中断的标志设置为了true,它只是发出了终止线程的请求
需要线程在下一个合适的时候检查这个标志才能终止线程,某些方法如sleep wait join会严格的检查是否中断这个标志,如果他们发现中断标志位true
那么会抛出interruptException(必须先调用上诉的方法进入阻塞状态后再调用interrupt方法才会中断线程),interrupted方法是用来清除中断清除,也就是把是否中断的标志设置为false
通常中断是实现线程终止的合理方式
17.Thread线程中的几个方法的含义:
sleep(time);线程睡眠,指定睡眠时长,睡眠Time时间后线程会进入可运行状态
yield();
理论上,yield意味着放手,放弃,投降
Yield是一个静态的原生(native)方法
Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态
join();
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join();      //调用join方法,等待线程t执行完毕
t.join(1000);  //等待 t 线程,等待时间是1000毫秒。
wait和notify和notifyAll全是Object的方法,用于实现线程之间的通信
wait();
类似sleep( ), 不同的是,wait( )会先释放锁住的对象,然后再执行等待的动作。注意,这个函数属于Object类。另外,由于wait( )所等待的对象必须先锁住,因此,它只能用在同步化程序段或者同步化方法内,
否则,会抛出异常IllegalMonitorStateException.
wait(),notify(),notifyAll();
2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁,或者叫管程)
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;
18.中断线程策略:一个线程在接受到中断请求时应该做哪些工作,取消线程还是还是暂停线程还是继续线程,这需要自定义好
19.如果代码中没有调用sleep,join,wait等阻塞方法,那么中断将不起任何作用,
如果想要用中断来取消线程,任然可以在run方法中不断轮询判断isInterrupt的值来取消线程
20.可以通过Future来取消线程
21.怎样处理那些不可中断的阻塞,对于sleep这类阻塞,是可以处理中断请求并抛出中断异常的
那么如何处理不可中断的阻塞,这类问题包括有:
同步的Socket I/O 通过关闭IO可以抛出异常
同步的I/O 
Selector的异步I/O
获取某个锁的线程,当一个线程陷入阻塞获取某个锁的时候也不会处理中断请求,但是如果用JDK中的显式锁可以解决这个问题
22.通过newTaskFor来封装自己的非标准的取消线程代码
23.BlockingQueue:阻塞队列,一个队列中如果没有数据或者数据已满那么线程会被阻塞
线程池的使用
24.如果每个任务相互独立没有依赖,这将为任务并发的执行减少很多麻烦的问题,幸运的是在基于网络的服务器应用程序中
网页服务器 邮件服务器 以及文件服务器,它们的任务请求都是同一类型的并且相互独立
25.线程池的使用:合理设置线程数量以及处理任务队列中任务饱和的问题以及任务队列的管理问题
对于Executor框架newCachedThreadPool是一个很好的默认选择
26.线程工厂:可以自定义线程工厂,制造线程指定名字,未捕获异常的处理策略等等
避免活跃性危险
27.什么是活跃性:
活跃性没有明确的定义。安全性的含义是“永远不发生糟糕的事情”,而活跃性则关注于另一个目标,即“某件正确的事情最终会发生”。
当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环,从而使循
环之后的代码无法得到执行。线程将带来其他一些活跃性问题。例如,如果线程 A在等待线程B释放其持有的资源,而线程B永远都不释
放该资源,那么A就会永久地等待下去。本次将介绍各种形式的活跃性问题,以及如何避免这些问题, 包括死锁,饥饿,以及活锁。 
28.一个简单的死锁:一个线程拥有一个锁,但是确需要获取另一把锁才能继续执行,而另一把锁的拥有线程却希望获取之前线程的锁,这样看似每个线程都拥有一半的资源
但是它们却一直想等待直到获取全部资源,在这之前它们不会放弃手中的资源,这就导致了死锁,最终谁都不会获取全部的资源,而陷入了无限的等待中
糟糕的是:死锁往往是在高负载的情况下容易显露
29.调用一个方法需要获得两个锁的都可能造成死锁的问题,但是有时候两个锁并不是显式的而是隐式的
开放调用:调用一个方法时不需要获取锁,也就是非同步那么这种叫做开放调用,但这可能丢失操作的原子性
30.饥饿死锁:如果一个任务队列总有一个任务将永远执行那么其他任务将永远等待(单线程执行任务)。这叫做饥饿死锁
31.避免死锁:如果一个方法需要持有多个锁才能执行那么就可能死锁,如何避免,尽量避免这种设计,如果必须的话,对于获取多个锁的方法,要严格制定锁的顺序
不要出现交错的锁顺序,这可能容易造成死锁 例如E16给出的例子,另一种解决办法,使用显式的锁Lock,这将在后面详细学习
32.线程转储信息可以用来诊断死锁问题,它可以获取线程需要哪些锁,哪些线程在等待锁,实际上数据库事务不会造成死锁,因为会定期检查死锁,如果出现了,那么会让其中一个事务失败让另一个事务执行
但是jvm没有提供这种功能
33.除了死锁,其他的活跃性问题还有饥饿,丢失信号,活锁
饥饿:和之前的饥饿死锁类似
丢失信号:后面介绍
活锁:该问题不会阻塞线程,但是其他线程却不能继续执行下去,一般发生在事务的处理中
当一个事务失败发生回滚,该任务又会被放到队列的头部,会被反复的执行,类似于死循环一样
最终导致其他任务不能执行

  性能与可伸缩性,一个设计糟糕的并发程序甚至比串行程序更加糟糕,首先使程序正确然后提高性能
34.要想通过并发获得更好的性能必须做好:更有效利用现有的资源以及在出现新资源时尽可能利用这些新资源,这其中包括cpu资源 io 网络等
35.可伸缩性:当计算机资源增加时,程序的吞吐量和处理能力也相应的增加,大多数提高性能的技术往往会破坏可伸缩性
例如 著名的mvc模式分层后比没分层可伸缩性更好但是性能却要低,对于服务器应用程序往往负载能力比单一提升性能更为重要
36.并发带来的开销:
上下文切换:线程之间的来回切换
内存同步:同步变量,同步代码块等带来的开销
阻塞
37.降低锁的竞争:
减少锁的持有时间:缩小锁的范围,将不需要同步的代码排除在外
降低锁的请求频率:通过锁分解和锁分段适当增加锁的数量,如果只用一个锁那么竞争将会更加激烈,更多的线程将会被挂起
使用带有协调机制的独占锁(这些机制允许更高的并发性)
锁分解:例如一个方法只有一个锁this,如果可以细分,为这个方法加多个锁(不同的代码块),那么就可以降低锁的竞争
锁分段:例如ConcurrentHashMap会把map分成多个桶,每个桶用一个锁,对map的大多数操作只需要获得某个桶的一个锁。那么可以实现更好的并发可以有多个线程访问不同的桶,只要他们持有锁
  所以如果要使用并发容器不要去用Collections中的方法把容器同步后使用,而是直接使用Concurrent类的容器,性能更好 
独占锁一个资源只能同时被一个线程访问,共享锁一个资源可以被多个线程共同访问,这在显示锁Lock可以实现
38.对io的操作如日志:将io的操作封装到单独的线程中,因为io的操作会带来堵塞,降低可伸缩性,响应能力
并发程序的测试
39.测试指标
安全性测试和活跃性测试
吞吐量
响应性
可伸缩性

并发高级主题
40.显式锁: 协调对共享对象的访问在jdk5之前只有volatile和synchronized,在这之后引入了一种新的机制Lock
它并不是要替代之前的东西,而是提供一个更加高级的选择
Lock接口的实现子类
ReenTrantLock
ReadLock
WriteLock
ReadLockView
WriteLockView    
Lock的好处:更加灵活,可以解决死锁的问题,但是也面临着比较繁琐的代码编写
41:轮询锁和定时锁:由tryLock方法实现,它会尝试获取所有锁,如果在规定时间内不能获得,那么
就会释放现有的锁资源,然后返回失败,这避免了死锁           
42.条件队列,条件谓词,锁:对于内置锁synchronized来说,同一个锁上的线程有一个条件队列
而对于显式锁Lock每个condition(中文就是条件的意思)关联一个Lock,一个Lock下可以有多个Condition
condition中封装了一些线程之间通信的方法比如 await signal signalAll
条件谓词就是依靠什么依据来使线程wait或者notify,notifyAll,需要注意的是通常条件谓词的判断是
在一个while循环
43.notify和notifyAll:优先使用notifyAll而不是notify,notify只会唤醒一个锁上的线程中的一个
44.对于condition的理解:实际上Condition就是一个条件谓词,只不过用Condition可以使用细粒度的线程之间的通信
例如 先使用Condition使某个线程处于wait,那么在其他地方调用之前的Condition的signal可以使得之前的线程醒来
也就是对于一个Condition来说它在哪里使得哪个线程被睡眠,那么它调用signal时就会使得哪个线程醒来
原子变量与非阻塞同步机制(了解)
Java内存模型


猜你喜欢

转载自blog.csdn.net/qq_37667364/article/details/79326567
今日推荐