【Java核心知识】JUC包相关知识

文章目录

  • JUC包主要内容
  • Java内置锁
    • 为什么会有线程安全问题
    • Synchronize锁
    • Java对象结构
    • Synchronize锁优化
    • 线程间通信
    • Synchronize与wait原理
  • CAS和JUC原子类
    • CAS原理
    • `JUC`原子类
    • `ABA`问题
  • 可见性和有序性
    • 为什么会有可见性
    • 参考链接
  • 显式锁
    • Lock接口常用方法
    • 显式锁分类
    • 显式锁实现原理
    • 参考链接

JUC包主要内容

JUC包是与并发编程相关的包,主要包含四部分原子类并发集合多线程,如下图所示。

其中,

  • 锁可以分为内置锁和显式锁;
  • 原子类主要是一些通过CAS实现的原子类;
  • 并发集合主要就是一些线程安全的集合,比如ConcurrentHashMap,BlockQueue等;
  • 多线程包括callable接口和线程池等;

【Java核心知识】JUC包相关知识_第1张图片

Java内置锁

为什么会有线程安全问题

i++线程不安全的原因在于自增操作不是原子性的,可以分为三步:内存取值寄存器加1存值到内存

除了原子性之外,可见性有序性也会导致线程安全问题。可见性是指线程B并不一定能够及时看到线程A对变量的修改。

Synchronize锁

Synchronize关键字可以作用在方法上,也可以作用于代码块上,本质上都是锁住了某个对象,但synchronize作用于方法上是一种粗粒度的锁,会导致其他线程也不能访问该对象的其他方法。

JVM的堆中,每个对象都会有一个对象监视器,synchronize就是锁住了这个对象监视器,从而保证了原子性。

那么如何保证可见性呢?线程加锁时,必须清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取;线程在解锁时,需要把工作内存中最新的共享变量的值写入到主存,以此来保证共享变量的可见性。(这里是个泛指,不是说只有在退出synchronized时才同步变量到主存)

Java对象结构

Java的对象都放在JVM的堆中,每个对象的结构包括:

  • 对象头:

    • Mark Word:记录哈希码,GC标志位、锁状态等信息。不同锁状态下Mark Word是不同的,但最后两位都代表了锁状态。

    • 类对象指针:指向方法区的该类相关信息

    • 数组长度:如果对象是数组才有此结构

  • 对象体:包含对象的实例变量,包含父类的实例变量

  • 对齐字节:为了保证8字节的对齐而填充的数据

Synchronize锁优化

为了优化Synchronize锁的性能,Java提出了逐步升级的四种锁:无锁->偏向锁->轻量级锁->重量级锁。

  • 无锁:
  • 偏向锁:Mark Word中存储持有锁的线程ID,当有线程执行时,先判断对象头的线程ID是否与此线程ID相等,如果相等,直接向下执行;如果不相等,说明存在竞争,锁升级为轻量级锁。
  • 轻量级锁:对象头存储持有锁的线程ID,将对象头原来的哈希码放入线程栈帧中的锁记录中。当别的线程竞争锁时,不会立即阻塞,切换用户态,而是会自旋,然后使用CAS尝试获取锁,降低了阻塞线程的消耗。自旋等待时间和上一个竞争线程等待结果有关:如果上一个竞争线程自旋成功了,那么这次自旋的次数会更多;如果上一个竞争线程自旋失败了,那么这次自旋的次数会减少。自旋不会一直持续下去,如果超过了指定时间,会膨胀为重量级锁!
  • 重量级锁:重量级锁对象头会指向一个监视器对象(每个对象都有一个监视器对象),该监视器通过三个队列(竞争队列、阻塞队列、等待时间片的就绪队列)来登记和管理排队的线程,会涉及到线程的阻塞,切换用户态。

轻量级锁执行过程:

  • 1、判断对象是否加锁,如果没加锁,进行以下操作

  • 2、在自己的栈帧中创建锁记录,用来存放加锁对象的哈希码

  • 3、创建好锁记录后,通过CAS自旋操作,尝试将锁对象头的锁记录指针替换成栈帧中锁记录的地址

  • 4、替换栈帧后会返回锁对象的哈希码,然后填入栈帧的锁记录中

线程间通信

可以使用Objectwait(),notify()方法来进行线程间的通信。

wait()方法的原理

1)当线程调用了lock(某个同步锁对象)的wait()方法后,JVM会将当前线程加入lock监视器的WaitSet(等待集),等待被其他线程唤醒。
2)当前线程会释放lock对象监视器的Owner权利,让其他线程可以抢夺lock对象的监视器。
3)让当前线程等待,其状态变成WAITING

notify()方法的原理

1)当线程调用了lock(某个同步锁对象)的notify()方法后,JVM会唤醒lock监视器WaitSet中的第一个等待线程。
2)当线程调用了locknotifyAll()方法后,JVM会唤醒lock监视器WaitSet中的所有等待线程。
3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED
4)EntryList中的线程抢夺到监视器Owner权利之后,线程的状态从BLOCKED变成Runnable,具备重新执行的资格。

缓冲队列

/**
 * 生产者消费者队列
 */
//数据缓冲区,类定义
public class DataBuffer<T> {
    public static final int MAX_AMOUNT = 10; //数据缓冲区最大长度
    //保存数据
    private List<T> dataList = new LinkedList<>();
    //数据缓冲区长度
    private Integer amount = 0;
    // 用来保证只有一个线程存元素或者取元素
    private final Object LOCK_OBJECT = new Object();
    // 当队列满了后,用于阻塞生产者
    private final Object NOT_FULL = new Object();
    // 当队列为空时,用于阻塞消费者
    private final Object NOT_EMPTY = new Object();
    // 向数据区增加一个元素
    public void add(T element) throws Exception
    {
        // 队列已满,不能存元素
        while (amount > MAX_AMOUNT)
        {
            synchronized (NOT_FULL)
            {
                System.out.println("队列已经满了!");
                // 等待未满通知,这里为什么需要wait,是因为需要等待一个条件满足,而不能只用synchronize,某一时刻只有一个线程拥有NOT_FULL是不行的
                NOT_FULL.wait();
            }
        }
        // 保证原子性
        synchronized (LOCK_OBJECT)
        {
            dataList.add(element);
            amount++;
            System.out.println(Thread.currentThread().getName() + "生产了一条消息" + amount);
        }
        synchronized (NOT_EMPTY)
        {
            //发送未空通知
            NOT_EMPTY.notify();
        }
    }
    /**
     * 从数据区取出一个商品
     */
    public T fetch() throws Exception
    {
        // 数量为零,不能取元素
        while (amount <= 0)
        {
            synchronized (NOT_EMPTY)
            {
                System.out.println(Thread.currentThread().getName() + "队列已经空了!");
                //等待未空通知
                NOT_EMPTY.wait();
            }
        }
        T element = null;
        // 保证原子性
        synchronized (LOCK_OBJECT)
        {
            element = dataList.remove(0);
            amount--;
            System.out.println(Thread.currentThread().getName() + "消费了一条消息" + amount);
        }
        synchronized (NOT_FULL)
        {
            //发送未满通知
            NOT_FULL.notify();
        }
        return element;
    }
}

生产者和消费者

@Test
public void testProducerConsumerQueue() throws InterruptedException {
    //共享数据区,实例对象
    DataBuffer<String> dataBuffer = new DataBuffer<>();
    // 同时并发执行的线程数
    final int THREAD_TOTAL = 20;
    //线程池,用于多线程模拟测试
    ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_TOTAL);
    //假定共11条线程,其中有10个消费者,但是只有1个生产者
    final int CONSUMER_TOTAL = 10;
    final int PRODUCE_TOTAL = 1;
    for (int i = 0; i < PRODUCE_TOTAL; i++) {
        //生产者线程每生产一个商品,间隔50毫秒
        threadPool.submit(() -> {
            for (int j = 0; j < 10; j ++) {
                //首先生成一个随机的商品
                String s = "商品";
                //将商品加上共享数据区
                try {
                    dataBuffer.add(s);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
    for (int i = 0; i < CONSUMER_TOTAL; i++)
    {
        //消费者线程每消费一个商品,间隔100毫秒
        threadPool.submit(() -> {
            for (int j = 0; j < 2; j ++) {
                // 从PetStore获取商品
                String s = null;
                try {
                    s = dataBuffer.fetch();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
    Thread.sleep(10000);
}

Synchronize与wait原理

Synchronizewait都会将线程加入到等待队列中,但是两者加入的等待队列并不是同一个,Synchronize加入的是对象监视器的等待队列,当退出Synchronize代码块后会自动唤醒线程,而waitObject的方法,加入的是另一个等待集合,只能通过notify()notifyAll()唤醒。

CAS和JUC原子类

CAS原理

CAS(Compare And Swap),是比较交换的缩写,可以用来实现乐观锁。乐观锁本质上是无锁的,每次更新前都把原来的旧值和要更新的新值一块传入,如果发现传入的旧值和当前内存上的旧值一样,则更新成功;否则更新失败;

乐观锁就是一直调用CAS操作,不断获取旧值,计算新值,然后传入旧值和新值进行更新,线程一直在自旋,直到更新成功为止。

示例

public class CompareAndSwap {
    public volatile int value; //值
    //不安全类
    // private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final Unsafe unsafe = getUnsafe();
    //value 的内存偏移(相对与对象头部的偏移,不是绝对偏移)
    private static final long valueOffset;
    //统计失败的次数
    public static final AtomicLong failure = new AtomicLong(0);
    static
    {
        try
        {
            //取得value属性的内存偏移
            valueOffset = unsafe.objectFieldOffset(CompareAndSwap.class.getDeclaredField("value"));
            System.out.println("valueOffset:=" + valueOffset);
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }
    //通过CAS原子操作,进行“比较并交换”
    public final boolean unSafeCompareAndSet(int oldValue, int newValue)
    {
        //原子操作:使用unsafe的“比较并交换”方法进行value属性的交换
        return unsafe.compareAndSwapInt( this, valueOffset, oldValue, newValue );
    }
    //使用无锁编程实现安全的自增方法
    public void selfPlus()
    {
        int oldValue = value;
        //通过CAS原子操作,如果操作失败就自旋,直到操作成功
        for(;;) {
            oldValue = value;
            failure.incrementAndGet();
            if (unSafeCompareAndSet(oldValue, oldValue + 1)) return;
        }
        // do
        // {
        //     // 获取旧值
        //     oldValue = value;
        //     //统计无效的自旋次数
        //     //记录失败的次数
        //     failure.incrementAndGet();
        // } while (!unSafeCompareAndSet(oldValue, oldValue + 1));
    }

    /**
     * 通过反射获取Unsafe
     * @return
     */
    public static Unsafe getUnsafe()
    {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}
/**
 * 测试CAS操作
 * @throws InterruptedException
 */
@Test
public void testCAS() throws InterruptedException {
    final CompareAndSwap compareAndSwap = new CompareAndSwap();
    AtomicInteger res = new AtomicInteger(0);
    //倒数闩,需要倒数THREAD_COUNT次
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++)
    {
        // 提交10个任务
            Executors.newCachedThreadPool().submit(() ->
            {
                //每个任务累加1000次
                for (int j = 0; j < 1000; j++)
                {
                    compareAndSwap.selfPlus();
                    res.getAndIncrement();
                }
                latch.countDown();// 执行完一个任务,倒数闩减少一次
            });
    }
    latch.await();// 主线程等待倒数闩倒数完毕
    System.out.println(res);
    System.out.println("累加之和:" + compareAndSwap.value);
    System.out.println("失败次数:" + CompareAndSwap.failure.get());
}

JUC原子类

JUC包下的原子类可以分为四组:

  • 基本原子类:AtomicInteger,整型;AtomicLong,大整数;AtomicBoolean:布尔型;
  • 数组原子类:AtomicIntegerArray:整型数组原子类;AtomicLongArray:长整型数组原子类;AtomicReferenceArray:引用类型数组原子类。
  • 引用原子类:AtomicReference:引用类型原子类;AtomicMarkableReference:带有更新标记位的原子引用类型;AtomicStampedReference:带有更新版本号的原子引用类型。
  • 字段更新原子类:AtomicIntegerFieldUpdater:原子更新整型字段的更新器;AtomicLongFieldUpdater:原子更新长整型字段的更新器;AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

JUC原子类下的底层实现也是通过不断CAS自旋+volatile(实现可见性)实现的,可以从源码看到。

ABA问题

使用CAS自旋更新虽然没有加锁,降低了线程切换成本,但是容易产生ABA问题。即线程1将值从A到B又到A,此时线程2被唤醒,以为变量没有改变过,从而引起错误的判断。解决办法是添加时间戳,可以借助AtomicStampedReference原子类实现。

可见性和有序性

为什么会有可见性

现代处理器都是多核的,每个核都会有自己独有的高速缓存L1,L2,L3,这些核又共享一个主内存,每次涉及变量更新或读取时,CPU都是先从高级缓存中读取并进行修改,然后随机写入到主存。这样就产生了问题,如果核1对公有变量A进行了修改,但是还没来得及写入主存,那么核2从主存中读取到的值就是未及时更新的脏值。

一般操作系统会使用Lock指令在总线上进行广播,哪些变量的高速缓存已失效,必须从主存中重新读取。Java的volatile关键字会在字节码上加入loadloadloadstorestorestorestoreload内存屏障来保证更改后的变量立即写入主存,且告知其他核的高速缓存该值已失效,必须从主内存重新读取。

volatile并不保证原子性,因为虽然volatile会强制将修改刷回主存,但是修改并刷回主存的指令不是原子性的,可能有中断的可能。比如线程A修改完变量后,准备刷回主存,这时发生了线程调度,线程B知道自己的数据失效了,但是从主存中重新获取的数据不一定是最新的,因为线程A只是在本地修改了数据,但还没有写入主存。

参考链接

内存屏障与JVM指令

如果你知道这灵魂拷问的6连击,面试volitile时就稳了

显式锁

Lock接口常用方法

所有的锁实现类都会实现Lock接口,该接口主要有以下几个方法:

  • lock():阻塞获取锁,如果当前线程不能抢到锁,线程会加入阻塞队列进行等待,直到获取到锁;

  • tryLock(): 非阻塞抢锁,如果当前线程抢不到锁,线程会立刻返回false

  • tryLock(long time, TimeUnit unit): 超时返回,如果当前线程在一段时间内抢不到锁,则会返回false

  • unlock(): 释放锁;

下面是一个使用ReentrantLock的示例,使用三个线程同时对某一个执行加一操作,每个线程操作100次,累计300次。

public class LockTest {
    private int count;

    @Test
    public void testReentrantLock() throws InterruptedException {
        Lock lock = new ReentrantLock();

        ExecutorService executorService = Executors.newCachedThreadPool();

        CountDownLatch countDownLatch = new CountDownLatch(3);

        for (int i = 0; i < 3; i ++) {
            executorService.execute(() -> {
                for (int j = 0; j < 100; j ++) {
                    // 获取锁
                    lock.lock();
                    try {
                        count ++;
                    } finally {
                        // 释放锁
                        lock.unlock();
                    }
                }
                // 每完成一个线程,就更新countDownLatch
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();

        System.out.println(count); 
    }
}

显式锁分类

显式锁的分类有很多种,大致上可以分为下面这些:

【Java核心知识】JUC包相关知识_第2张图片

显式锁实现原理

JUC包下的显式锁都是基于AQS实现的,AQS使用一个队列保存想要获取锁的线程,同时在队头使用CAS竞争获取锁,不会阻塞线程,是一种乐观锁

当有新线程加入时,会通过CAS加入队尾,然后监控队列前一个元素的状态,这时不会发生CAS,但线程也不会阻塞,而是会调用yield()主动让出时间片。

参考链接

Java中常见的各种锁

你可能感兴趣的:(#,Java核心知识,java,开发语言)