并发编程的目的是为了让程序运行得更快,但是不是更多的线程就能让程序最大限度的并发执行。比如上下文切换、死锁的问题,以及受限于软件和硬件的资源限制问题。
软件资源限制:有数据库的链接数和socket连接数等
硬件的资源限制有带宽的上传、下载速度、硬盘读写速度和CPU处理速度。
减少上下文切换的方法
避免死锁
建议使用并发容器和工具类来解决并发问题
java代码在编译后会变成java字节码,字节码被类加载器加载到jvm当中,jvm执行字节码,最终需要转化为汇编指令在CPU上执行。
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized。
特性:
- 可见性
如果对声明了volatile的变量进行写操作,jvm就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会系统内存,同时根据处理器的缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的数据是否过期,过期就要设置无效重新加载。
- 禁止指令重排
实现同步的基础,java中每一个对象都可以作为锁:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchonized括号里面的配置对象
每个对象有一个monitor与之关联,获得monitor获得锁,代码块同步是使用monitorenter和monitorexit实现的,插入到同步代码块的前后位置。检查是否持有monitor所有权,即尝试获得对象的锁
为了解决统一线程多次活动一个锁,需要切换上下文等代价。是一种等到竞争出现才释放锁的机制
做法:
- 在锁对象的对象头和栈帧的锁记录里存储锁偏向的线程ID
- 如果检测没有偏向锁,检测Mark Word中的锁标识是否设置为1,如果没有设置就是用CAS竞争锁
- 如果设置了1,就尝试用CAS将对象的偏向锁指向当前线程
加锁:
- 在当前线程的栈帧中创建用于存储锁记录的空间,将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,失败则与其他线程竞争,尝试自旋获得锁。
解锁:
- CAS将栈帧中存储的Mark Word替换回对象中,失败就代表存在竞争,膨胀为重量级锁
这几种都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。
使用基于缓存的加锁或基于总线的加锁
- 基于总线。使用处理器提供的一个LOCK 信号,当一个处理器在总线上输出此信号时,其他处理器的请求被阻塞,该处理器独占总线。
- 基于缓存。使用缓存一致性原则
使用锁和循环CAS的方式实现
- 循环CAS,循环进行CAS直到成功。存在三大问题:
1. ABA问题
2. 循环时间长开销大
3. 只能保证一个共享变量的原子操作,可以考虑把多个变量合成,或者使用锁
- 使用锁。偏向锁,轻量级锁和互斥锁。除了偏向锁,其他实现锁的方式都使用了循环CAS获得和释放锁
并发编程需要解决两大问题,线程如何通信和同步
通信机制有两种,共享内存和消息传递。共享内存是通过读写内存中的公共状态进行隐式通信,消息传递是发送消息进行显示通信
同步是指程序中用于控制不同线程间发生相对顺序的机制。
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,存储了副本
重排序?
设置线程优先级时,频繁阻塞(休眠或IO)的线程需要设置较高优先级,偏重计算(需要较多CPU时间或偏运算)的线程则设置较低的优先级,确保处理器不会独占。
Daemon线程用作支持性工作,在java虚拟机退出时Daemon线程的finally块并不一定会执行,所以不能依靠finally确保关闭或清理资源。
interrupt()方法中断线程,对于阻塞状态线程会抛出InterruptedException并且清除中断标识。
一个线程A调用了对象O的wait方法进入同步队列等待状态,另一个线程B调用了对象O的notify或者notifyAll方法,线程A收到通知后等待B释放锁之后,从wait状态返回。
Thread.join(),当前调用join的线程A等待线程Thread线程终止之后回来。
ThreadLocal,线程变量,一个以ThreadLocal对象为键、任意对象为值的存储结构。
线程池。一方面消除了频繁创建和消亡线程的系统资源开销,另一方面,面对多任务的提交能够平缓的劣化。
线程池的本质就是使用了一个线程安全的队列链接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程可以不断从工作队列取出线程。
使用synchronized可以隐式的获得锁,支持重进入,先获得后释放,固化了所的过程,Lock接口可控性更强
是用来构建锁或者其他同步组件的基础框架,提供了getState(),setState()和compareAndSetState来进行操作。
同步队列,依赖内部的FIFO双向队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器将当前线程和等待状态等信息加入同步队列,同时阻塞线程。
还有些内容,需要仔细再阅读
包含synchronized的基本功能,还加入了公平性、等待可中断、等待条件等功能。
公平锁可以减少饥饿发生的概率,等待越久的请求越容易优先满足,但是会频繁切换线程,降低吞吐量,所以非公平锁是默认。在获得锁的条件增加了在同步队列中该节点是否有前驱节点的判断。
如何实现:
判断当前线程是否为获得锁的线程来决定操作是否成功,如是则增加同步状态值返回true,释放锁的时候要减少状态值。
一对锁,同一时刻可以允许多个读线程访问,但是写线程访问时,所有的读线程和写线程阻塞。
用一个整型变量代表状态,高16位表示读,低16位表示写。
是一个支持重进入的排它锁。如果当前读锁已经被获取或者写锁在其他线程手里,阻塞。
如果存在读锁,写锁不能获取。原因在于,写锁要保证操作对读锁可见,如果存在读锁的时候对写锁获取,那么正在运行的其他线程就无法得知当前写线程的操作。
支持重进入的共享锁。没有其他写线程就可以访问。
保持住写锁,再获得读锁,随后先释放写锁。一个线程有写锁是可以加读锁的
适用于读优先于写,如数据库的读写,读线程一来写线程就要降级
需要首先获得锁,主要使用await和signal方法。调用await(),当前线程会释放锁,其他线程调用Condition对象的signal()方法,当前线程才从await()返回,并且已获得了锁。
在并发编程中使用HashMap可能会导致死循环(resize()方法),而线程安全的HashTable效率非常低(synchroniezd标记方法如put),所以使用ConcurrentHashMap。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构构成。Segment是一种可重入锁。HashEntry是用于存储键值对数据。
一个ConcurrentHashMap有一个Segment数组,每个Segment数组中又有一个HashEntry数组,每个HashEntry是一个链表。
初始化
先进行一次再散列,然后使用这个散列值通过散列运算定位到Segment。get操作不需要加锁,所以高效,这需要将get里面使用的共享变量都定义为volatile类型。对volatile字段的写操作先于读操作。
定位Segment是hashcode再散列之后值的高位,而定位HashEntry是直接使用再散列之后的值。
(hash>>> sgementShift ) & segmentMask
hash & (tab.length - 1)
定位Segment之后
1. 判断是否需要对HashEntry数组扩容
Segment是在插入元素之前判断,HashMap是在插入之后判断。创建一个两倍的数组,在散列插入新数组,只对某个segment扩容
2. 定位添加元素的位置,放进去
实现线程安全队列的两种方式,阻塞方式(用锁),非阻塞方式(CAS)
入队节点添加到队列的尾部。
- 第一步将入队节点设置为当前队列尾节点的下一个节点。
- 更新tail。如果tail下一个不为null,更新为入队节点,如果为空,不移动(CAS)
tail不总是尾节点,需要通过tail节点来找到尾节点。 HOPS,tail和尾节点的长度大于常量才更新。
这种设计是通过增加对volatile节点的读操作,减少了写操作,写操作开销大,所以入队效率高。
从队列头返回一个节点。
当队列头有元素,直接返回不更新head,当head为空,出队更新head,通过hops来减少cas更新head节点的消耗。
支持阻塞插入和移除的方法。
java里的阻塞队列:
- ArrayBlockingQueue,一个由数组结构组成的有界阻塞队列
- LinkedBlockingQueue,一个由链表组成的有界阻塞队列
- PriortyBlockingQueue,一个支持优先级排序的无界阻塞队列
- DealyQueue,使用优先级队列实现的无界阻塞队列
- SynchronousQueue,不存储元素的阻塞队列
- LinkedTransferQueue,由链表组成的无界阻塞队列
- LinkedBlockingDeque,由链表组成的双向阻塞队列
把大任务切割成小任务,最后汇总结果
队列从别的队列窃取任务执行。把子任务分别放到不同的队列里,并为每个队列创建单独的线程。窃取从尾部拿任务
创建ForkJoin任务,重写compute,是否足够细分,小任务join
类似线程池
允许一个或多个线程等待其他线程完成操作。类似join功能,完成一定顺序的执行。
调用countDown,N-1,await阻塞当前线程直到n为0。await等待n为0
可循环使用的屏障,让一组线程到达一个屏障被阻塞,直到最后一个到达,屏幕开门继续执行。
await()阻塞等待,还可以提供构造函数,都到达了优先执行barrierAction。
可用于多线程计算数据合并结果。
区别:
CountDownLatch只能使用一次,CyclicBarrier可以使用reset()复用
信号量,控制同时访问特资源的线程数量,用作流量控制,不满就可以进去(acquire\release),满了等着
线程间写作的工具类,数据交换。
好处:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性。
ExecutorService exec = Executors.newSingleThreadExecutor();
ExecutorService exec = new ThreadPoolEcecutor(各种参数);
核心池,线程池,工作队列,等待时间
- 线程池判断核心池是否满了,如果没有创建新线程执行,如果满了下一阶段
- 判断工作队列是否已满,没满加入,满了继续下一个流程
- 判断线程池线程是否已满,没有创建新线程执行,满了使用饱和策略(四种:抛异常、用调用者线程执行、对其队列最近一个任务执行这个、不处理丢弃)
线程池创建线程时,会将线程封装成工作线程Worker,执行任务,执行完毕后会从工作队列循环获取继续执行。
调用interrupt方法
- shutdown,设置状态SHUTDOWN状态,中断没有执行任务的线程
- shutdownNow,设置状态为stop,尝试停止正在执行或暂停任务的线程,返回等待执行的列表
考虑因素:
- 任务的性质:CPU密集型、IO密集型和混合型任务
- 优先级
- 执行时间
- 任务依赖性
CPU就尽量小,CPU+1,IO尽量多,2*CPU
建议使用有界队列
jdk5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,执行机智由Executor框架
在VM中,java线程会被一对一映射为本地操作系统线程。在上层,java多线程通常把应用分解为若干个任务,然后使用Execturo框架将这些任务映射为固定数量的线程,在底层,操作系统内核将这些线程映射到硬件处理器上
详解:
FixedThreadPool把corePoolSize和maximumPoolSize都设置为n,同时keepAliveTime为0,多余的空闲线程立即终止。使用无界队列,max和keep都没用了,不会拒绝任务
CachedThreadPool,core为0,max为Integer.MAX_VALUE,keepalivetime为60s,使用没有容量的SynchronousQueue作为工作对垒