Java中的并发问题

Java 集合中的快速失败机制

有线程在遍历集合的同时,有另外线程进行了集合结构的修改,则会引发异常,异常是告知遍历集合的线程当前集合已经发生了改变[modcount++],要求重新获取遍历器迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出 任 何 硬 性 保 证 。 快 速 失 败 迭 代 器 会 尽 最 大 努 力 抛 出ConcurrentModificationException,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。

它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时有可能会产生 fail-fast 机制。记住是有可能,而不是一定。

两个线程同时修改 List,编程不会有问题,但是执行结果不可提前估算,所以不能使用ConcurrentModificationException 不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。

迭代器在调用 next()、remove()方法时都是调用 checkForComodification()方法,该方法主要就是检测 modCount == expectedModCount ? 若 不 等 则 抛 出
ConcurrentModificationException 异常,从而产生 fail-fast 机制。

解决并发修改问题的方法

使用同步处理----并发性
不能使用 wait 方法,因为会释放锁
使用并发集合 java.util.concurrent 包
特殊情况:在一个线程中遍历数据的同时进行修改
解决方案是:不要采用 List 中提供的 remove 方法,而是使用 Iterator 中提供的 remove方法,则不会再报异常
在这里插入图片描述
针对 List 的特殊迭代器 ListIterator
Iterator 只能单向遍历集合中的元素,只支持删除元素;而 ListIterator 可以双向遍历,并允许添加、修改和删除元素、
在这里插入图片描述

java 并发编程

三种性质

  • 可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存可导致可见性问题
  • 原子性:一个或多个 CPU 执行操作不被中断。线程切换可导致原子性问题
  • 有序性:编译器优化可能导致指令顺序发生改变。编译器优化可能导致有序性问题。

三个问题

  • 安全性问题:线程安全
  • 活跃性问题:死锁、活锁、饥饿
  • 性能问题:
    使用无锁结构:TLS 线程局部存储,Copy-On-Write,乐观锁;Java 的原子类,
    减少锁的持有时间:让锁细粒度。如 ConcurrentHashmap;再如读写锁,读无
    锁写有锁

C 语言中的原意:禁用 CPU 缓存,从内存中读出和写入。Java 语言的引申义:Java 会将变量立刻写入内存,其他线程读取时直接从内存读(普通变量改变后,什么时候写入内存是不一定的)、禁止指令重排序
解决问题:

  • 保证可见性
  • 保证有序性
  • 不能保证原子性

是一种轻量级的线程安全处理机制

互斥锁 sychronized
锁对象:非静态 this,静态 Class,括号 Object
参数预防死锁:互斥:不能破坏
占有且等待:同时申请所有资源
不可抢占:sychronized 解决不了,Lock 可以解决
循环等待:给资源设置 id 字段,每次都是按顺序申请锁
等待通知机制:wait、notify、notifyAll
重要说明
在 JDK 1.6 之前,synchronized 是重量级锁,效率低下
从 JDK 1.6 开始,synchronized 做了很多优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销
synchronized 同步锁一共包含四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。synchronized 同步锁可以升级但是不可以降级,目的是为了提高获取锁和释放锁的效率
synchronized 修饰的代码块: 通过反编译.class 文件,通过查看字节码可以得到:在代码块中使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指明同步代码块的结束位置
synchronized 修 饰 的 方 法 : 查 看 字 节 码 可 以 得 到 : 在 同 步 方 法 中 会 包 含ACC_SYNCHRONIZED 标记符。该标记符指明了该方法是一个同步方法,从而执行相应的同步调用

公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于 Java ReentrantLock 而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于 synchronized 而言,也是一种非公平锁。由于其并不像 ReentrantLock 是通过AQS 的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取该锁。
对于 Java ReentrantLock 而言, 他的名字就可以看出是一个可重入锁,其名字是 Re entrant Lock 重新进入锁。
对于 Synchronized 而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
特殊的是 ReentrantReadWriteLock 可重入读写锁:读锁属于共享锁,写锁属于独占锁一个线程持有读锁获取写锁时互斥持有写获取读没问题ReentrantReadWriteLock 是 Lock 的另一种实现方式,ReentrantLock 是一个排他锁,同一时间只允许一个线程访问,而 ReentrantReadWriteLock 允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock 能够提供比排他锁更好的并发性和吞吐量。

独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于 Java ReentrantLock 是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,多线程中读写\写读\写写的过程是互斥的。独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。对于 synchronized 而言,当然是独享锁。

乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 -CAS悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在 Java 中的使用,就是利用各种锁。
乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,典型的例子就是原子类,通过 CAS 自旋实现原子操作的更新。

偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对 Synchronized。在 Java 5 通过引入锁升级的机制来实现高效 Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁
在 Java 中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU
自旋锁的本质:执行几个空方法,稍微等一等,也许是一段时间的循环,也许是几行空的汇编指令。
tryLock 是防止自锁的一个重要方式。
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

while(!lock.tryLock()){
    
     // lock.lock();
 //计算 1+2+...+1000=?
}

锁消除
即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持,那么是什么是逃逸分析?对于虚拟机来说需要使用数据流分析来确定是否消除变量底层框架的同步代码,因为有许多同步的代码不是自己写的

线程阻塞的代价

java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,这将会消耗很多 CPU 处理时间;
如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized 会导致争用不到锁的线程进入阻塞状态,所以说它是 java 语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM 从 1.5 开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

猜你喜欢

转载自blog.csdn.net/Lecheng_/article/details/115094350