关于java并发:并发基础第01章实现线程的正确方式

1. 问题的引出:实现线程有几种形式?2种?5种?正确答案:两种 实现Runnable接口继承Thread类1.1 Thread类中的run()Thread类的源码: private Runnable target;@Overridepublic void run() { if (target != null) { target.run(); }}Thread类有个Runnable的target援用,如果结构器传入了target且不为null,就执行它的run();但前提是它有机会执行--什么意思呢?1.2 既实现了Thread又实现了Runnable接口执行谁的run()且看如下一段代码:MyTask类是Runnable接口的实现;MyThread是Thread类的子类;如果初始化MyThread时把MyTask作为Runnable传入到target,会执行谁的run()? package com.niewj.basic.createthread;/** * 既实现Runnable又继承Thread * * @author niewj * @description * @copyright © 2022 niewj.com * @department 研发 * @date 2023/1/3 23:14 */public class RunnableThread { public static void main(String[] args) { // 1. 实现了Runnable MyTask task = new MyTask(); // 2. 实现了Runnable,也继承了Thread, thread2输入什么? Thread thread = new MyThread(task); thread.start(); } // MyThread是继承Thread的类 static class MyThread extends Thread { public MyThread(Runnable runnable) { super(runnable); } @Override public void run() { System.out.println("==============MyThread.run()===="); } } // MyTask是实现Runnable的接口 static class MyTask implements Runnable { @Override public void run() { System.out.println("==============Runnable.run()===="); } }}执行后果如下:可见执行了Thread的run()而不是Runnable的;这里比拟蛊惑的是,不是说target!=null就执行它的run()吗?要害是这里不是判断target的问题,而是整个Thread子类的run()办法被子类笼罩了,没有机会执行到MyThread父类Thread的run()办法了,这才是要害!==============MyThread.run()====1.3 简写成如下的代码再了解一遍:package com.niewj.basic.createthread;/** * 既实现Runnable又继承Thread * * @author niewj * @description * @copyright © 2022 niewj.com * @department 研发 * @date 2023/1/3 23:14 */public class RunnableThread { public static void main(String[] args) { // 1. 实现了Runnable Thread thread = new Thread(() -> System.out.println("==============Runnable.run()====")) { @Override public void run() { System.out.println("==============MyThread.run()===="); } }; // 2. 实现了Runnable,也继承了Thread, thread2输入什么? thread.start(); }}控制台: ...

January 4, 2023 · 1 min · jiezi

关于java并发:从零开始自己动手写阻塞队列

从零开始本人入手写阻塞队列前言在咱们平时编程的时候一个很重要的工具就是容器,在本篇文章当中次要给大家介绍阻塞队列的原理,并且在理解原理之后本人入手实现一个低配版的阻塞队列。 需要剖析在后面的两篇文章ArrayDeque(JDK双端队列)源码深度分析和深刻分析(JDK)ArrayQueue源码当中咱们认真介绍了队列的原理,如果大家感兴趣能够查看一下! 而在本篇文章所谈到的阻塞队列当中,是在并发的状况下应用的,下面所谈到的是队列是并发不平安的,然而阻塞队列在并发下状况是平安的。阻塞队列的次要的需要如下: 队列根底的性能须要有,往队列当中放数据,从队列当中取数据。所有的队列操作都要是并发平安的。当队列满了之后再往队列当中放数据的时候,线程须要被挂起,当队列当中的数据被取出,让队列当中有空间的时候线程须要被唤醒。当队列空了之后再往队列当中取数据的时候,线程须要被挂起,当有线程往队列当中退出数据的时候被挂起的线程须要被唤醒。在咱们实现的队列当中咱们应用数组去存储数据,因而在构造函数当中须要提供数组的初始大小,设置用多大的数组。阻塞队列实现原理线程阻塞和唤醒在下面咱们曾经谈到了阻塞队列是并发平安的,而且咱们还有将线程唤醒和阻塞的需要,因而咱们能够抉择可重入锁ReentrantLock保障并发平安,然而咱们还须要将线程唤醒和阻塞,因而咱们能够抉择条件变量Condition进行线程的唤醒和阻塞操作,在Condition当中咱们将会应用到的,次要有以下两个函数: signal用于唤醒线程,当一个线程调用Condition的signal函数的时候就能够唤醒一个被await函数阻塞的线程。await用于阻塞线程,当一个线程调用Condition的await函数的时候这个线程就会阻塞。数组循环应用因为队列是一端进一端出,因而队列必定有头有尾。 当咱们往队列当中退出一些数据之后,队列的状况可能如下: 在上图的根底之上咱们在进行四次出队操作,后果如下: 在下面的状态下,咱们持续退出8个数据,那么布局状况如下: 咱们晓得上图在退出数据的时候不仅将数组后半局部的空间应用完了,而且能够持续应用前半部分没有应用过的空间,也就是说在队列外部实现了一个循环应用的过程。 为了保障数组的循环应用,咱们须要用一个变量记录队列头在数组当中的地位,用一个变量记录队列尾部在数组当中的地位,还须要有一个变量记录队列当中有多少个数据。 代码实现成员变量定义依据下面的剖析咱们能够晓得,在咱们本人实现的类当中咱们须要有如下的类成员变量: // 用于爱护临界区的锁private final ReentrantLock lock;// 用于唤醒取数据的时候被阻塞的线程private final Condition notEmpty;// 用于唤醒放数据的时候被阻塞的线程private final Condition notFull;// 用于记录从数组当中取数据的地位 也就是队列头部的地位private int takeIndex;// 用于记录从数组当中放数据的地位 也就是队列尾部的地位private int putIndex;// 记录队列当中有多少个数据private int count;// 用于寄存具体数据的数组private Object[] items;构造函数咱们的构造函数也很简略,最外围的就是传入一个数组大小的参数,并且给下面的变量进行初始化赋值。 @SuppressWarnings("unchecked")public MyArrayBlockingQueue(int size) { this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); this.notFull = lock.newCondition(); // 其实能够不必初始化 类会有默认初始化 默认初始化为0 takeIndex = 0; putIndex = 0; count = 0; // 数组的长度必定不可能小于0 if (size <= 0) throw new RuntimeException("size can not be less than 1"); items = (E[])new Object[size];}put函数这是一个比拟重要的函数了,在这个函数当中如果队列没有满,则间接将数据放入到数组当中即可,如果数组满了,则须要将线程挂起。 ...

August 13, 2022 · 4 min · jiezi

关于java并发:Java并发笔记03-互斥锁上解决原子性问题

原子性问题的源头是线程切换 Q:如果禁用 CPU 线程切换是不是就解决这个问题了?A:单核 CPU 可行,但到了多核 CPU 的时候,有可能是不同的核在解决同一个变量,即使不切换线程,也有问题。 所以,解决原子性的要害是「同一时刻只有一个线程解决该变量,也被称为互斥」。 如何做到呢?用「锁」。 一、锁模型一)繁难锁模型个别看到的锁模型长上面这样。 但对于这个模型,会有几个疑难: 锁的是什么?临界区的这一堆代码相干的都被锁了?爱护的又是什么? 二)改良后的锁模型用上面这个模型来解释就解答了下面几个问题: 要爱护的是临界区中的资源 R因而要为 R 创立一个对应的锁 LR须要解决资源 R 的时候先加锁,解决完之后解锁 一个资源必须和锁对应,不能用 A 锁去锁 B 资源二、Java 提供的锁技术Java 提供了多种技术,这里仅谈及 Synchronized。 Synchronized 关键字Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字能够用来润饰办法,也能够用来润饰代码块。 class X { // 润饰非静态方法 synchronized void foo() { // 临界区 } // 润饰静态方法 synchronized static void bar() { // 临界区 } // 润饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } }} Q:synchronized 没看到 lock 和 unlock?A:在编译的时候会做转换,synchronized起始的中央加锁,完结的中央解锁。 ...

May 14, 2022 · 1 min · jiezi

关于java并发:Java并发编程的艺术一书知秋

前言最近又把《Java并发编程的艺术》这本书,重读了一遍,感觉播种比上一次还大!因为在理论的学校学习中,Java并发这一块,其实并没有很罕用,然而恰好不巧的事,在企业中在理论生产中,并发编程是不可或缺的一块,所以重读这本书我觉得很有必要。最近感悟,学习是一件很高兴的事,每当读《Java并发》《JVM》时,感觉莫名的兴奋,对技术的激情还是有哒!我的《平庸的世界》还没有读完,wuwuw,等忙完这阵子也要补补了。 一、并发编程的挑战1、上下文切换 (无锁并发编程(防止锁竞争),CAS算法,应用起码线程,协程)2、死锁 条件:1、互斥条件 2、不可剥夺条件 3、申请与放弃条件 4、循环期待条件解决:1防止同时取得多个锁 2防止一个锁内应用多个资源 3尝试应用定时锁 4数据库锁加锁和解锁在一个链接里Java:按序加锁、加锁时限、死锁检测(保护一个数据结构)3、资源限度(硬件(上传下载 读写)(集群) 软件(连接数)(池化复用)) 二、Java并发机制的底层实现1、volatile ①Lock前缀指令会引起处理器缓存写会主内存②处理器缓存写会主内存会使其余处理器的缓存生效2、synchronized 1.6后优化 无锁: 偏差锁标识为0,则CAS获取偏差锁偏差锁:偏差锁标识为1,锁对象记录线程ID,下次该线程获取时间接测试,失败则CAS尝试锁记录指向以后线程ID敞开偏差锁,间接进入轻量级锁轻量级锁:加锁:以后线程的栈帧中创立空间,对象头的MarkWord复制该空间,CAS尝试对象图的MW指向该空间,胜利则取得锁,失败则自旋获取锁;解锁:CAS操作将栈帧中的锁记录Displaced MarkWoed替换回对象头,胜利即解锁,失败则有竞争,收缩为重量级锁,阻塞竞争线程,本人开释锁并唤醒竞争线程重量级锁:竞争线程阻塞,解锁后唤醒竞争线程3、原子操作 锁(CAS形式开释获取锁)自旋CAS(ABA(版本号)、循环开销、只保障一个共享变量的原子操作)三、Java内存模型Java并发采纳共享内存模式(消息传递模式)隐式通信显示同步主内存存储共享变量 每个线程有本地内存保留主内存的共享变量正本四、Java并发根底 1、过程资源分配的最小单位、线程程序执行的最小单位 2、线程的状态 new->runnable(running、ready)->terminated waiting|time_waiting|blocked 3、中断线程interrupt 而不应用stop(不开释资源);也可在线程外部应用一个boolean变量 4、线程先获取监视器对象,获取失败则进入SynchronizedQueue wait->进入WaitQueue->notify->SynchronizedQueue? |5、t.join 以后线程进入wating态期待t线程执行完 波及期待告诉机制 给t加锁 让以后线程期待,t完结时唤醒以后线程 6、ThreadLocal线程本地变量 数据库连贯治理保障以后线程操作的Connection都为同一个 ThreadLocal.set会在thread中ThreadLocalMap保留值,以ThreadLocal为键,value为值 ThreadLocal.get会获取thread中的ThreadLocalMap以ThreadLocal取值 ThreadLocalMap是ThreadLocal的外部类,被thread援用五、Java中的锁 1、Lock接口 非阻塞获取锁、超时获取锁、响应中断的获取锁 2、AQS 1定义同步组件,实现AQS,重写相应办法,同步组件调用AQS的模板办法即可 2依赖同步队列实现同步状态的治理 同步队列中有节点保留获取同步线程失败的线程援用,期待状态 同步器蕴含头尾节点,首节点是获取同步状态胜利的节点,开释同步状态时唤醒后继节点并设置为首节点 3、重入锁:反复加锁而不被本人阻塞 ReentrantLock|Synchronized 偏心与非偏心锁:是否申请程序 ReentrantLock 4、读写锁ReentrantReadWriteLock 一个读多个写 保护一对读锁写锁 同步整型状态按位切割 锁降级(写->读) 5、LockSupport工具 阻塞唤醒以后线程 6、Condition(Lock.newCondition) 相似wait/notify 作为同步器的外部类,AQS援用多个Condition期待队列 await同步队列首节点->期待队列尾结点 signal期待队列首节点->同步队列首节点六、Java并发容器和框架 1、ConcurrentHashMap JDK7 segment[](继承自ReentantLockd)->数组+链表 get不加锁(volatile润饰) put加分段锁(判断是否扩容,单个Segment扩容) JDK8 数组+链表+红黑树 get不加锁 put时初始化,没有hash抵触间接CAS插入,有就synchronized锁住头结点插入 put操作 1数组为空则进行初始化 2首节点为空则cas直接插入 3须要扩容则帮助扩容 扩容时多线程并发扩容helpTransfer 非凡首节点ForwardingNode示意已扩容过间接跳过 4首节点加synchronized锁put 2、线程平安队列 非阻塞(循环cas)|阻塞(锁) 3、ConcurrentLinkedQueue无界限程平安队列 cas入队尾结点 cas出队列头结点 4、阻塞队列BlockingQueue ArrayBlockingQueue 数组构造的有界阻塞队列(必须指定长度) 不保障偏心拜访 LinkedBlockingQueue 链表构造的有界阻塞队列(可指定,默认长度为int_max) PriorityBlockingQueue 反对优先级的无界阻塞队列(可扩容,长度最大也为int_max(queue为数组实现的堆)) DelayQueue 延时获取元素的无界阻塞队列 SynchronnousQueue 一个put对应一个take不存储元素 LinkedTransferQueue 链表构造的无界阻塞队列 LinkedBlockingQueue 链表构造组成的双向阻塞队列 阻塞队列实现原理 期待告诉模式 condition.await|signal->LockSupport.park|unpark(先保留以后线程再阻塞)->底层park|unpark七、原子操作类 原子更新根本类型 AtomicBoolean AtomicInteger AtomicLong(其余根本类型 转为int型) 以AtomicInteger的getAndIncrement为例 get()获取旧值 新值=旧值+1 cas(旧值,新值)->unsafe的native办法 循环直到胜利 原子更新数组 AtomicIntegerArray AtomicLongArray AtomicReferenceArray 原子更新援用类型 AtomicReference AtomicReferenceFiledUpdatr AtomicMarkableReference 原子更新字段类 AtomicIntegerFiledUpdater AtomicLongFiledUpdater AtomicStampedReference八、Java中的并发工具类 1、CountDownLatch代替join实现计数器 2、CyclicBarrier 阻塞一批线程直到所有线程都实现,关上屏障 下一步动作实施者不一样CountdownLatch为主线程 CyclicBarrier为其余线程 CyclicBarrier 计数器能够重置 适宜更简单场景 3、Semaphore 管制并发线程的数量 流控 4、Exchanger 线程间替换数据九、线程池 1、实现原理 execute提交工作 外围线程池未满(即便有闲暇,不销毁)则创立线程执行工作,已满则阻塞队列是否已满,未满则退出队列,已满则看线程池是否已满,未满则创立线程执行工作,已满则执行回绝策略 创立的线程封装为工作线程Worker,执行完从阻塞队列中获取工作 2、new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler) corePoolSize 外围线程大小 maximumPoolSize 最大线程数量 keepAliveTime 闲暇线程存活工夫 workQueue ArrayBlockingQueue LinkedBlockingQuene SynchronousQuene PriorityBlockingQueue threadFactory 线程工厂 次要定义名字 handler 回绝策略 CallerRunsPolicy 调用者线程执行 AbortPolicy 间接抛弃 抛出RejectedExecutionException异样 DiscardPolicy 间接抛弃 DiscardOldestPolicy 摈弃最早进入队列的工作,放进队列 3、Executor框架 ThreadPoolExecutor SingleThreadExecutor 1-1 LinkedBlockingQueue FixedThreadPool core-max LinkedBlockingQueue CacheThreadPool 0-max SynchronousQueue 不同创立线程 ScheduledThreadPoolExecutor 比Timer更灵便 ScheduledThreadPoolExecutor DelayQueue 外部是一个PriorityQueue把time小的优先 工作须要实现Delay接口 SingleThreadScheduledExecutor Future 示意后果 Runnable Callable Executors总结最近,没那么多工夫来写博客了,只能简略的对重读的常识,进行总结,所以排版不是很好,有工夫会回来优化的! ...

May 4, 2021 · 1 min · jiezi

关于java并发:Java并发JMM的8大原子操作及并发3之volatile关键字可见性

摘要咱们之前解说了JMM模型,以及其引入的必要行,以及JMM与JVM内存模型的比拟和JMM与硬件内存构造的对应关系。 思维导图本节次要解说思维导图如下: 内容1、JMM的8大原子操作1、lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 2、unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,开释后的变量 才能够被其余线程锁定。3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作应用。 4、load(载入):作用于工作内存的变量,它把read操作从主内存中失去的变量值放入工作内存的 变量正本中。 5、use(应用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟时机到一个须要应用变量的值的字节码指令时将会执行这个操作。 6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接管的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 7、store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作应用。8、write(写入):作用于主内存的变量,它把store操作从工作内存中失去的变量的值放入主内存的变量中。 留神:1、如果须要把变量总主内存赋值给工作内存:read和load必须是间断;read只是把主内存的变量值从主内存加载到工作内存中,而load是真正把工作内存的值放到工作内存的变量正本中。 2、如果须要把变量从工作内存同步回主内存;就须要执行程序执行store跟write操作。store作用于工作内存,将工作内存变量值加载到主内存中,write是将主内存外面的值放入主内存的变量中。 代码实例: public class VolatileTest2 { static boolean flag = false; public void refresh(){ this.flag = true; String threadName = Thread.currentThread().getName(); System.out.println("线程: "+threadName+" 批改共享变量flag为"+flag); } public void load(){ String threadName = Thread.currentThread().getName(); while (!flag){ } System.out.println("线程: "+threadName+" 嗅探到flag状态的扭转"+" flag:"+flag); } public static void main(String[] args) { /** * 创立两个线程 */ VolatileTest2 obj = new VolatileTest2(); Thread thread1 = new Thread(() -> { obj.refresh(); }, "thread1"); Thread thread2 = new Thread(() -> { obj.load(); }, "thread2"); thread2.start(); try { /** * 确保咱们线程2先执行 */ Thread.sleep(2000); }catch (Exception e){ e.printStackTrace(); } thread1.start(); }}咱们发现下面代码数据后果为: ...

January 13, 2021 · 2 min · jiezi

关于java并发:Java并发JMM

摘要之前咱们解说过cpu多级缓存模型,然而对于JVM来说为了屏蔽掉各种操作系统跟各种硬件的差别,是各个操作系统和硬件数据读写原理一致性而引入了java内存模型JMM; 思维导图内容JMM模型前言: JMM它是一个虚构的货色,是一个形象的概念;形容的是一组标准;形象的就是cpu的多核缓存架构;为了实现java跨平台;屏蔽掉计算机硬件跟操作系统,保障在各个操作系统上读取数据的一致性。如下,咱们能够把java内存模型跟计算机多核cpu缓存模型进行形象。 java的工作内存能够是:计算机主内存、cpu的多级缓存、cpu的寄存器。jvm外面的主内存能够是:计算机主内存、cpu的多级缓存、cpu的寄存器。

January 10, 2021 · 1 min · jiezi

关于java并发:Java并发线程及并发

摘要之前咱们解说了cpu多多级缓存模型,以及为什么须要引入cpu多级缓存模型?(为了解决cpu运算速度远高于基于I/O总线读取主内存数据速度)而后引入cpu多级缓存模型之后产生的问题?(数据缓存一致性)而后就是解决cpu缓存一致性问题的计划?(总线加锁及缓存一致性协定MESI)而后具体解说了缓存一致性协定MESI中多线程读写主内存数据时候产生的问题;这一讲次要解说下什么是线程以及为什么须要并发? 思维导图咱们依照以下思维逻辑导图解说线程及并发。 内容1、线程及过程过程: 零碎分配资源的根本单位;其就是咱们运行的一个应用程序;比方:JVM过程、微信、QQ。 线程: 操作系统调度cpu的根本单位;是过程中的一个场景快照。 线程及过程关系: 1、过程是零碎分配资源的根本单位;线程是调度cpu的根本单位。过程是没有调用cpu的权力的,如果咱们的jvm是一个应用程序,它自身是没有调用cpu的权力的(调用cpu的权力是操作系统的性能),如果咱们的jvm可能不依赖于咱们的操作系统。间接能够操作cpu和显卡的话,那么这个jvm不就是一个操作系统了吗?那他还装置到操作系统干嘛呢?线程是调度cpu的根本单位,自身是不具备更多的资源,只具备本身须要的简略资源,所以说线程会共享过程外面的资源。 2、综上所叙述:线程是调度cpu的根本单位,一个过程至多蕴含一个线程,线程寄生在过程当中。每个线程都有一个程序计数器(记录要执行的下一条指令)、一组寄存器(保留以后线程的工作变量)。 分类: 依照线程所属空间,咱们将线程分为:用户线程(User-Level Thread)、内核线程(Kernel-Level Thread);不同品种的线程工作在不同的空间外面。 咱们看下如下代码: str = “I like learning“ //用户空间 x = x + 2file.write(str) //切换到内核空间y=x+4 //切换到用户空间 如上图:咱们的用户空间划分为两局部:用户空间、内核空间。内核空间:零碎内核运行的空间。 用户空间:用户程序运行的空间;比方JVM、PS、播放器。 咱们程序运行的状态,如果运行在内核空间就是内核态,运行在用户空间的话它是用户态。为了安全性起见,两者是隔离的,即便用户空间的JVM解体了,内核空间不受影响的。内核空间的话是能够执行任何命令,调用零碎任何资源。用户空间的话简略运算,不能调度系统资源。内核空间会提供一个接口供用户空间发送指令。 cpu特权级别: cpu特权级别分为:ring0、ring3。 cpu特权级别:为什么咱们的JVM须要依赖咱们的内核能力调用咱们的cpu呢?因为咱们的cpu分为了两级特权。一级是ring0(最好级别,领有cpu最高操作权限)、一级是ring3最低级别(领有简略的操作权限,没有外围操作权限);用户空间只有cpu的ring3特权级别。内核空间只有cpu的ring0特权级别。为什么须要这样的划分?为了安全性:如果说咱们的JVM在用户空间,具备cpu0的特权,那么他就可能操作咱们的内核空间,这样的话通过更改内核能够植入病毒,以及管制其余应用程序。所以只有内核空间才具备操作cpu的最高特权级别。 用户线程与内核线程: 用户线程(ULT): 就是在用户空间外面的过程创立的线程。用户线程它是没有cpu的应用权限的,它是不可能间接去调度cpu(只能简略操作权限),因为咱们的内核是不晓得多线程的存在的。内核基本不晓得多线程的用户线程存在,因为在其外部保护的是一个过程表。咱们cpu处理器资源的调配工夫分片的话,是以过程为根本单位的,所以线程是依靠于咱们的主过程去执行的。所有的线程都执行在同一条线上。这就有一个问题当咱们的用户线程阻塞了,比方线程1阻塞了。整个主过程将会被阻塞。如下: 内核线程(KLT):内核线程由在内核空间创立,保护在内核空间的线程表外面(如下面图示:过程表里保护了用户空间的过程、线程表外面保护了内核空间的线程);同理,内核过程也是在内核空间创立,在内核空间保护过程表。内核级线程是操作系统去实现的。cpu会为咱们的内核级线程调配工夫片。所以多个线程都能够去抢夺cpu资源。内核线程阻塞了的话不会影响内核过程的运行。 在java外面用的是哪种线程呢?在java外面1.2版本之前用的是ULT;1.2之后用的是KLT内核级线程;java线程模型就是依赖于底层内核线程去保护的,两者有什么关系呢?如下: 他们是一一映射的关系。 如上:咱们的jvm过程是能够创立多个线程的,实质上是jvm去创立了线程栈空间(其实没有去创立真正的线程);线程栈空间外面会有一些栈针指令; 创立真正的线程是须要通过咱们的库调度器去调度咱们的内核空间去创立内核线程,从而操作调度cpu; java线程<--->内核线程。 2、并发Java线程生命周期 Java线程生命周期的话是仅仅限度在咱们的JVM外面,总共就只有6中状态: 新建: NEW运行:RUNNABLE期待:WAITING阻塞:BLOCKED 终止:TERMINATED 留神,咱们的阻塞之后 不是不运行了,而是进入就绪状态,等待时间片调配。 为什么用并发 1、充分利用多核CPU的计算能力。 2、不便进行业务拆分,晋升利用性能。 并发产生的问题: 1、高并发场景下,导致频繁的上下文切换 2、临界区线程平安问题,容易呈现死锁的,产生死锁就会造成零碎性能不可用。 留神:在咱们单核处理器也反对多线程代码的,只不过是cpu给每一个线程调配工夫片(时分复用)来实现这种机制的。这情状况下cpu给每个线程调配的工夫比拟短,让咱们感觉多个线程如同是同时执行的。因为这个工夫片的工夫只有几十毫秒,是十分的短,然而须要频繁的线程上下文切换。 还有一点要须要留神的:并发与并行区别:并发:多个工作交替执行;比方工夫片cpu的切分。 并行:才是真正意义上的多个工作同时进行。 如果咱们的零碎内核只有一个cpu,那么他应用多线程时候,实际上并不是并行的,他只能通过工夫片的模式。去并发执行工作。真正意义上的并发只会呈现在多个cpu的零碎当中。 毛病:如果在大并发条件下大量创立线程。因为咱们的linux零碎或者jvm他的线程数是有一个峰值的。如果超过了这个峰值的话,性能会大幅度降落。 线程上下文切换原理分析如下: ...

January 10, 2021 · 1 min · jiezi

Week-1-Java-多线程-锁优化轻量级锁偏向锁原理及锁的状态流转

前言学习情况记录 时间:week 1SMART子目标 :Java 多线程记录在学习Java 多线程中 锁优化的有关知识点。 为了进一步改进高效并发,HotSpot虚拟机开发团队在JDK1.6版本上花费了大量精力实现各种锁优化。如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。(主要指的是synchronized的优化)。 适应性自旋 (自旋锁)为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。引入自旋锁的原因是互斥同步对性能最大的影响是阻塞的实现,管钱线程和恢复线程的操作都需要转入内核态中完成,给并发带来很大压力。自旋锁让物理机器有一个以上的处理器的时候,能让两个或以上的线程同时并行执行。我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。 在 JDK 1.6之前,自旋次数默认是10次,用户可以使用参数-XX:PreBlockSpin来更改。 JDK1.6引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。(这个应该属于试探性的算法)。 锁消除锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行清除。锁清除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步枷锁自然就无需进行。 简单来说,Java 中使用同步 来保证数据的安全性,但是对于一些明显不会产生竞争的情况下,Jvm会根据现实执行情况对代码进行锁消除以提高执行效率。 举例说明对于一些看起来没有加锁的代码,其实隐式的加了很多锁,这些也是锁消除优化的对象。例如下面的字符串拼接代码就隐式加了锁: String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作: 每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。 锁粗化如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。当多个彼此靠近的同步块可以合并到一起,形成一个同步块的时候,就会进行锁粗化。该方法还有一种变体,可以把多个同步方法合并为一个方法。如果所有方法都用一个锁对象,就可以尝试这种方法。轻量级锁 (@重点知识点)JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。 重量级排序 :重量级锁 > 轻量级锁 > 偏向锁 > 无锁 先介绍一下HotSpot 虚拟机对象头的内存布局: 上面这些数据被称为Mark Word - 标记关键词。 其中 tag bits 对应了五个状态,这些状态的含义在右侧的 state 表格中给出。除了 marked for gc 状态(gc标记状态),其它四个状态已经在前面介绍过了。 ...

July 8, 2019 · 1 min · jiezi

Week-1-Java-多线程-CAS

前言学习情况记录 时间:week 1SMART子目标 :Java 多线程记录在学习线程安全知识点中,关于CAS的有关知识点。 线程安全是指:多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。 常见的线程安全实现方法分为不可变对象、线程互斥同步、非阻塞同步、线程本地存储等方案,本文要讲的就是非阻塞同步中的核心CAS. 非阻塞同步从处理问题的方式上说,互斥同步属于一种悲观的并发策略。 随着硬件指令集的发展,我们可以采用基于冲突检查的乐观并发策略,通俗地说,就是先行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现偶读不需要把线程挂起,因此这种同步操作称为非阻塞同步。 CAS乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。 各种Atomic开头的原子类,内部都应用到了CAS。就拿AtomicInteger为例。 J.U.C 包里面的原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。 看看AtomicInteger对象一次自增,CAS起了什么作用,以下代码是 incrementAndGet() 的源码,可以看到内部调用了 Unsafe 对象的 getAndAddInt() 。 以下代码是 getAndAddInt()源码,var1 指示对象内存地址,var2指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。 compareAndSwapInt(var1, var2, var5, var5 + var4 其实换成compareAndSwapInt(obj, offset, expect, update)比较清楚,意思就是如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作,取出乍一看这也是两个步骤了啊,其实在JNI里是借助于一个CPU指令完成的。所以还是原子操作。 ...

July 8, 2019 · 1 min · jiezi

Java并发17并发设计模式-Immutability模式如何利用不变性解决并发问题

解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。 快速实现具备不可变性的类实现一个具备不可变性的类,还是挺简单的。将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。 Java SDK 里很多类都具备不可变性,只是由于它们的使用太简单,最后反而被忽略了。例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的 我们结合 String 的源代码来解释一下这个问题,下面的示例代码源自 Java 1.8 SDK,我略做了修改,仅保留了关键属性 value[] 和 replace() 方法,你会发现:String 这个类以及它的属性 value[] 都是 final 的;而 replace() 方法的实现,就的确没有修改 value[],而是将替换后的字符串作为返回值返回了。 public final class String { private final char value[]; // 字符替换 String replace(char oldChar, char newChar) { // 无需替换,直接返回 this if (oldChar == newChar){ return this; } int len = value.length; int i = -1; /* avoid getfield opcode */ char[] val = value; // 定位到需要替换的字符位置 while (++i < len) { if (val[i] == oldChar) { break; } } // 未找到 oldChar,无需替换 if (i >= len) { return this; } // 创建一个 buf[],这是关键 // 用来保存替换后的字符串 char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } // 创建一个新的字符串返回 // 原字符串不会发生任何变化 return new String(buf, true); }}通过分析 String 的实现,你可能已经发现了,如果具备不可变性的类,需要提供类似修改的功能,具体该怎么操作呢?做法很简单,那就是创建一个新的不可变对象,这是与可变对象的一个重要区别,可变对象往往是修改自己的属性。 ...

July 4, 2019 · 2 min · jiezi

Java-SDK-并发包全面总结

一、Lock 和 ConditionJava 并发包中的 Lock 和 Condition 主要解决的是线程的互斥和同步问题,这两者的配合使用,相当于 synchronized、wait()、notify() 的使用。 1. Lock 的优势比起传统的 synchronized 关键字,Lock 最大的不同(或者说优势)在于: 阻塞的线程能够响应中断,这样能够有机会释放自己持有的锁,避免死锁支持超时,如果线程在一定时间内未获取到锁,不是进入阻塞状态,而是抛出异常非阻塞的获取锁,如果未获取到锁,不进入阻塞状态,而是直接返回三种情况分别对应 Lock 的三个方法:void lockInterruptibly(),boolean tryLock(long time, TimeUnit unit),boolean tryLock()。 Lock 最常用的一个实现类是 ReentrantLock,代表可重入锁,意思是可以反复获取同一把锁。除此之外,Lock 的构造方法可以传入一个 boolean 值,表示是否是公平锁。 2. Lock 和 Condition 的使用前面实现的简单的阻塞队列就是使用 Lock 和 Condition ,现在其含义已经非常明确了: public class BlockingQueue<T> { private int capacity; private int size; //定义锁和条件 private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); /** * 入队列 */ public void enqueue(T data){ lock.lock(); try { //如果队列满了,需要等待,直到队列不满 while (size >= capacity){ notFull.await(); } //入队代码,省略 //入队之后,通知队列已经不为空了 notEmpty.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { //在finally块中释放锁,避免死锁 lock.unlock(); } } /** * 出队列 */ public T dequeue(){ lock.lock(); try { //如果队列为空,需要等待,直到队列不为空 while (size <= 0){ notEmpty.await(); } //出队代码,省略 //出队列之后,通知队列已经不满了 notFull.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } //实际应该返回出队数据 return null; }}可以看到,Lock 需要手动的加锁和解锁,并且解锁操作是放在 finally 块中的,这是一种编程范式,尽量遵守。 ...

May 29, 2019 · 7 min · jiezi

Java-中的线程安全容器

一、同步容器常用的一些容器例如 ArrayList、HashMap、都不是线程安全的,最简单的将这些容器变为线程安全的方式,是给这些容器所有的方法都加上 synchronized 关键字。 Java 的 Collections 中实现了这些同步容器: 简单的使用如下: List<String> list = Collections.synchronizedList(new ArrayList<>());Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());Set<String> set = Collections.synchronizedSet(new HashSet<>());同步容器虽然简单,但是相应的效率较低,因为锁的粒度较大。 循环遍历同步容器 如果在遍历同步容器的时候,组合了多个方法,这会可能会存在竞态条件,仍然不是线程安全的。解决的办法便是对容器加锁。例如下面这样: public static void main(String[] args) { List<String> list = Collections.synchronizedList(new ArrayList<>()); //省略添加数据的操作 String[] str = new String[list.size()]; int k = 0; synchronized (list){ Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ str[k ++] = iterator.next(); } }}二、并发容器Java 中还提供了一系列并发容器,相比于同步容器,其性能更好。并发容器共分为了四类:List、Map、Set、Queue。 1. ListList 中一个最主要的实现类是 CopyOnWriteArrayList ,CopyOnWrite,即写时复制,这样的好处是读操作是无锁的。 ...

May 25, 2019 · 1 min · jiezi

Java并发10-ReadWriteLock快速实现一个完备的缓存

大家知道了Java中使用管程同步原语,理论上可以解决所有的并发问题。那 Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性 今天我们就介绍一种非常普遍的并发场景:读多写少场景。实际工作中,为了优化性能,我们经常会使用缓存,例如缓存元数据、缓存基础数据等,这就是一种典型的读多写少应用场景。缓存之所以能提升性能,一个重要的条件就是缓存的数据一定是读多写少的. 针对读多写少这种并发场景,Java SDK 并发包提供了读写锁——ReadWriteLock,非常容易使用,并且性能很好。 什么是读写锁读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则: 允许多个线程同时读共享变量;只允许一个线程写共享变量;如果一个写线程正在执行写操作,此时禁止读线程读共享变量。读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行读操作和写操作的。 快速实现一个缓存在下面的代码中,我们声明了一个 Cache<K, V> 类,其中类型参数 K 代表缓存里 key 的类型,V 代表缓存里 value 的类型。缓存的数据保存在 Cache 类内部的 HashMap 里面,HashMap 不是线程安全的,这里我们使用读写锁 ReadWriteLock 来保证其线程安全。ReadWriteLock 是一个接口,它的实现类是 ReentrantReadWriteLock,通过名字你应该就能判断出来,它是支持可重入的。下面我们通过 rwl 创建了一把读锁和一把写锁。 Cache 这个工具类,我们提供了两个方法,一个是读缓存方法 get(),另一个是写缓存方法 put()。读缓存需要用到读锁,读锁的使用和前面我们介绍的 Lock 的使用是相同的,都是 try{}finally{}这个编程范式。写缓存则需要用到写锁,写锁的使用和读锁是类似的。 class Cache<K,V> { final Map<K, V> m = new HashMap<>(); final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 final Lock r = rwl.readLock(); // 写锁 final Lock w = rwl.writeLock(); // 读缓存 V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 V put(String key, Data v) { w.lock(); try { return m.put(key, v); } finally { w.unlock(); } }}实现缓存的按需加载设计封装缓存类时,我们需要在当应用查询缓存,并且数据不在缓存里的时候,触发加载源头相关数据进缓存的操作,这也是我们需要实现的最基本的功能。下面看下利用 ReadWriteLock 来实现缓存的按需加载。 ...

May 13, 2019 · 2 min · jiezi

Java并发9Lock和Condition下-Dubbo如何用管程实现异步转同步

在上一篇文章中,我们讲到 Java SDK 并发包里的 Lock 有别于 synchronized 隐式锁的三个特性:能够响应中断、支持超时和非阻塞地获取锁。那今天我们接着再来详细聊聊 Java SDK 并发包里的 Condition。 Condition 实现了管程模型里面的条件变量在之前我们详细讲过, Java 语言内置的管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,这是二者的一个重要区别。 在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容易。例如,实现一个阻塞队列,就需要两个条件变量。 这里我们温故知新下前面的内容。 public class BlockedQueue<T>{ final Lock lock = new ReentrantLock(); // 条件变量:队列不满 final Condition notFull = lock.newCondition(); // 条件变量:队列不空 final Condition notEmpty = lock.newCondition(); // 入队 void enq(T x) { lock.lock(); try { while (队列已满){ // 等待队列不满 notFull.await(); } // 省略入队操作... // 入队后, 通知可出队 notEmpty.signal(); }finally { lock.unlock(); } } // 出队 void deq(){ lock.lock(); try { while (队列已空){ // 等待队列不空 notEmpty.await(); } // 省略出队操作... // 出队后,通知可入队 notFull.signal(); }finally { lock.unlock(); } }}不过,这里你需要注意,Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的, 不要相互使用。 ...

May 12, 2019 · 2 min · jiezi

Java并发8Lock和Condition上-隐藏在并发包中的管程

Java SDK 并发包内容很丰富。但是最核心的还是其对管程的实现。因为理论上利用管程,你几乎可以实现并发包里所有的工具类。在前面我们提到过在并发编程领域,有两大核心问题:一个是互斥:即同一时刻只允许一个线程访问共享资源;另一个是 同步:即线程之间如何通信、协作。 这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。 今天我们重点介绍 Lock 的使用,在介绍 Lock 的使用之前,有个问题需要你首先思考一下:Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?很显然它们之间是有巨大区别的。那区别在哪里呢? 再造管程的理由让我们回顾下在之前的死锁问题中。提出一个破坏不可抢占条件的方案。但是这个方案 synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是: 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。如果我们重新设计一把互斥锁去解决这个问题,那该怎么设计呢?我觉得有三种方案。 1. 能够响应中断synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。 2. 能够支持超时如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。 3. 非阻塞地获取锁如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。 这三种方案可以全面弥补 synchronized 的问题。这三个方案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法。详情如下: // 支持中断的 APIvoid lockInterruptibly() throws InterruptedException;// 支持超时的 APIboolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 支持非阻塞获取锁的 APIboolean tryLock();如何保证可见性Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{}。需要重点关注的是在 finally 里面释放锁。这个范例无需多解释。但是有一点需要解释一下,那就是可见性是怎么保证的。你已经知道 Java 里多线程的可见性是通过 Happens-Before 规则保证的,而 synchronized 之所以能够保证可见性,也是因为有一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁的加锁。那 Java SDK 里面 Lock 靠什么保证可见性呢?例如在下面的代码中,线程 T1 对 value 进行了 +=1 操作,那后续的线程 T2 能够看到 value 的正确结果吗? ...

May 12, 2019 · 2 min · jiezi

安全发布对像

发布对像定义: 是一个对象能够被当前范围之外的代码所使用对象溢出一种错误的发布。当一个对象该没有构造完成时,就使被其他线程所见。下面我们来看一下没有安全发布的对象 @Slf4jpublic class UnsafePublish { private String[] states = {"a", "b", "c"}; public String[] getStates() { return states; } public static void main(String[] args) { UnsafePublish unsafePublish = new UnsafePublish(); log.info("{}", Arrays.toString(unsafePublish.getStates())); unsafePublish.getStates()[0] = "d"; log.info("{}", Arrays.toString(unsafePublish.getStates())); }}我们看这段代码,我们创建了一个对象通过getStates方法我们可以获取这个对象的数组,此时我们将数组内容打印出来结果,如果此时我们将这个对象发布出去,然后其他线程(这里没有模拟其他线程对其修改)又对这个对象的states的值进行修改,此时在拿到这个对象的期望的是没有被修改的,事实上得到的对象是修改过后的。也就是说我们不能直接通过一个public的一个set方法就行return。 下面我们再看一段对象溢出的代码 public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } void doSomething(Event e) { } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { }}这将导致this逸出,所谓逸出,就是在不该发布的时候发布了一个引用。在这个例子里面,当我们实例化ThisEscape对象时,会调用source的registerListener方法,这时便启动了一个线程,而且这个线程持有了ThisEscape对象(调用了对象的doSomething方法),但此时ThisEscape对象却没有实例化完成(还没有返回一个引用),所以我们说,此时造成了一个this引用逸出,即还没有完成的实例化ThisEscape对象的动作,却已经暴露了对象的引用。其他线程访问还没有构造好的对象,可能会造成意料不到的问题。 ...

May 9, 2019 · 1 min · jiezi

Java并发6管程java管程初探

并发编程这个技术领域已经发展了半个世纪了。有没有一种核心技术可以很方便地解决我们的并发问题呢?这个问题, 我会选择 Monitor(管程)技术。Java 语言在 1.5 之前,提供的唯一的并发原语就是管程,而且 1.5 之后提供的 SDK 并发包,也是以管程技术为基础的。除此之外,C/C++、C# 等高级语言也都支持管程。 什么是管程操作系统原理课程告诉我们,用信号量能解决所有并发问题。但是为什么 Java 在 1.5 之前仅仅提供了 synchronized 关键字及 wait()、notify()、notifyAll() 这三个看似从天而降的方法?当然这里因为 Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。并且 管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。 管程,对应的英文是 Monitor,很多 Java 领域的同学都喜欢将其翻译成“监视器”,这是直译。操作系统领域一般都翻译成“管程”,这个是意译,在这里我更倾向于使用“管程”。 管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。那管程是怎么管的呢? MESA 模型在管程的发展史上,先后出现过三种不同的管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。其中,现在广泛应用的是 MESA 模型,并且 Java 管程的实现参考的也是 MESA 模型。所以我们重点介绍一下 MESA 模型。 在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。 我们先来看看管程是如何解决互斥问题的。 管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来。在下图中,管程 X 将共享变量 queue 这个队列和相关的操作入队 enq()、出队 deq() 都封装起来了;线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。从中可以看出,管程模型和面向对象高度契合的。而我在前面章节介绍的互斥锁用法,其背后的模型其实就是它。 ...

April 27, 2019 · 2 min · jiezi

Java线程的生命周期

概要目前CPU的运算速度已经达到了百亿次每秒,甚至更高的量级,家用电脑即使维持操作系统正常运行的进程也会有数十个,线程更是数以百计。线程是CPU的调度和分派的基本单位,为了更充分地利用CPU资源以及提高生产率和高效地完成任务,在现实场景中一般都会采用多线程处理。线程的生命周期线程的生命周期大致可以分为下面五种状态:New(新建状态)、RUNABLE(就绪状态)、RUNNING(运行状态)、休眠状态、DEAD(终止状态)1、新建状态,是线程被创建且未启动的状态;这里的创建,仅仅是在JAVA的这种编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。Thread t1 = new Thread()2、就绪状态,指的是调用start()方法之后,线程等待分配给CPU执行(这时候,线程已经在操作系统层面被创建)t1.start()3、运行状态,当CPU空闲时,线程被分得CPU时间片,执行Run()方法的状态4、休眠状态,运行状态的线程,如果调用一个阻塞的API或者等待某个事件,那么线程的状态就会转换到休眠状态,一般有以下几种情况同步阻塞:锁被其它线程占用主动阻塞:调用Thread的某些方法,主动让出CPU执行权,比如:sleep()、join()等方法等待阻塞:执行了wait()方法5、终止状态,线程执行完(run()方法执行结束)或者出现异常就会进入终止状态对应的是JAVA中Thread类State中的六种状态public class Thread implements Runnable { //……… public enum State { NEW, // 初始化状态 RUNNABLE, // 可运行/运行状态 BLOCKED, // 阻塞状态 WAITING, // 无时限等待 TIMED_WAITING, // 有时限等待 TERMINATED; // 终止状态 } // ……….}休眠状态(BLOCKED、WAITING、TIMED_WAITING)与RUNNING状态的转换1、RUNNING状态与BLOCKED状态的转换线程等待 synchronized 的隐式锁,RUNNING —> BLOCKED线程获得 synchronized 的隐式锁,BLOCKED —> RUNNING2、RUNNING状态与WAITING状态的转换获得 synchronized 隐式锁的线程,调用无参数的Object.wait()方法调用无参数Thread.join()方法调用LockSupport.park()方法,线程阻塞切换到WAITING状态,调用LockSupport.unpark()方法,可唤醒线程,从WAITING状态切换到RUNNING状态3、RUNNING状态与TIMED_WAITING状态的转换调用带超时参数的 Thread..sleep(long millis)方法获得 synchronized 隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法调用带超时参数的Thread.join(long millis)方法调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)方法调用带超时参数的LockSupport.parkUntil(long deadline)方法废弃掉的线程方法 :stop()、suspend()、resume()stop()方法,会真正的杀死线程,不给线程任何喘息的机会,假设获得 synchronized 隐式锁的线程,此刻执行stop()方法,该锁不会被释放,导致其它线程没有任何机会获得锁,显然这样的结果不是我们想要见到的。suspend() 和 resume()方法同样,因为某种不可预料的原因,已经被建议不在使用不能使用stop()、suspend() 、resume() 这些方法来终止线程或者唤醒线程,那么我们应该使用什么方法来做呢?答案是:优雅的使用Thread.interrupt()方法来做优雅的Thread.interrupt()方法中断线程interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知,这个方法通过修改了调用线程的中断状态来告知那个线程,说它被中断了,线程可以通过isInterrupted() 方法,检测是不是自己被中断。当线程被阻塞的时候,比如被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞时;调用它的interrput()方法,会产生InterruptedException异常。扩展知识点探秘局部变量不会引发并发问题的原因在Java领域,线程可以拥有自己的操作数栈,程序计数器、局部变量表等资源;我们都知道,多个线程同时访问共享变量的时候,会导致数据不一致性等并发问题;但是 Java 方法里面的局部变量是不存在并发问题的,具体为什么呢?我们先来了解一下这些基础知识。局部变量的作用域是方法内部的,当方法执行完了,局部变量也就销毁了,也就是说局部变量应该是和方法同生共死的。Java中的方法是如何调用的?当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。从上面我们可以得出:方法的调用就是压栈和出栈的过程,而在Java中的方法的局部变量又是存储在栈帧中,所以我们用下面的示意图帮助大家理解一下说了那么多,我们还没有解释局部变量为啥不会产生并发问题,以上,我们知道了,方法的调用是压栈和出栈(栈帧)的过程,局部变量又存储在栈帧中。那么我们的线程和调用栈又有什么关系呢,答案是:每个线程都有自己独立的调用栈到现在,相信大家都已经明白了,局部变量之所以不存在并发问题,是因为,每个线程都有自己的调用栈,局部变量都保存在线程各自的调用栈里面,没有共享,自然就不存在并发问题。欢迎大家关注公众号:小白程序之路(whiteontheroad),第一时间获取最新信息!!!

April 1, 2019 · 1 min · jiezi

[Java并发]2,入门:并发编程Bug的源头

之前我们说了:1,可见性2,原子性3,有序性3个并发BUG的之源,这三个也是编程领域的共性问题。Java诞生之处就支持多线程,所以自然有解决这些问题的办法,而且在编程语言领域处于领先地位。理解Java解决并发问题的方案,对于其他语言的解决方案也有触类旁通的效果。什么是Java内存模型我们已经知道了,导致可见性的原因是缓存,导致有序性的问题是编译优化。那解决问题的办法就是直接禁用 缓存和编译优化。但是直接不去使用这些是不行了,性能无法提升。所以合理的方案是 按需禁用缓存和编译优化。如何做到“按需禁用”,只有编写代码的程序员自己知道,所以程序需要给程序员按需禁用和编译优化的方法才行。Java的内存模型如果站在程序员的角度,可以理解为,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile,synchronized和final三个关键字段。以及六项 Happens-Before 规则。使用volatile的困惑volatile 关键字并不是 Java 语言特有的,C语言也有,它的原始意义就是禁用CPU缓存。例如,我们声明一个volatile变量 ,volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。看起来语义很明确,实际情况比较困惑。看下以下代码:class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里 x 会是多少呢? } }}直觉上看,这里的X应该是42,那实际应该是多少呢?这个要看Java的版本,如果在低于 1.5 版本上运行,x 可能是42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。分析一下,为什么 1.5 以前的版本会出现 x = 0 的情况呢?因为变量 x 可能被 CPU 缓存而导致可见性问题。这个问题在 1.5 版本已经被圆满解决了。Java 内存模型在 1.5 版本对 volatile 语义进行了增强。怎么增强的呢?答案是一项 Happens-Before 规则。Happens-Before 规则这里直接给出定义:Happens-Before :前面一个操作的结果对后续操作是可见的。所以比较正式的说法是:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。看一看Java内存模型定义了哪些重要的Happens-Before规则1,程序的顺序性规则这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这还是比较容易理解的,比如刚才那段示例代码,按照程序的顺序,第 6 行代码 “x = 42;” Happens-Before 于第 7 行代码 “v = true;”,这就是规则 1 的内容,也比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。2,volatile 变量规则2. volatile 变量规则这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。3,传递性4,管程中锁的规则5,线程 start() 规则6,线程 join() 规则参考:Java内存模型 ...

March 18, 2019 · 1 min · jiezi