多线程学习笔记

本文深入探讨了Java并发编程的基础知识,包括synchronized、volatile、final等关键字的使用,以及如何在多线程环境下确保数据的一致性和可见性。重点介绍了CopyOnWriteArrayList、LinkedBlockingQueue和ConcurrentHashMap等集合类的并发特性,提供了实现线程安全的高效解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前阵子一直在学习一些多线程的知识,写在这里,当个备份

主要内容:
  • 一些并发的基础知识
  • 一些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();}
    }

通过对写操作进行加锁,保证线程安全,同时用volatile变量保存array,保证在读的时候不需要额外的同步操作即可获取最后修改。


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;


readValueUnderLock方法的注释
/*** 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的互斥可以保证读到最新。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值