理想论坛_专业20年的财经股票炒股论坛交流社区 - 股票论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 2922|回复: 0

Java 并发核心机制

[复制链接]

9650

主题

9650

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
28966
发表于 2019-12-27 14:52 | 显示全部楼层 |阅读模式
本文以及示例源码已归档在 javacore
一、J.U.C 简介

Java 的 java.util.concurrent 包(简称 J.U.C)中供给了大量并发工具类,是 Java 并发本事的重要表现(留意,不是全数,有部分并发本事的支持在其他包中)。从功用上,大略可以分为:

  • 原子类 - 如:AtomicInteger、AtomicIntegerArray、AtomicReference、AtomicStampedReference 等。
  • 锁 - 如:ReentrantLock、ReentrantReadWriteLock 等。
  • 并发容器 - 如:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet 等。
  • 阻塞行列 - 如:ArrayBlockingQueue、LinkedBlockingQueue 等。
  • 非阻塞行列 - 如: ConcurrentLinkedQueue 、LinkedTransferQueue 等。
  • Executor 框架(线程池)- 如:ThreadPoolExecutor、Executors 等。
我小我大白,Java 并发框架可以分为以下条理。

由 Java 并发框架图不丢脸出,J.U.C 包中的工具类是基于 synchronized、volatile、CAS、ThreadLocal 这样的并发焦点机制打造的。所以,要想深入大白 J.U.C 工具类的特征、为什么具有这样那样的特征,就必须先大白这些焦点机制。
二、synchronized

synchronized 是 Java 中的关键字,是 操纵锁的机制来实现互斥同步的
synchronized 可以保证在同一个时辰,只要一个线程可以实行某个方式大要某个代码块
假如不必要 Lock 、ReadWriteLock 所供给的高级同步特征,应当优先考虑操纵 synchronized ,出处以下:

  • Java 1.6 今后,synchronized 做了大量的优化,其性能已经与 Lock 、ReadWriteLock 底子上持平。从趋历来看,Java 未来仍将继续优化 synchronized ,而不是 ReentrantLock 。
  • ReentrantLock 是 Oracle JDK 的 API,在其他版本的 JDK 中不愿定支持;而 synchronized 是 JVM 的内置特征,全数 JDK 版本都供给支持。
synchronized 的用法

synchronized 有 3 种利用方式:

  • 同步实例方式 - 对于普通同步方式,锁是当前实例工具
  • 同步静态方式 - 对于静态同步方式,锁是当前类的 Class 工具
  • 同步代码块 - 对于同步方式块,锁是 synchonized 括号里设备的工具
说明:
类似 Vector、Hashtable 这类同步类,就是操纵 synchonized 修饰其垂危方式,来保证其线程平安。
究竟上,这类同步容器也非绝对的线程平安,当实行迭代器遍历,按照条件删除元素这类场景下,就大要出现线程不服安的情况。此外,Java 1.6 针对 synchonized 举行优化前,由于阻塞,其性能不高。
综上,这类同步容器,在今世 Java 步伐中,已经渐渐不用了。
同步实例方式

毛病示例 - 未同步的示例
  1. public class NoSynchronizedDemo implements Runnable {    public static final int MAX = 100000;    private static int count = 0;    public static void main(String[] args) throws InterruptedException {        NoSynchronizedDemo instance = new NoSynchronizedDemo();        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count);    }    @Override    public void run() {        for (int i = 0; i < MAX; i++) {            increase();        }    }    public void increase() {        count++;    }}// 输出成果: 小于 200000 的随机数字
复制代码
Java 实例方式同步是同步在具有该方式的工具上。这样,每个实例其方式同步都同步在差此外工具上,即该方式所属的实例。只要一个线程可以大要在实例方式同步块中运转。倘使有多个实例存在,那末一个线程一次可以在一个实例同步块中实行操纵。一个实例一个线程。
  1. public class SynchronizedDemo implements Runnable {    private static final int MAX = 100000;    private static int count = 0;    public static void main(String[] args) throws InterruptedException {        SynchronizedDemo instance = new SynchronizedDemo();        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count);    }    @Override    public void run() {        for (int i = 0; i < MAX; i++) {            increase();        }    }    /**     * synchronized 修饰普通方式     */    public synchronized void increase() {        count++;    }}
复制代码
同步静态方式

静态方式的同步是指同步在该方式地点的类工具上。由于在 JVM 中一个类只能对应一个类工具,所以同时只答应一个线程实行同一个类中的静态同步方式。
对于不同类中的静态同步方式,一个线程可以实行每个类中的静态同步方式而无需等待。不管类中的那个静态同步方式被挪用,一个类只能由一个线程同时实行。
  1. public class SynchronizedDemo2 implements Runnable {    private static final int MAX = 100000;    private static int count = 0;    public static void main(String[] args) throws InterruptedException {        SynchronizedDemo2 instance = new SynchronizedDemo2();        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count);    }    @Override    public void run() {        for (int i = 0; i < MAX; i++) {            increase();        }    }    /**     * synchronized 修饰静态方式     */    public synchronized static void increase() {        count++;    }}
复制代码
同步代码块

偶然你不必要同步全部方式,而是同步方式中的一部分。Java 可以对方式的一部分举行同步。
留意 Java 同步块机关器用括号将工具括起来。在上例中,操纵了 this,即为挪用 add 方式的实例自己。在同步机关器中用括号括起来的工具叫做监视器工具。上述代码操纵监视器工具同步,同步实例方式操纵挪用方式自己的实例作为监视器工具。
一次只要一个线程可以大要在同步于同一个监视器工具的 Java 方式内实行。
  1. public class SynchronizedDemo3 implements Runnable {    private static final int MAX = 100000;    private static int count = 0;    public static void main(String[] args) throws InterruptedException {        SynchronizedDemo3 instance = new SynchronizedDemo3();        Thread t1 = new Thread(instance);        Thread t2 = new Thread(instance);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count);    }    @Override    public void run() {        for (int i = 0; i < MAX; i++) {            increase();        }    }    /**     * synchronized 修饰代码块     */    public static void increase() {        synchronized (SynchronizedDemo3.class) {            count++;        }    }}
复制代码
synchronized 的道理

synchronized 经过编译后,会在同步块的前后别离构成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码指令都必要一个援用典范的参数来指明要锁定息争锁的工具。假如 synchronized 大白拟订了工具参数,那就是这个工具的援用;假如没有大白指定,那就按照 synchronized 修饰的是实例方式还是静态方式,去对对应的工具实例或 Class 工具来作为锁工具。
synchronized 同步块对同一线程来说是可重入的,不会出现锁死题目。
synchronized 同步块是互斥的,即已进入的线程实行完成前,会阻塞其他试图进入的线程。
锁的机制

锁具有以下两种特征:

  • 互斥性:即在同一时候只答应一个线程持有某个工具锁,经过这类特征来实现多线程中的和谐机制,这样在同一时候只要一个线程对需同步的代码块(复合操纵)举行拜候。互斥性我们也凡是称为操纵的原子性。
  • 可见性:必须确保在锁被开释之前,对同享变量所做的点窜,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新同享变量的值),否则另一个线程大如果在当地缓存的某个副本上继续操纵从而引发不齐截。
锁典范


  • 工具锁 - 在 Java 中,每个工具城市有一个 monitor 工具,这个工具实在就是 Java 工具的锁,凡是会被称为“内置锁”或“工具锁”。类的工具可以有多个,所以每个工具有其自力的工具锁,互不干扰。
  • 类锁 - 在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁现实上是经过工具锁实现的,即类的 Class 工具锁。每个类只要一个 Class 工具,所以每个类只要一个类锁。
synchronized 的优化

Java 1.6 今后,synchronized 做了大量的优化,其性能已经与 Lock 、ReadWriteLock 底子上持平。
自旋锁

互斥同步进入阻塞状态的开销都很大,应当尽管束止。在很多利用中,同享数据的锁定状态只会连续很短的一段时候。自旋锁的脑筋是让一个线程在请求一个同享数据的锁时实行忙循环(自旋)一段时候,假如在这段时候内能获得锁,便可以制止进入阻塞状态。
自旋锁固然能制止进入阻塞状态从而淘汰开销,可是它必要举行忙循环操纵占用 CPU 时候,它只适用于同享数据的锁定状态很短的场景。
在 Java 1.6 中引入了自顺应的自旋锁。自顺应意味着自旋的次数不再牢固了,而是由前一次在同一个锁上的自旋次数及锁的具有者的状态来决议。
锁消除

锁消除是指对于被检测出不大要存在合作的同享数据的锁举行消除
锁消除重如果经过逃逸分析来支持,假如堆上的同享数据不大要逃逸进来被此外线程拜候到,那末便可以把它们当做私稀有据看待,也便可以将它们的锁举行消除。
对于一些看起来没有加锁的代码,实在隐式的加了很多锁。例以下面的字符串拼接代码就隐式加了锁:
  1. public static String concatString(String s1, String s2, String s3) {    return s1 + s2 + s3;}
复制代码
String 是一个不成变的类,编译器会对 String 的拼接自动优化。在 Java 1.5 之前,会转化为 StringBuffer 工具的连续 append() 操纵:
  1. public static String concatString(String s1, String s2, String s3) {    StringBuffr sb = new StringBuffer();    sb.append(s1);    sb.append(s2);    sb.append(s3);    return sb.toString();}
复制代码
每个 append() 方式中都有一个同步块。捏造机观察变量 sb,很快就会发现它的静态感化域被限制在 concatString() 方式内部。也就是说,sb 的全数援用永久不会逃逸到 concatString() 方式之外,其他线程没法拜候到它,是以可以举行消除。
锁粗化

假如一系列的连续操纵都对同一个工具频频加锁息争锁,频仍的加锁操纵就会致使性能消耗。
上一节的示例代码中连续的 append() 方式就属于这类情况。假如捏造机探测到由这样的一串零碎的操纵都对同一个工具加锁,将会把加锁的范围扩大(粗化)到全部操纵序列的内部。对于上一节的示例代码就是扩大到第一个 append() 操纵之前直至末端一个 append() 操纵以后,这样只必要加锁一次便可以了。
轻量级锁

Java 1.6 引入了偏向锁和轻量级锁,从而让锁具有了四个状态:

  • 无锁状态(unlocked)
  • 偏向锁状态(biasble)
  • 轻量级锁状态(lightweight locked)
  • 重量级锁状态(inflated)
轻量级锁是相对于传统的重量级锁而言,它 操纵 CAS 操纵来制止重量级锁操纵互斥量的开销。对于绝大部分的锁,在全部同步周期内都是不存在合作的,是以也就不必要都操纵互斥量举行同步,可以先采取 CAS 操纵举行同步,假如 CAS 失利了再改用互斥量举行同步。
当尝试获得一个锁工具时,假如锁工具标志为 0 01,说明锁工具的锁未锁定(unlocked)状态。此时捏造机在当前方程的捏造机栈中建立 Lock Record,然后操纵 CAS 操纵将工具的 Mark Word 更新为 Lock Record 指针。假如 CAS 操纵乐成了,那末线程就获得了该工具上的锁,而且工具的 Mark Word 的锁标志变成 00,表现该工具处于轻量级锁状态。
偏向锁

偏向锁的脑筋是偏向于让第一个获得锁工具的线程,这个线程在以后获得该锁就不再必要举行同步操纵,甚至连 CAS 操纵也不再必要
三、volatile

volatile 的要点

volatile 是轻量级的 synchronized,它在多处置惩罚器斥地中保证了同享变量的“可见性”。
可见性的意义是当一个线程点窜一个同享变量时,此外一个线程能读到这个点窜的值。
一旦一个同享变量(类的成员变量、类的静态成员变量)被 volatile 修饰以后,那末就具有了两层语义:

  • 保证了不同线程对这个变量举行操纵时的可见性,即一个线程点窜了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止举行指令重排序。
假如一个字段被声明成 volatile,Java 线程内存模子确保全数线程看到这个变量的值是齐截的。
volatile 的用法

假如 volatile 变量修饰符操纵得当的话,它比 synchronized 的操纵和实行本钱更低,由于它不会引发线程高低文的切换和调理。可是,volatile 没法替换 synchronized ,由于 volatile 没法保证操纵的原子性。
凡是来说,操纵 volatile 必须具有以下 2 个条件

  • 对变量的写操纵不依靠于当前值
  • 该变量没有包含在具有其他变量的安定式中
示例:状态标志量
  1. volatile boolean flag = false;while(!flag) {    doSomething();}public void setFlag() {    flag = true;}
复制代码
示例:两重锁实现线程平安的单例类
  1. class Singleton {    private volatile static Singleton instance = null;    private Singleton() {}    public static Singleton getInstance() {        if(instance==null) {            synchronized (Singleton.class) {                if(instance==null)                    instance = new Singleton();            }        }        return instance;    }}
复制代码
volatile 的道理

观察加入 volatile 关键字和没有加入 volatile 关键字时所天生的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令
lock 前缀指令现实上相当于一个内存屏障(也成内存栅栏),内存屏障会供给 3 个功用:

  • 它确保指令重排序时不会把厥后背的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后背;即在实行到内存屏障这句指令时,在它前面的操纵已经全数完成;
  • 它会逼迫将对缓存的点窜操纵立即写入主存;
  • 假如是写操纵,它会致使其他 CPU 中对应的缓存行无效。
四、CAS

CAS 的要点

互斥同步是最多见的并发切确性保障本事。
互斥同步最重要的题目是线程阻塞和叫醒所带来的性能题目,是以互斥同步也被称为阻塞同步。互斥同步属于一种灰心的并发计谋,总是以为只要不去做切确的同步步伐,那就必定会出现题目。不管同享数据能否真的会出现合作,它都要举行加锁(这里会商的是概念模子,现实上捏造机会优化掉很大一部分不必要的加锁)、用户态焦点态转换、保护锁计数器和检查能否有被阻塞的线程必要叫醒等操纵。
随着硬件指令集的成长,我们可以操纵基于辩说检测的悲观并发计谋:先辈行操纵,假如没有此外线程争用同享数据,那操纵就乐成了,否则采取补偿步伐(不停地重试,直到乐成为止)。这类悲观的并发计谋的很多实现都不必要将线程阻塞,是以这类同步操纵称为非阻塞同步。
为什么说悲观锁必要 硬件指令集的成长 才华举行?由于必要操纵和辩说检测这两个步伐具有原子性。而这点是由硬件来完成,假如再操纵互斥同步来保证就落空意义了。硬件支持的原子性操纵最典范的是:CAS。
CAS(Compare and Swap),字面意义为比力并交换。CAS 有 3 个操纵数,别离是:内存值 V,旧的预期值 A,要点窜的新值 B。当且仅当预期值 A 和内存值 V 类似时,将内存值 V 点窜成 B,否则什么都不做。
CAS 的道理

Java 是怎样实现 CAS ?
Java 重要操纵 Unsafe 这个类供给的 CAS 操纵。
Unsafe 的 CAS 依靠的是 JV M 针对差此外操纵系统实现的 Atomic::cmpxchg 指令。
Atomic::cmpxchg 的实现操纵了汇编的 CAS 操纵,并操纵 CPU 供给的 lock 信号保证其原子性。
CAS 的利用

原子类

原子类是 CAS 在 Java 中最典范的利用。
我们先来看一个常见的代码片断。
  1. if(a==b) {    a++;}
复制代码
假如 a++ 实行前, a 的值被点窜了怎样办?还能获得预期值吗?出现该题目标原因原由是在并发情况下,以上代码片断不是原子操纵,随时大要被其他线程所篡改。
打点这类题目标最典范方式是利用原子类的 incrementAndGet 方式。
  1. public class AtomicIntegerDemo {    public static void main(String[] args) throws InterruptedException {        ExecutorService executorService = Executors.newFixedThreadPool(3);        final AtomicInteger count = new AtomicInteger(0);        for (int i = 0; i < 10; i++) {            executorService.execute(new Runnable() {                @Override                public void run() {                    count.incrementAndGet();                }            });        }        executorService.shutdown();        executorService.awaitTermination(3, TimeUnit.SECONDS);        System.out.println("Final Count is : " + count.get());    }}
复制代码
J.U.C 包中供给了 AtomicBoolean、AtomicInteger、AtomicLong 别离针对 Boolean、Integer、Long 实行原子操纵,操纵和上面的示例大要类似,不做赘述。
自旋锁

操纵原子类(本质上是 CAS),可以实现自旋锁。
所谓自旋锁,是指线程频频检查锁变量能否可用,直到乐成为止。由于线程在这一进程中连结实行,是以是一种忙等待。一旦获得了自旋锁,线程会不停连结该锁,直至显式开释自旋锁。
示例:非线程平安示例
  1. public class AtomicReferenceDemo {    private static int ticket = 10;    public static void main(String[] args) {        ExecutorService executorService = Executors.newFixedThreadPool(3);        for (int i = 0; i < 5; i++) {            executorService.execute(new MyThread());        }        executorService.shutdown();    }    static class MyThread implements Runnable {        @Override        public void run() {            while (ticket > 0) {                System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票");                ticket--;            }        }    }}
复制代码
输出成果:
  1. pool-1-thread-2 卖出了第 10 张票pool-1-thread-1 卖出了第 10 张票pool-1-thread-3 卖出了第 10 张票pool-1-thread-1 卖出了第 8 张票pool-1-thread-2 卖出了第 9 张票pool-1-thread-1 卖出了第 6 张票pool-1-thread-3 卖出了第 7 张票pool-1-thread-1 卖出了第 4 张票pool-1-thread-2 卖出了第 5 张票pool-1-thread-1 卖出了第 2 张票pool-1-thread-3 卖出了第 3 张票pool-1-thread-2 卖出了第 1 张票
复制代码
很明显,出现了反复售票的情况。
示例:操纵自旋锁来保证线程平安
可以经过自旋锁这类非阻塞同步来保证线程平安,下面操纵 AtomicReference 来实现一个自旋锁。
  1. public class AtomicReferenceDemo2 {    private static int ticket = 10;    public static void main(String[] args) {        threadSafeDemo();    }    private static void threadSafeDemo() {        SpinLock lock = new SpinLock();        ExecutorService executorService = Executors.newFixedThreadPool(3);        for (int i = 0; i < 5; i++) {            executorService.execute(new MyThread(lock));        }        executorService.shutdown();    }    static class SpinLock {        private AtomicReference atomicReference = new AtomicReference();        public void lock() {            Thread current = Thread.currentThread();            while (!atomicReference.compareAndSet(null, current)) {}        }        public void unlock() {            Thread current = Thread.currentThread();            atomicReference.compareAndSet(current, null);        }    }    static class MyThread implements Runnable {        private SpinLock lock;        public MyThread(SpinLock lock) {            this.lock = lock;        }        @Override        public void run() {            while (ticket > 0) {                lock.lock();                if (ticket > 0) {                    System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票");                    ticket--;                }                lock.unlock();            }        }    }}
复制代码
输出成果:
  1. pool-1-thread-2 卖出了第 10 张票pool-1-thread-1 卖出了第 9 张票pool-1-thread-3 卖出了第 8 张票pool-1-thread-2 卖出了第 7 张票pool-1-thread-3 卖出了第 6 张票pool-1-thread-1 卖出了第 5 张票pool-1-thread-2 卖出了第 4 张票pool-1-thread-1 卖出了第 3 张票pool-1-thread-3 卖出了第 2 张票pool-1-thread-1 卖出了第 1 张票
复制代码
CAS 的题目

一样平常情况下,CAS 比锁性能更高。由于 CAS 是一种非阻塞算法,所以其制止了线程阻塞和叫醒的等待时候。
可是,CAS 也有一些题目。
ABA 题目

假如一个变量初度读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操纵就会误以为它历来没有被改变过。
J.U.C 包供给了一个带有标志的原子援用类 AtomicStampedReference 来打点这个题目,它可以经过控制变量值的版本来保证 CAS 的切确性。大部分情况下 ABA 题目不会影响步伐并发的切确性,假如必要打点 ABA 题目,改用传统的互斥同步大要会比原子类更高效。
循环时候长开销大

自旋 CAS (不停尝试,直到乐成为止)假如长时候不乐成,会给 CPU 带来很是大的实行开销。
假如 JVM 能支持处置惩罚器供给的 pause 指令那末服从会有必定的提拔,pause 指令有两个感化:

  • 它可以迟误流水线实行指令(de-pipeline),使 CPU 不会消耗过量的实行资本,迟误的时候取决于具体实现的版本,在一些处置惩罚器上迟误时候是零。
  • 它可以制止在退出循环的时候因内存次第辩说(memory order violation)而引发 CPU 流水线被清空(CPU pipeline flush),从而进步 CPU 的实行服从。
比力花费 CPU 资本,即使没有任何用也会做一些无勤劳。
只能保证一个同享变量的原子性

当对一个同享变量实行操纵时,我们可以操纵循环 CAS 的方式来保证原子操纵,可是对多个同享变量操纵时,循环 CAS 就没法保证操纵的原子性,这个时候便可以用锁。
大要有一个取巧的法子,就是把多个同享变量合并成一个同享变量来操纵。比若有两个同享变量 i = 2, j = a,合并一下 ij=2a,然后用 CAS 来操纵 ij。从 Java 1.5 起头 JDK 供给了 AtomicReference 类来保证援用工具之间的原子性,你可以把多个变量放在一个工具里来举行 CAS 操纵。
五、ThreadLocal

ThreadLocal 是一个存储线程当地副本的工具类
要保证线程平安,不愿定非要举行同步。同步只是保证同享数据争用时的切确性,假如一个方式本来就不触及同享数据,那末自然不必同步。
Java 中的 无同步计划 有:

  • 可重入代码 - 也叫纯代码。假如一个方式,它的 返回成果是可以猜测的,即只要输入了类似的数据,就能返回类似的成果,那它就满足可重入性,固然也是线程平安的。
  • 线程当地存储 - 操纵 ThreadLocal 为同享变量在每个线程中都建立了一个当地副本,这个副本只能被当前方程拜候,其他线程没法拜候,那末自然是线程平安的。
ThreadLocal 的用法

ThreadLocal 的方式:
[code]public class ThreadLocal {    public T get() {}    public void set(T value) {}    public void remove() {}    public static  ThreadLocal withInitial(Supplier

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|理想论坛_专业20年的财经股票炒股论坛交流社区 - 股票论坛

GMT+8, 2020-7-7 08:02 , Processed in 0.182854 second(s), 29 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表