前阵子一直在学习一些多线程的知识,写在这里,当个备份
主要内容:
- 一些并发的基础知识
- 一些jave.util.concurrent
一些基础知识
前提
- 现在的系统中,通常会有缓存来提高速度,数据读取和写入会有延迟。
- Java程序在编译或运行时通常会做一些优化,代码真正执行的顺序不一定和代码的顺序一致
synchronized
- 表示想要进入到{}之间的代码必须要获取到lock对象的锁,而同一时间仅有一个thread可以获取到lock的锁。
- 退出一个synchronized块之后,释放锁的时候会将缓存中需要写入主存的数据都写进去。这样在这个线程中写入的数据对其他线程可见(visible)。
- 进入一个synchronized块之前,获得锁的时候会使缓存过期,这样接下来读取变量都是从主存中取(堆)。这样可以拿到其他线程visible的变量。
- synchronized对程序的reorder有一定限制,代码块内部都是可以重排序,但是代码块之间不可以。
注:ReentrantLock提供和synchronized相同的jmm语义
volatile
- volatile通常用于线程间的交互,每次对voilate变量的读操作都会读到其他线程最后写入的值。也就是说,每次写入变量,必须保证写入到主存,每次读voilate变量时,必须保证从主存中读取。
- 在每次读取volatile变量的时候会产生和获得锁一样的内存操作,使缓存失效
- 在每次写入volatile变量的时候会产生和释放锁一样的内存操作,将缓存中需要写入主存的数据都写进去
- 对于reorder的限制相当于把volatile当成一个synchronized块
注:AtomicInteger之类的原子操作类提供和volatile同样的jmm语义
final
- final域的值在构造函数中设置,如果构造正确的执行(安全发布),那么在构造函数中给final域设置的值对其他线程是可见的(其他类型的不保证)。
- 如果final域是个引用,那么可以保证引用所指向的值会是在引用赋值时或之后赋的值(不保证最新)。
public class Escape{
public Escape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomsthring(e);
}
});
}
}
内部类的实例包含了外部类实例的隐含引用,在内部类会启动事件监听线程,那么事件监听线程会在外部对象还未构造完成时就会看到外部对象的this引用,一旦事件执行使用外部对象,很可能造成错误。无论在构造函数中隐式启动线程,还是显式启动线程,都会造成this引用逸出,新线程总会在所属对象构造完毕前看到它。所以如果要在构造函数中创建线程,那么不要启动它,而应该采用一个专有的start方法来统一启动线程。
一些java.util.concurrent
CopyOnWriteArrayList:从名称上可以看出来,这个的整体实现方式是在写入的时候进行整体数据的Copy。
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
public E set(int index, E element) {
lock.lock();
try {Object[] elements = getArray();
Object oldValue = elements[index];
if (oldValue != element) {
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);}
return (E)oldValue;
} finally {lock.unlock();}
}
LinkedBlockingQueue:一个会阻塞的queue
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
###对所有的取出操作使用的锁
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
###所有的放入操作使用的锁
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger(0);
###向队列新增或者删除,都会更改count,并且通过atomic的方法来解决并发问题。
public void put(E e) throws InterruptedException {
putLock.lockInterruptibly();
try {
###所有造成count增加的都是putlock内的,所以统一时间仅可能有一个线程对count进行加操作,这时及时有对count进行减操作的线程在执行,也不会影响判断结果
while (count.get() == capacity) { notFull.await();}
enqueue(e);
###原子的++
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {putLock.unlock();}
###先释放putLock再在notEmpty上signal,避免造成死锁
if (c == 0) signalNotEmpty();
通过读写分离,减小锁的粒度,减少冲突,获得更好的效果,count采用AtomicInteger,一方面可以保证每次读的最新,同时还可以使用其中的原子方法,减少额外的同步。
ConcurrentHashMap:差不多同步的map
把数据分成多个segment,减小锁的数据范围,减少锁冲突
//Segment定义 static final class Segment<K,V>
extends ReentrantLock implements Serializable
{
//当前segment范围内的元素个数,协调修改和读取操作
transient volatile int count;
//引起tablesize变化的update次数
transient int modCount;
//保证修改信息的可见性
transient volatile HashEntry<K,V>[] table;
}
节点元素final的使用
static final class HashEntry<K,V>
{
//key不可变
final K key;
//保证value值的可见性,确保读操作不用额外同步
volatile V value;
//保证next的引用不可变,遍历操作不需要额外同步
final HashEntry<K,V> next;
}
删除操作解析
V remove(Object key, int hash, Object value) {
lock();
try {
###网上说:读取到volatile变量,保证所有读到的值最新,其实上面的lock同样有这个作用的。
int c = count - 1;
###寻找要删除的元素
if (e != null) {
###因为next是final,所以,需要特殊处理,没什么特别的,不研究
###网上说:最后一步写volatile,保证方法里面的所有更新对外可见,同样,最后unlock的时候有同样作用的。
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
Get方法分析
V get(Object key, int hash) {
if (count != 0) {
HashEntry<K,V> e = getFirst(hash); ##上面两步操作都有voilate变量的读取,保证读取数据相对较新
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
###因为value不是final,jmm中不保证其不通过锁就可以正常使用
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
/*** Reads value field of an entry under lock. Called if value
* field ever appears to be null. This is possible only if a
* compiler happens to reorder a HashEntry initialization with
* its table assignment, which is legal under memory model
* but is not known to ever occur. */
因为导致上面情况出现的仅可能是tab[index]= new HashEntry()这里逸出,而在执行这里的时候一定有lock,所以readValueUnderLock方法中通过lock的互斥可以保证读到最新。