JAVA并发(JUC)
- 前言
- volatile关键字
- volatile无法保证原子性
- 解决方法
- 有序性
- 使用Volatile的地方
- CAS
- AtomicInteger 的compareAndSet方法
- AtomicInteger 的getAndIncrement方法
- AtomicInteger成员变量
- CAS的缺点:
- ABA问题
- AtomicReference原子引用
- ABA问题解决
- 集合类不安全问题
- Java锁
- 可重入锁
- 自旋锁(SpinLock)
- 读写锁/独占锁/共享锁
- 常用辅助类
- 阻塞队列
- 应用
前言
相关包
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
并发:多个线程访问同一个资源(例如单核cpu处理多件事)
并行:多个线程访问多个资源一堆事情同时在做
JMM:Java内存模型(Java Memory Model),不是JVM
每个线程有自己独立的工作内存(栈空间)
主内存中有共享变量,每个工作内存中拷贝了一份主内存中的共享变量,对其进行修改,修改完成后再更新到主内存中。
JMM对此进行了控制
可能带来的问题: 可见性、原子性和有序性
可见性:某个线程对主内存的更改,应该立刻通知到其他线程。
原子性:一个操作是不可分割的,不能执行到一半,就不执行了,要么同时成功,要么同时失败。
有序性:指令是有序的,不会被重排。
volatile关键字
volatile是一个轻量级的关键字,相对于synchronized来说
提供了同步机制
能保证可见性,和有序性(禁止指令重排序),不保证原子性。
注意:如下两个方法会产生同步
/**
* Prints a String and then terminate the line. This method behaves as
* though it invokes <code>{@link #print(String)}</code> and then
* <code>{@link #println()}</code>.
* System.out.println底层会使用synchronized进行同步
* @param x The <code>String</code> to be printed.
*/
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
* Performs a {@link Thread#sleep(long, int) Thread.sleep} using
* this time unit.
* This is a convenience method that converts time arguments into the
* form required by the {@code Thread.sleep} method.
*
* @param timeout the minimum time to sleep. If less than
* or equal to zero, do not sleep at all.
* @throws InterruptedException if interrupted while sleeping
* TimeUnit.SECONDS.sleep()底层使用了Thread.sleep方法,也实现了同步
*/
public void sleep(long timeout) throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
Thread.sleep(ms, ns);
}
}
volatile无法保证原子性
如在多线程里执行i++
这是由于i++是非原子性操作,在线程1改变了i的值时,更新了主内存里的i值,此时线程2里也已经完成了i++操作,但还没有返回给主内存,由于volatile的同步,使得i变为了1,丢失了加操作。
对class文件执行javap -c
可见
//i++操作,被拆分成了多步
Code:
0: aload_0
1: dup
2: getfield #2 //读
// Field number:I
5: iconst_1 //++常量1
6: iadd //加操作
7: putfield #2 //写操作
// Field number:I
10: return
解决方法
1、在不能保证原子性的方法上加上synchronized关键字
//等待所有线程都执行完毕可以使用此代码,当剩余两个线程时退出循环
//此时剩余主线程和gc线程
while(Thread.activeCount()>2){
Thread.yield();
}
2、使用java.util.concurrent.AtomicInteger类替代Integer 这是int类型的原子引用
AtomicInteger atomicInteger = new AtomicInteger();//默认值为0,不需要使用volatile关键字
使用AtomicInteger的getAndIncrement()方法替代i++。
有序性
内存屏障,又称内存栅栏,是一个cpu指令,volatile底层就是用的cpu的内存屏障指令来是实现的,有两个作用
1、保证特定操作的顺序性
2、保证变量的可见性
对Volatile变量进行写操作时:会在写操作后加入store屏障指令,工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时:会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
StoreStore屏障和StoreLoad屏障,通过插入屏障禁止在其前后的指令进行重排序优化
使用Volatile的地方
DCL单例模式双重检查锁
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造方法执行了");
}
public static SingletonDemo getInstance(){
if (instance == null) {
synchronized (SingletonDemo.class){
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
进行new操作时有三步 不使用volatile时2,3步会调换,其引用的对象可能未完成初始化,但是此变量已经不为null了
1、先开辟内存空间
2、在初始化对象
3、最后将变量指向分配的内存地址
17: new #12 // class thread/SingletonDemo
20: dup
21: invokespecial #13 // Method "<init>":()V
24: putstatic #11// Field instance:Lt
CAS
全称:Compare-And-Swap 比较并交换。
它的功能是判断主内存某个位置的值是否为跟期望值一样,相同就进行修改,否则一直重试,直到一致为止。这个过程是原子的。
AtomicInteger 的compareAndSet方法
AtomicInteger atomicInteger = new AtomicInteger(5);
//参数一:期待主内存中的数据值
//参数二:更新后的主内存中的数据
atomicInteger.compareAndSet(5,10);
//获取主内存的值
atomicInteger.get()
compareAndSet源码:
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
* 执行了CAS
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSwapInt方法源码:
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
* 执行了unsafe类里的此方法
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
AtomicInteger 的getAndIncrement方法
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.getAndIncrement();
addAtomic方法源码
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
getAndAddInt方法源码
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object <code>o</code>
* at the given <code>offset</code>.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
compareAndSwapInt方法源码:
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
* 依然执行了此方法
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
没有synchronized也保证了同步,
CAS并发原语言体现在JAVA语言中就是sum.misc.Unsafe类中的方法。
JVM会帮我实现出CAS汇编指令。是一种完全依赖于硬件的功能,实现了原子操作。
原语:属于操作系统用于范畴,由若干条指令组成的,用于完成某一个功能的一个过程,并且原语的执行是连续的,在执行过程中不允许被中断,CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
AtomicInteger成员变量
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
//偏移量,用于寻找物理地址,Unsave是根据内存偏移地址获取数据的。(Java里变量的地址值并不是真实地址,而是hash值,需要偏移量提供给C/C++真实的地址值)
private static final long valueOffset;
//物理地址上真正的值,保证了多线程的内存可见性。
private volatile int value;
Java语言是无法操作内存的,而Unsafe类是预留用于操作内存地址的后门,native方法由C/C++实现。
CAS实际是一种自旋锁
CAS的缺点:
1、一直循环,开销比较大。getAndAddInt方法中有do while,如果失败,会一直进行尝试。如果CAS一直不成功,会对CPU带来很大开销
2、对一个共享变量执行操作时,可以使用循环CAS的方式保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作原子性,这个时候就可以用锁来保证原子性
3、ABA问题
ABA问题
在进行CAS时,期望值可能没变,但是其已经经过了一系列的变化,只是最终又变回了原值。
AtomicReference原子引用
非基本数据类型的原子引用可以使用AtomicReference,其无法解决ABA问题。
ABA问题解决
AtomicStampedReference类似于时间戳
维护了一个版本号Stamp,在进行CAS操作时,不仅要比较当前值,还要比较版本号。两者都相等的时候才执行更新操作。
/**
* Atomically sets the value of both the reference and stamp
* to the given update values if the
* current reference is {@code ==} to the expected reference
* and the current stamp is equal to the expected stamp.
*
* @param expectedReference the expected value of the reference
* @param newReference the new value for the reference
* @param expectedStamp the expected value of the stamp
* @param newStamp the new value for the stamp
* @return {@code true} if successful
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//判断主内存的值是否期待的值一样
expectedReference == current.reference &&
//判断当前主内存的版本号和期待的版本号一致
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
集合类不安全问题
解决ConcurrentModificationExcption
ArrayList的线程安全类CopyOnWriteArrayList
CopyOnWriteArrayList的add方法源码
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
* CopyOnWrite写时复制的容器,实现读写分离
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取数组
Object[] elements = getArray();
int len = elements.length;
//拷贝,并将长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将新数组最后一位更新为新元素
newElements[len] = e;
//设置新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
HashSet的线程安全类CopyOnWriteArraySet
Map的线程安全类ConcurrentHashMap,比HashTable轻量级
区别:HashMap和ConcurrentHashMap
Java锁
Synchronized是由JVM控制的隐式的锁(非公平锁)
当synchronized关键字修饰的是普通方法,则当前锁对象为当前调用的对象。
static,synchronized同时修饰方法时,则锁对象为当前对象所在类的类名.class。
手动加锁
Lock lock = new ReetrantLock();
ReetrantLock构造方法源码
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
* 默认是非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
* 传入true是公平锁,否则为非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
非公平锁:多个线程抢夺锁,会导致优先级反转或饥饿现象
公平锁:多个线程按照申请锁的顺序来获得锁,先到先得
区别:
公平锁在获取锁是先查看此锁维护的等待队列,为空或者当前线程是等待队列的队首,则直接占有锁,否则插入到等待队列,采用FIFO原则。
非公平锁直接使用先尝试占有锁,失败则采用公平锁方式。
非公平锁的吞吐量比公平锁更大。
可重入锁
又叫递归锁,值得是同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。
ReetrantLock就是可重入锁,不会出现死锁。
锁的配对:加了几把锁,就得解开几把锁。
自旋锁(SpinLock)
尝试获取锁的线程不会立刻阻塞,而是采用循环的方式,去尝试获取。一致循环获取,就像自旋一样。好处是减少线程切换的上下文开销,没有类似wait的阻塞,缺点是会消耗CPU。
CAS的底层getAndAddInt就是自旋锁的思想。
public class SpinLockDemo2 {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
while(!atomicReference.compareAndSet(null,thread)){};
System.out.println(Thread.currentThread().getName()+"加了锁");
}
public void unlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"解了锁");
}
public static void main(String[] args) {
SpinLockDemo2 spinLockDemo2 = new SpinLockDemo2();
Thread thread1 = new Thread(()->{
spinLockDemo2.lock();
try{TimeUnit.SECONDS.sleep(3);}catch (InterruptedException ex){ex.printStackTrace();}
spinLockDemo2.unlock();
},"T1");
Thread thread2 = new Thread(()->{
spinLockDemo2.lock();
try{TimeUnit.SECONDS.sleep(3);}catch (InterruptedException ex){ex.printStackTrace();}
spinLockDemo2.unlock();
},"T2");
thread1.start();
thread2.start();
}
}
读写锁/独占锁/共享锁
读锁是共享的,写锁是独占的。juc.ReentrantLock和synchronized都是独占锁,独占锁就是一个锁只能被一个线程锁持有。需要读写分离时,需要引入读写锁,juc.ReetrantReadWriteLock
读锁的共享锁课保证并发读是非常高效的,读写、写读、写写的过程是互斥的。
缓存需要读写锁控制,读的get方法使用了ReentrantReadWriteLock.ReadLock(),写的put方法使用了ReentrantReadWriteLocke.WriteLock()。避免了写被打断,实现了多个线程同时读。
常用辅助类
每个锁里都有一个成员变量Sync类。 继承了AbstractQueuedSynchronizer抽象类,即抽象队列同步器AQS。
1、CountDownLatch
倒计时门闩,维护了一个计数器,只有计数器==0时,某些线程才会停止阻塞,开始执行。
让一些线程阻塞直到另外一些线程完成操作后才被唤醒(班长关门)
主要有两个方法,await方法会被阻塞,countDown会让计数器-1,不会阻塞,将计数器变为0时,调用await方法的线程会被唤醒,继续执行。
使用方式:
//参数为线程数,这里指三个线程
CountDownLatch countDownLatch = new CountDownLatch(3);
//线程内调用此方法,计数-1
countDownLatch.countDown();
//线程外等待方法,计数为0则继续向下执行
countDownLatch.await();
2、CyclicBarrier
篱栅,是可循环使用的屏障,当一组线程得到了一个屏障(同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续工作。进入屏障通过awati方法。
使用方法:
//第一个参数是需要的数量,第二个参数是个runnable,凑齐后要执行的方法。
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
//凑齐后执行的方法
});
//线程进行await()等待,进行+1操作,当到指定数时则执行CyclicBarrier里的方法。
new Thread(()->{
cyclicBarrier.await();
}.start();
3、Semaphore
信号灯,信号量主要用于两个目的,一个是多个资源的互斥使用,一个是并发线程数的控制。
使用方式:
//模拟资源类,有3个资源
Semaphore semaphore = new Semaphore(3);
new Thread(()->{
try{
//占用资源,计数+1,当达到资源类上限则不允许占用
semaphore.acquire();
}catch(Exception e){
}finally{
//释放资源,计数-1
semaphore.release();
}
)
阻塞队列
阻塞指在某些情况下会挂起线程(即阻塞),一旦满足条件,被挂起的线程又会被自动唤醒。
阻塞队列是一个队列。
当队列是空的,从队列中获取(Take)元素的操作会被阻塞,直到其他线程往空的队列中插入新的元素。
当队列是满的,从队列中添加(Put)元素的操作会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列空闲起来后并后续新增。
好处:不用手动控制什么时候被阻塞,什么时候被唤醒,简化了操作。
Collection->Queue->BlockingQueue->七个阻塞队列实现类
LinkedBockingDeque //由数组结构构成的有界阻塞队列
ArrayBlockingQueue //由链表结构构成的有界阻塞队列(默认值为Integer.MAX_VALUE 约21亿)
LinkedTransferQueue
SynchronousQueue //不存储元素的阻塞队列(单个元素的队列)
PriorityBlockingQueue
LinekdBolckingQueue
DelayQueue
BlockingQueue接口方法:
应用
生产者消费者
//通知所有线程,Condition类
condition.signalAll()
//线程等待
condition.await();
虚假唤醒
有多个生产者时,用if条件判断,多个线程都在等待,当唤醒时,由于没有再次判断,所以都执行了。如果用while判断则没有此问题。
精准通知顺序访问
ReentrantLock.Condition
阻塞队列模式生产者消费者
不用关心什么时候需要阻塞线程,什么时候需要唤醒线程。