Java并发编程的艺术-笔记

并发存在的问题:上下文切换耗时,死锁,软硬件资源限制
解决方法:减少上下文切换:
1.无锁并发编程:让不同的线程处理不同的数据段(将数据ID采用hash算法分配给不同的线程)
2.CAS算法:Compare and Set使用JNI
3.使用最少线程:减少处于waiting状态的线程(jstack与dump分析)
synchronize使线程获得monitor,调用wait进入wait set
每次waiting到runnable都会进行一次上下文的切换
4.协程:单线程中实现多任务的调度,单线程中多任务的切换。

造成死锁的原因:
synchronized(A){
synchronized(B)
}

synchronized(B){
synchronized(A)
}
拿到锁因为一些异常没有释放锁
拿到一个数据库锁释放时抛出异常
死锁解决:避免一个线程同时获取多个锁
一个锁只占用一个资源
定时锁:lock.tryLock(timeout)替代内部锁
数据库锁加减锁必须在一个数据库里

资源限制:硬件有带宽上下行速度,硬盘速度,CPU速度
软件有数据库连接数,socket连接数等。
解决:硬件使用服务器集群,hash机器
软件:资源池复用,如使用连接池将数据库和socket连接复用,或者在调用webservice接口获取数据是只建立一个连接

线程数比连接数要大时线程会被阻塞,等到数据库连接。

volatile 追加到64位,避免头尾节点互相锁,声言lock#,修改后写回内存并使其他值无效
synchronized 对象头存储锁信息,重量级,响应缓慢
原子操作:
原子性由对总线加锁和对缓存加锁来实现多处理器间的原子操作,对处理器使用LOCK#
总线锁定会导致CPU和内存之间的通信锁住,开销很大,因此一般使用缓存锁
AutomicReference类可以保证引用对象之间的原子性,把多个变量绑定在一起进行原子操作

线程间的通信机制:共享内存和消息传递
通信:本地内存和主内存,本地内存刷新至主内存,再从主内存中去读取


编译器优化的重排序(单线程程序语义)
指令集并行的重排序(多条指令的重叠执行)
内存系统的重排序(缓存和读写缓冲区)

写缓冲区在处理器中会造成麻烦,写缓存再刷新过程中读取到的数据是脏数据
在多处理器之间需要对他们进行重排序
内存屏障禁止重排序 常见开销最大:StoreLoadBarries
happens-before规则满足了以上重排序
数据依赖性(指对同一个变量做操作,其中有一个为写操作,即存在数据依赖性)
as-if-serial(单线程中允许重排序)
重排序会打乱多线程的执行顺序

顺序一致性保证所有内存的读写操作都具有原子性
总线事务包括读事务和写事务
总线会仲裁,同时只有一个处理器能向总线发出请求
监视器会对其他处理器保持互斥
处理器(计算)->总线->内存(存储)

volatile变量相当于对读写long或double的操作进行加锁,也就是synchronized
锁的语义决定了临界区代码的执行具有原子性
volatile具有原子性,但volatile++不具有原子性
volatile的写相当于锁的释放,volatile的读相当于锁的获取
volatile写前面提到过是把本地内存中的共享变量值刷新到主内存
读的时候将本地内存置为无效,然后去主内存中读取
通过添加内存屏障
但其实volatile只对变量有用,而锁是互斥执行直接管理临界区

线程释放锁就是把线程对应的本地内存中的共享变量刷新到主内存中
线程获得锁的时候也是会把该线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从主内存中读取共享变量
AQS框架
独占锁和共享锁,concurrent包
独占锁(悲观锁)模式下每次只有一个线程能持有锁,ReentrantLock就是以独占的方式实现的互斥锁。
共享锁(乐观锁)允许多个线程同时获取锁,并发访问共享资源,如ReadWriteLock(一写多读)
AQS队列中是存放等待线程
还有公平锁和非公平锁
CAS更新同时具有volatile读和volatile写语义

 

concurrent包的实现:
1.声明共享变量为volatile
2.使用CAS的原子条件更新实现线程间的同步与通信

static特性:
在装载时初始化,不是每次创建新对象时都初始化
修饰的属性称为类属性或类方法,可以直接通过类名调用
final特性:其值不可变,其引用不可变

延迟初始化可以降低初始化类和创建对象的开销

通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源。
interrupted()和cancel()

线程开始运行时有自己的栈空间
等待通知机制:生产者和消费者通过检测变量看是否外部环境发生改变
管道输入输出主要介质为内存,是线程间的数据传输

读写锁:高16位读,低16位写,读状态S=2,写状态S=3
使用位运算写状态=S&0x0000FFFF(抹去高16位)
读状态S>>>16
写状态增加1时,等于S+1,读状态增加1时,等于S+(1<<16),也就是S+0x00010000
写锁是一个支持重进入的排它锁。读锁是一个支持重进入的共享锁。
如果当前线程已经获取了写锁,则增加写状态。
如果当前线程在获取写锁时,读锁已经被获取,则当前线程进入等到状态。
只有在读锁都被释放后写锁才能被获取
写锁也会阻塞之后的所有读写操作

HashMap是线程不安全的,在并发中可能会导致死循环
HashTable线程安全但又效率低下
HashMap的put操作会导致多线程中,HashMap的Entry链表形成环形数据结构,这样Entry的next的节点永远不为空,就会产生死循环获取Entry
HashTable使用了synchronized来保证线程安全,但激烈环境下HashTable效率非常低下,阻塞轮询,put添加阻塞get
ConcurrentHashMap也就是使用了锁分段技术,容器里有多把锁,每把用于锁一部分数据,就不会产生锁竞争
ConcurrentLinkedQueue使用了非阻塞循环CAS来实现线程安全队列
(需要做的事有定位尾节点,使用CAS算法将入队节点设置成为节点的next结点,直到成功)
也就是看tail后的next

通知模式
Fork/Join框架:细分子任务
工作窃取算法,使用双端队列

基于AQS实现的同步器包括:
ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask
AQS由于使用了原子操作,因此是很多阻塞队列的底层结构

 

猜你喜欢

转载自blog.csdn.net/parallel2333/article/details/81152601