《java并发编程实战》笔记
《java并发编程实战》这本书配合并发编程网中的并发系列文章一起看,效果会好很多。
并发系列的文章链接为: Java并发性和多线程介绍目录
建议: 《java并发编程实战》第3章和第4章可以暂时先跳过。。这部分内容的文字和概念很多,代码块偏少。不容易看进去。
一、线程
1.线程的使用可以提升程序的性能。
2.线程如果没有同步,操作的执行顺序是不可预测的。
3.线程之间共享数据时,必须使用同步机制。不然数据会发生无法预料的变化。
二、线程的安全性
0.线程安全性:当多个线程访问某个对象时,这个对象不会出错。
1.构建并发程序,必须正确使用线程和锁。要编码线程安全的代码,其核心在于要对状态访问操作进行管理,特别是共享和可变状态的访问。
2.竞态条件:在并发编程中,由于不恰当的执行时序而出现不正确的结果,称为“竞态条件”
3.加锁机制。
java提供了一种内置的锁机制来支持原子性:同步代码块
public synchronized void methodName() { }
4.假如,在并发编程中,使用synchronzied修饰service层的某个方法,在同一时刻只有一个线程可以执行该方法。
此时,该方法是线程安全的,但是多个客户端无法同时访问该方法,服务的响应效率非常低,性能差。
5.java并发中的特性。
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
内存可见性:确保当一个线程修改了对象状态后,其他线程可以看到发生的状态变化。
有序性:即程序执行的顺序按照代码的先后顺序执行。因为尽管jvm的指令重排序不会影响单个线程的执行,
但是会影响到线程并发执行的正确性。
6.同步,可以确保原子性,有序性,可见性。
三、对象的共享
1.如果多个线程同时访问同一个共享变量,没有使用同步机制会导致结果不可预测。
2.volatile这个关键字有什么用?
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
volatile变量只能确保可见性,而加锁机制既可以确保可见性,又可以确保原子性。
3.发布,是指使对象能够在当前作用域之外的代码中使用。
逸出,当某个不应该发布的对象被发布时,就称为逸出。
4.线程封闭,就是不在线程之间共享数据。线程封闭可以实现线程安全性。
ThreadLocal,保证了每个线程都有独立的对象副本,保证了对象的唯一性,维持线程的封闭性。
5.不变性,不可变对象一定是线程安全的。
6.final修饰的变量,具有不变性。
五、基础构建模块(介绍juc的api)
1.当普通的同步容器类,例如Vector,HashTable类,如果在迭代过程中被修改,就会抛出一个ConcurrentModificationException。
2.ConcurrentHashMap用于替代同步Map。ConcurrentHashMap基于锁分段技术。在这种机制下,任意数量的读取线程可以并发访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且以一定数量的的写入线程可以并发地修改Map。
3.CopyOnWriteArrayList用于替代同步List。CopyOnWrite,"写时拷背",也就是在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。因此,多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改容器的线程相互干扰。
4.阻塞队列。生产者消费者模式。
阻塞队列先进先出。BlockingQueue。可进行put()和take()操作。
5.阻塞方法和中断方法。interruptedException。
6.并发工具类。闭锁CountCownLatch,信号量Semephore,栅栏Barrier
六、任务执行
1.主要讲Executor框架。为每个任务分配一个线程。
2.无限制创建线程的不足:线程生命周期的开销非常高;资源消耗大;稳定性有影响。
3.Callable和Future,返回通过future.get()获取执行结果。
4.CompletionService相当于将Executor和BlockingQueue的功能整合在一起。
七、取消与关闭
1.中断(interrupt)并不会马上停止目标线程正在进行的工作,而是改变中断状态。。等到调用wait()、sleep()、join()等方法时,才抛出中断的异常InterruptedException 。
2.恢复中断状态的方法: Thread.currentThread().interrupt();
3.还通过Future来实现取消。Future拥有一个cancel(),方法。即 boolean cancel(boolean mayInterruptIfRunning);
可以通过设置mayInterruptIfRunning为true,取消任务。
第八章 线程池 (重要!!!)
1.**Core Pool Size**:线程池的基本大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
**Max Pool Size**:表示可同时活动的线程数量的上限。
**KeepAliveTime**:如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的。
2.newFixedThreadPool:线程池的基本大小和最大线程数量大小一样,而且创建的线程不会超时。
newCachedThreadPool:基本大小为零,最大线程池大小为Integer.MAX_VALUE,而基本大小设置为零,并将超时设置为1分钟,这种方法创建出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。
3.**BlockingQueue**:
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。这个BlockingQueue其实就是工作队列。
基本的排队方法有三种:无界队列,有界队列,同步移交(Synchronous Handoff)。
newFixedThreadPool和newSingleThreadExecutor默认使用无界的LinkedBlockingQueue。
有界队列:ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。
有界队列可以避免资源耗尽的情况发生。
有界队列被填满后,新的任务进入时,会执行拒绝策略。
4.**拒绝策略**
默认策略:中止(Abort),抛出未检查的RejectedExecutionException。
抛弃(Discard):当新提交的任务无法保存到队列中等待执行时,抛弃该任务
抛弃最旧的(Discard-Oldest):抛弃下一个将要执行的任务,然后尝试重新提交新的任务。
调用者运行(Caller-Runs):实现了一种调节机制,既不抛弃任务,也不抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。他不会在线程池某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。
十三章 显式锁
Lock接口与ReentrantLock(可重入锁)
0.使用Lock接口,必须在finally接口中释放锁。如下
Lock lock=new ReentrantLock(); lock.lock(); try{ }catch(){ }finally{ lock.unlock(); }
1.公平性。
公平锁,线程按照他们发出请求的顺序来获得锁。如果有另外一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。但在非公平锁中,则允许插队。
2.ReentrantLock和Synchronized有哪些区别?
Synchronized是基于监视锁monitor实现的,当monitor为0时,线程能够获得锁,此时monitor加一。当有线程进行请求时,判断monitor是否为0,如果不为0,说明锁已经被其他线程拿到了。Synchronized是JVM级别的锁。
ReentrantLock是基于AQS实现的,AQS是一个抽象队列容器。当有线程发起请求时,如果是公平锁,需要在队列中排除等待。如果是非公平锁,会插队。
**由于是基于AQS实现的,因此内部有一个共享状态变量state。当state为0时可以获取资源并置为1,若已获取资源,则state不断加1,这个就是可重入性的体现。在释放资源时,state减1。**
ReentrantLock还提供了定时的锁等待,可中断的锁等待,公平锁与非公平锁。
但是,ReentrantLock必须在finally里面进行unlock(),否则会出错。
读写锁ReadWriteLock
1.读写锁:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
也就是:“读读共存,写写不共存,读写不共存”。
2.思考:为什么要有读写锁?
解答:如果使用互斥锁(比如Synichronized和ReentrantLock),那么就一个线程获取锁在读的时候,其他线程不能读。
3. ReentrantReadWriteLock:可重入的读写锁。
十四章 同步工具
AQS:也就是AbstractQueuedSynchronized,抽象队列同步工具。
在AQS中,基础操作包括获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。在不同的并发类中,acquire()和release(),以及对应的state状态变量变化,有不同的表现。
state状态变量,在ReentrantLock中表示线程获取锁的次数。在Semaphore
中表示剩余的许可数量。FutureTask用它表示任务的状态(尚未开始,正在运行,已完成以及已取消)。
第十五章 原子变量与非阻塞同步机制
0.非阻塞算法 : 如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就称为“非阻塞算法”
无锁算法: 如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也称为“无锁算法”
1.原子变量类:AtomicInteger,非阻塞,基于CAS。
2.CAS:CompareAndSwap,比较和替换。
CAS包含三个操作数:需要读写的内存位置V,进行比较的值A和拟写入的新值B。
如果V的值是A,那么就将它改为B。
CAS是非阻塞的,乐观的。
3.CAS的性能随着处理器的数量而变化。
4.CAS可能存在哪些问题?
答:ABA。如果数值从A变为B,又变回A。那么即使满足CAS也无法说明数据没有变化过。
解决方案:引入状态量或版本号。也就是说即使存在ABA问题,版本号也是不同的。
其他:
1.volatile变量的读/写和CAS可以实现线程之间的通信。
把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。
如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile;
- 然后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:
疑问:
1.线程安全的概念。
解答:多个线程对数据进行读写时,数据的读写等操作结果保持正确。
2.活跃性。
解答:就是饥饿和公平。
3.竞态条件如何理解??
解答:在并发编程中,由于不恰当的执行时序而出现不正确的结果。
4.@ThreadSafe,这个注解怎么用?
5.ThreadFactory如何理解?
解答:用于创建线程,可以指定线程名称。
6.Future类有什么用?
解答:比ExcutorService多返回一个结果。
7.无阻塞算法是什么?如何区别阻塞算法?
解答:阻塞算法,比如BlockingQueue。。无阻塞算法,比如AtomicInteger.
8.java监视器模式。