JAVA并发(JUC)

前言

相关包

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

阻塞队列模式生产者消费者
不用关心什么时候需要阻塞线程,什么时候需要唤醒线程。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值