Java并发中的线程安全性

本文探讨了Java并发中的线程安全问题,包括对象不变性、线程安全的集合类、迭代器的快速失败机制、unmodifiableList/Map原理以及synchronizedList的实现。此外,还讲解了SimpleDateFormat的线程不安全原因、读写锁的工作原理,以及ArrayBlockingQueue、CountDownLatch、SynchronousQueue和Semaphore等并发工具类的用途。最后提到了工作窃取和CyclicBarrier与CountDownLatch的区别,阐述了并发场景下的容器操作策略,如如何避免ConcurrentModificationException。

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

1、线程安全性:对象的不变性条件不会在并发访问的条件下被破坏。

2、类中有复合操作(一个方法内操作两个相关联的受不变性条件约束的状态变量),不能仅靠委托实现线程安全性,需要提供加锁机制。

3、迭代器快速失败:单线程中对象如果不是调用iterator.remove()方法删除元素而是直接删除就会导致异常,实际上该异常只是对问题的预警指示器,并不解决并发问题。

4、unmodifyList/Map原理:将参数list/map传给父类的final引用,返回一个不可修改的List/Map视图。

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ?
         new UnmodifiableRandomAccessList<>(list) :
         new UnmodifiableList<>(list));
}

UnmodifiableRandomAccessList(List<? extends E> list) {
     super(list);
}
// 父类的final引用
final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
      super(list);
      this.list = list;
}
// 父类的final引用
final Collection<? extends E> c;
UnmodifiableCollection(Collection<? extends E> c) {
      if (c==null)
           throw new NullPointerException();
      this.c = c;
}

5、synchronizedList实现:可以指定mutex,默认为集合类父类SynchronizedCollection的对象本身,可以在构造方法中选择,子类的mutex基于上述两种情况。

public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
static class SynchronizedRandomAccessList<E>
        extends SynchronizedList<E>
        implements RandomAccess {

        SynchronizedRandomAccessList(List<E> list) {
            super(list);
        }

        SynchronizedRandomAccessList(List<E> list, Object mutex) {
            super(list, mutex);
        }
        
       /*....*/
}
static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
        SynchronizedList(List<E> list, Object mutex) {
            super(list, mutex);
            this.list = list;
        }
        /*....*/
}
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
        private static final long serialVersionUID = 3053995032091335093L;

        final Collection<E> c;  
        final Object mutex;    

        SynchronizedCollection(Collection<E> c) {
            this.c = Objects.requireNonNull(c);
            mutex = this;
        }

        SynchronizedCollection(Collection<E> c, Object mutex) {
            this.c = Objects.requireNonNull(c);
            this.mutex = Objects.requireNonNull(mutex);
        }
        /*....*/
}

6、若没有则添加:扩展原有的线程安全的集合类、客户端加锁实现,总结,加锁要加在同一个对象上。书中程序清单4-14中锁分别加在了类和list上,导致线程不安全。如果初始化list时将this作为mutex参数传入也不正确,因为synchronizedList不是public方法,不能被外部调用。

7、输出set时会自动调用toString()方法,对该set执行迭代。

8、监视器模式:把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

public class Counter{
    private long value = 0 ;
    
    public synchronized long getValue() {
        return value;
    }
    
    public synchronized long increment() {
        if (value == Long.MAX_VALUE) {
            throw new IllegalStateException("counter overflow");
        }
        return ++value;
    }
}

9、Simpledateformat为什么不是线程安全的?

SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息,如果SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat,同时也是共享这个Calendar引用。calendar对象有个clear后set值的过程,高并发下,set值的过程,会出现把上次set值给覆盖的情况。

public class Test {
	public static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd, HHmmss");
	public static void main(String[] args) {
		for (; ; ) {
			Runnable r = () -> {
				System.out.println(sdf.format(new Date(Math.abs(new Random().nextLong()))));
			};
			new Thread(r).start();
		}
	}
}

Exception in thread "Thread-111564" java.lang.ArrayIndexOutOfBoundsException

10、读写锁

读锁:

​ tryAcquire中线程获取写锁的条件:读锁没有线程获取,写锁被获取并且被获取的线程是自己,那么该线程可以重入的获取锁。对于读写锁而言,需要保证写锁的更新结果操作对读操作是可见的,这样的话写锁的获取就需要保证其他的读线程没有获取到读锁。

写锁的释放和ReentrantLock的锁释放思路基本相同,每次释放都是减少写状态,直到写状态值为0的时候释放写锁,后续阻塞等待的读写线程可以继续竞争锁。

写锁:

​ 读锁是支持重入的,能够被多个线程获取。在同一时刻的竞争队列中,如果没有写线程想要获取读写锁,那么读锁总会被读线程获取到。每个读线程都可以重入的获取读锁,而对应的获取次数保存在本地线程中,由线程自身维护该值。

获取读锁的条件:其他线程已经获取了写锁,则当前线程获取读锁会失败而进入等待状态;如果当前线程获取了写锁或者写锁没有被获取,那么就可以获取到读锁,并更新同步状态。读锁的每次释放都是减少读状态。

11、ArrayBlockingQueue、SynchronousQueue、Semaphore、CountDownLatch

(1)CountDownLatch

​ 在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。CountDownLatch可以用来管理一组相关的线程执行,只需在主线程中调用CountDownLatch 的await方法(一直阻塞),让各个线程调用countDown方法。当所有的线程都只需完countDown了,await也顺利返回,不再阻塞了。在这样情况下尤其适用:将一个任务分成若干线程执行,等到所有线程执行完,再进行汇总处理。

(2)ArrayBlockingQueue

​ 获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。

(3)SynchronousQueue

​ 每个插入操作必须等待另一个线程的对应移除操作,同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行 peek,因为仅在试图要移除元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能插入元素;也不能迭代队列,因为其中没有元素可用于迭代。

(4)Semaphore

​ 信号量维护了一个许可集。在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。

(5)CyclicBarrier

​ barrier 在释放等待线程后可以重用,称为可循环的barrier,允许一组线程互相等待,直到到达某个公共屏障点 。在涉及一组固定大小的线程的程序中。

12、工作密取

​ 每个消费者都有各自的双端队列,如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作,具有更好的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。

思考?

1、为什么写入复制的容器不会抛出concurrentmodificationexception

(1)请求在容器中添加元素的实现机制

​ 获取互斥锁、Copy当前元素数组创建新数组,数组内存空间增1、添加元素、请求容器数据数组内存地址变更为新数组地址、释放互斥锁、返回结果。

(2)容器迭代

​ 创建Iterator实现类(内部类)实例,传递容器当前数据数组,初始下标为0。返回Iterator实现类(内部类)实例。

总结:并发场景下对容器的添加操作是通过在容器内部数据数组的副本来完成的。对容器的迭代使用的是容器原始数据数组因为迭代不会产生修改,因此多个线程可以同时对容器进行迭代,而不会对彼此干扰或影响修改容器的线程。

2、CyclicBarrier与CountDownLatch的区别

​ CountDownLatch:等待其他线程执行完毕,主线程在继续执行,用于监听某些初始化操作,并且线程进行阻塞,等初始化执行完毕后,通知主线程继续工作执行。CountDownLatch计数的次数一定要与构造器传入的数字一致,比如构造器传入的是3,则countDown()一定要执行3次,否则线程将一直阻塞。CountDownLatch通常用来控制线程等待,它可以让线程等待倒计时结束,再开始执行。
  CyclicBrrier:循环栅栏其作用就是多线程的进行阻塞,等待某一个临界值条件满足后,同时执行。假设每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值