海安零距离 海安论坛 海安新闻 海安

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

Java 并发核心机制

[复制链接]

6234

主题

6234

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
18716
发表于 2019-12-27 15:18 | 显示全部楼层 |阅读模式
本文以及示例源码已归档在 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|手机版|小黑屋|深圳论坛-深圳人的网上家园  

GMT+8, 2020-7-13 19:39 , Processed in 0.145621 second(s), 30 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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