因为上下文的创建和切换存在系统开销,所以使用了多线程不一定就快。
多线程竞争条件时会引起上下文切换,所以可以用一些方法来变使用锁。
处理多线程数据的无锁方法有:
避免死锁的方法:
java代码编译后会变成java字节码,字节码被加载器加载到JVM里,JVM找到main入口执行代码,最终需要转换成汇编指令在cpu上执行。
java中所使用的汇编机制依赖于JVM的实现和cpu的指令。
volatile是轻量级的synchronized,能保证可见性。可见性是说当一个线程修改变量后另一个线程能看到。
volatile比synchronized执行成本更低,因为他不会引起上下文切换和调度。
为了提高处理速度,处理器不直接和内存通信,而是通过缓冲区,但是操作完缓冲区不会立即写内存。但是修改使用了volatile修饰的变量,会立即将缓存回写主内存,并将其他缓存中的旧数据置为无效。
2.2.1 synchronized实现同步的基础:java中每一个对象都可以作为锁
2.2.2 java对象头信息
hotspot虚拟机的对象头主要包括两部分数据:Mark Word、Kclass Pointer。Kclass Pointer指向类元数据。Mark Word存储自身运行时数据,其存储结构如下:
2.2.3 原理
synchronized在JVM的实现原理:同一时刻只有一个线程可以获得对象监视。
monitorenter和monitorexit指令在编译后分别插入到同步代码块的开始位置和结束位置。
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
2.3.1 处理器是如何实现原子操作的
处理器可以保证基本的内存操作的原子性。当一个处理器读取一个字节时,其他处理器不能访问这个内存地址。
但是复杂的内存操作处理器不能保证原子性,复杂操作比如:跨总线宽度,跨多个缓存行,跨页表的访问。
总线锁定和缓存锁定用来解决复杂操作的原子性。
应该优先使用缓存锁定,因为总线锁定会阻塞其他处理器的所有内存操作,但是有两种情况不能使用缓存锁定:操作数据不缓存在处理器内部或者操作的数据跨多个缓存行、有些处理器不支持缓存锁定。
2.3.2 java是如何实现原子操作的
有两个实现方法:cas和锁。
cas的三个问题:
并发编程中的两个问题:线程通信和同步。通信是指线程之间交换信息。同步是指线程间发生操作的相对顺序。
java用共享内存的方式解决通信问题,在共享内存并发模型里,同步是显式进行的,程序员需要显式指定某个逻辑需要线程之间互斥执行。
3.1.1 java内存模型
JMM模型如下图。JMM决定一个共享变量的变化何时对另一个线程可见。线程共享的区域才存在内存可见性问题。
3.1.2 JMM视角:java线程通信过程
每个线程都有一个本地内存,本地内存中存储了该线程以读写共享变量的副本。线程A和B通信必须经过主存。
3.1.3 重排序
编译期和处理器会做重排序。
这些重排序可能导致内存可见性问题。解决办法:
编译期重排序:JMM编译器重排序规则会禁止特定类型的编译器重排序。
处理器重排序:JMM编译器重排序规则会要求Java编译期生成指令序列时插入特定类型的内存屏障,通过屏障禁止特定类型的处理器重排序。
内存屏障可以被分为以下几种类型
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
3.1.4 happens-before
JSR-133内存模型引入happens-before概念。
在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间存在happens-before关系。这里的两个操作不一定在一个线程。
两个操作有happens-before关系,并不是说一个操作要在另一个操作之前执行。
happens-before仅仅要求前一个操作对后一个操作可见,切前一个操作按顺序排在第二个操作之前。
happens-before与JMM关系:
程序员遵循的happens-before规则影响这JMM是否要禁止某些重排序。程序员遵循的happens-before规则有:
3.2.1 数据依赖性
单个线程中的两个操作,且其中一个是写操作,则这两个操作存在数据依赖。这两个操作不会被重排序。
3.2.2 as-if-serial语义
含义:单线程环境下,重排序后,执行结果不能变。
as-if-serial语义完美诠释了happens-before的定义。
举个例子,计算圆的面积,示例代码如下:
按照程序顺序规则,A happens-before B,但是A和B可能会重排序,B先于A执行,但是计算圆面积的结果不变,遵循了as-if-serial语义。
3.2.3 重排序对多线程的影响
举个例子。
这里1和2不存在依赖关系,当这两个操作被重排序,多线程的语义被破坏。
3,4被重排序也会破坏多线程语义。
对flag加volatile修饰,或者将读写方法加synchronized可以实现正确同步。
3.4.1 volatile特性:
3.5.1 以ReentrantLock为例,加锁和解锁的实现是基于volatile state;
CAS也依赖于volatile的读写内存语义。
3.5.2 current包
current包的通用实现模式:
对final域,编译器和处理器遵循两个重排序规则:
JMM对程序员承诺:
3.7.1 双重锁检查的由来
懒汉模式的单例初始化为例
因为synchronized存在性能开销,因此出现了“双重锁检查”。代码如下。如果第一次检查instance不为null就不需要加锁直接返回。
上面代码的问题在于第7行在实际执行时其实是三个动作
如果2,3发生重排序,将会出问题。线程B可能读到一个还没有初始化的对象。
知道了问题的原因,就有了解决方案:
JVM在类的初始化阶段(class被加载后,被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
4.2.1 中断
4.2.2 终止
4.2.3 Thread.join()
4.2.4 ThreadLocal
4.3.1 volatile/synchronized
4.3.2 wait/notify
4.3.3 等待/通知经典范式
等待方:
通知方:
示例:
lock比synchronized优势在于:
读懂这段灵魂代码就够了
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
配合lock使用,提供await()、signal()两个方法。功能可替代synchronized和wait()、nitify().
优势:一个锁可以有多个condition().
为什么用ConcurrentHashMap:
remove(key)
加段锁
要将删除的节点之前的节点clone一遍,因为next指针域是final;
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
HashEntry<K,V>[] tab = table; //table是volatile,读写代价大,所以这里先赋值给tab
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount;
HashEntry<K,V> newFirst = e.next;
*for (HashEntry<K,V> p = first; p != e; p = p.next) //要将删除的节点之前的节点clone一遍
*newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
V get(Object key, int hash) {
if (count != 0) { // count是volatile 当前桶的数据个数是否为0
HashEntry<K,V> e = getFirst(hash); 得到头节点
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v; //不加锁
return readValueUnderLock(e); // 读到空值加锁在get一次
}
e = e.next;
}
}
returnnull;
}
put
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // 扩容判断
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; //覆盖旧值
}
else { //插入头结点
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
size()
把每个segement的count相加得到size是不安全的。怎样才能安全准确高效呢?
安全的做法是在统计size时锁住put、remove、clean但是这非常低效。
所以做法是:尝试不加锁统计size,如果统计完发现在统计过程中count有修改再加锁统计。
如何能统计完发现在统计过程中count有修改?
使用modCount变量,put、remove、clean操作前都会modCount++;
无界非阻塞线程安全队列
6.1.1 入队列
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {//队列已空,更新头结点
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
6.3.1 七个阻塞队列
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
Atomic包提供三个类:
使用场景:等待所有子线程执行完再继续执行主线程
功能类似join()。
使用场景:等所有子线程执行到一个节点后再开一起继续执行。
流量控制。与lock的区别:非owner线程可以释放Semaphore。lock只有owner才能释放锁。
数据交换,支持timeout
9.1.1 处理流程
1>当前运行的线程少于corePoolSize,创建新线程执行任务(加全局锁)
2>任务数>=corePoolSize,则加入BlockingQueue
3>BlockingQueue满,创建新线程执行任务(加全局锁)
4>运行的线程数>maximumPoolSize,调用拒绝策略拒绝任务
线程池创建线程会被封装成工作线程,工作线程处理完现在任务还会继续在队列中取任务。
9.1.2 线程池的创建
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//线程存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程创建工厂
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;//调用shutdown()
static final int STOP = 2;//调用shutdownNow()
static final int TERMINATED = 3;//线程处于SHUTDOWN或者STOP状态,并且所有工作线程已销毁,任务队列被清空或者已经执行完。
9.1.5线程池大小
cpu密集型任务:N+1
io密集型任务:2*N