前言

2019版多线程与高并发(马士兵)

各位同学,大家好,这是首次使用比较口语化的文字形成一本书,其实也不知道效果如何,希望各位收到书本后能够多提意见和建议。同时也请大家体谅,由于时间关系和忙碌程度,暂时只能总结成为口语化的形式,后面时间充裕了,将会以书面语言的方式进行重新更新。

第一节:线程的基本概念

首先给大家交代一下我们2019年这个版本给大家讲哪些内容,这个版本主要之中在多线程和高并发这两大块,这两大块儿是现在面试问的越来越多,也是相对一个初级的程序员向中高级迈进的必须要踏过的一个坎儿。

多线程与高并发大概讲六大块,

第一:基本的概念,从什么是线程开始

第二:JUC同步工具,就是各种同步锁

第三:同步容器

第四:线程池

第五:高频面试加分项的一些面试用的东西,包括纤程

第六:Disruptor,不知道有多少同学听说过这个框架的,这个框架它也是一个MQ框架(Message Queue)叫做消息队列,消息队列非常多,后面还会给大家讲Kafka、RabbitMQ,Redis等这些都是消息队列。Disruptor是目前大家公认的在单机环境上效率最高的、性能最快的MQ。

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(1)

我们先说一下为什么要讲多线程和高并发?

原因是,你想拿到一个更高的薪水,在面试的时候呈现出了两个方向的现象:

第一个是上天

项目经验

高并发 缓存 大流量 大数据量的架构设计

第二个是入地

各种基础算法,各种基础的数据结构

JVM OS 线程 IO等内容

多线程和高并发,就是入地里面的内容。

基本概念

我们先从线程的基本概念开始,给大家复习一下,不知道有多少同学是基础不太好,说什么是线程都不知道的,如果这样的话,花时间去补初级内容的课。

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(2)

什么是叫一个进程? 什么叫一个线程?

Program app ->QQ.exe

进程:做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,这个程序是一个静态的概念,它被扔在硬盘上也没人理他,但是当你双击它,弹出一个界面输入账号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念。

线程:作为一个进程里面最小的执行单元它就叫一个线程,用简单的话讲一个程序里不同的执行路径就叫做一个线程

示例:什么叫做线程

package com.mashibing.juc.c_000; import Java.util.concurrent.TimeUnit; public class T01_WhatIsThread {private static class T1 extends Thread { @Override public void run() { for(int i=0; i<10; i ) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("T1"); } } } public static void main(String[] args) { //new T1().run(); new T1().start(); for(int i=0; i<10; i ) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main"); } } }

观察上面程序的数据结果,你会看到字符串“T1”和“Main”的交替输出,这就是程序中有两条不同的执行路径在交叉执行,这就是直观概念上的线程,概念性的东西,理解就好,没有必要咬文嚼字的去背文字的定义。

创建线程的几种方式

package com.mashibing.juc.c_000; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; public class T02_HowToCreateThread { static class MyThread extends Thread { @Override public void run() { System.out.println("Hello MyThread!"); } } static class MyRun implements Runnable { @Override public void run() { System.out.println("Hello MyRun!"); } }static class MyCall implements Callable<String> { @Override public String call() { System.out.println("Hello MyCall"); return "success"; } } //启动线程的5种方式 public static void main(String[] args) { new MyThread().start(); new Thread(new MyRun()).start(); new Thread(()->{ System.out.println("Hello Lambda!"); }).start(); Thread t = new Thread(new FutureTask<String>(new MyCall())); t.start(); ExecutorService service = Executors.newCachedThreadPool(); service.execute(()->{ System.out.println("Hello ThreadPool"); }); service.shutdown(); } }

分享一道面试题

请你告诉我启动线程的三种方式 ?

你说第一个:new Thread().start(); 第二个: new Thread(Runnable).start() 这没问题 ;那第三个呢,要回答线程池也是用的这两种之一,他这么问有些吹毛求疵的意思,你就可以说通过线程池也可以启动一个新的线程 3:Executors.newCachedThreadPool()或者FutureTask Callable

我们来认识几个线程的方法

package com.mashibing.juc.c_000; public class T03_Sleep_Yield_Join { public static void main(String[] args) { //testSleep(); //testYield(); testJoin(); } /*Sleep,意思就是睡眠,当前线程暂停一段时间让给别的线程去运行。Sleep是怎么复活的?由 你的睡眠时间而定,等睡眠到规定的时间自动复活*/ static void testSleep() { new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("A" i); try { Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } /*Yield,就是当前线程正在执行的时候停止下来进入等待队列,回到等待队列里在系统的调度算 法里头呢还是依然有可能把你刚回去的这个线程拿回来继续执行,当然,更大的可能性是把原来等待的那些拿 出一个来执行,所以yield的意思是我让出一下CPU,后面你们能不能抢到那我不管*/ static void testYield() { new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("A" i); if(i == 0) Thread.yield(); } }).start(); new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("------------B" i); if(i == 0) Thread.yield(); } }).start(); } /*join, 意思就是在自己当前线程加入你调用Join的线程(),本线程等待。等调用的线程运行 完了,自己再去执行。t1和t2两个线程,在t1的某个点上调用了t2.join,它会跑到t2去运行,t1等待t2运 行完毕继续t1运行(自己join自己没有意义) */ static void testJoin() { Thread t1 = new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("A" i); try { Thread.sleep(500); //TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(()->{ try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } for(int i=0; i<100; i ) { System.out.println("A" i); try { Thread.sleep(500); //TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) { e.printStackTrace();} } }); t1.start(); t2.start(); } }

线程状态

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(3)

常见的线程状态有六种:

当我们new一个线程时,还没有调用start()该线程处于新建状态

线程对象调用 start()方法时候,他会被线程调度器来执行,也就是交给操作系统来执行了,那么操作系统来执行的时候,这整个的状态叫Runnable,Runnable内部有两个状态(1)Ready就绪状

/(2)Running运行状态。就绪状态是说扔到CPU的等待队列里面去排队等待CPU运行,等真正扔到CPU上去运行的时候才叫Running运行状态。(调用yiled时候会从Running状态跑到Ready状态去,线程配调度器选中执行的时候又从Ready状态跑到Running状态去)

如果你线程顺利的执行完了就会进去(3)Teminated结束状态,(需要注意Teminated完了之后还可不可以回到new状态再调用start?这是不行的,完了这就是结束了)

在Runnable这个状态里头还有其他一些状态的变迁(4)TimedWaiting等待(5)Waiting等待

(6)Blocked阻塞,在同步代码块的情况就下没得到锁就会阻塞状态,获得锁的时候是就绪状态运行。在运行的时候如果调用了o.wait()、t.join()、LockSupport.park()进入Waiting状态,调用o.notify()、o.notififiAll()、LockSupport.unpark()就又回到Running状态。TimedWaiting按照时间等待,等时间结束自己就回去了,Thread.sleep(time)、o.wait(time)、t.jion(time)、LockSupport.parkNanos()、LockSupport.parkUntil()这些都是关于时间等待的方法。

问题1:哪些是JVM管理的?哪些是操作系统管理的?

上面这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以呢,那个是操作系统和那个是JVM他俩分不开,JVM是跑在操作系统上的一个普通程序

问题2:线程什么状态时候会被挂起?挂起是否也是一个状态?Running的时候,在一个cpu上会跑很多个线程,cpu会隔一段时间执行这个线程一下,在隔一段时间执行那个线程一下,这个是cpu内部的一个调度,把这个状态线程扔出去,从running扔回去就叫线程被挂起,cpu控制它。

来看一下ThraedState这段代码

package com.mashibing.juc.c_000; public class T04_ThreadState { static class MyThread extends Thread { @Override public void run() { System.out.println(this.getState()); for(int i=0; i<10; i ) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } } } public static void main(String[] args) { Thread t = new MyThread(); //怎么样得到这个线程的状态呢?就是通过getState()这个方法 System.out.println(t.getState());//他是一个new状态 t.start();//到这start完了之后呢是Runnable的状态 try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } //然后join之后,结束了是一个Timenated状态 System.out.println(t.getState()); } }

synchronized

下面我们来讲synchronized关键字,有不少同学已经耳熟能详了,不过作为复习还是要复习一下。第一个是多个线程去访问同一个资源的时候对这个资源上锁。

为什么要上锁呢?访问某一段代码或者某临界资源的时候是需要有一把锁的概念在这儿的。

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(4)

比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1啊,如果两个线程共同访问的时候,第一个线程一读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候而第二个线程读到了它还是0,加1在写回去,本来加了两次,但还是1,那么我们在对这个数字递增的过程当中就上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它计算,我必须加完1收释放锁,其他线程才能对它继续加。

实质上,这把锁并不是对数字进行锁定的, 你可以任意指定,想锁谁就锁谁。

我第一个小程序是这么写的 ,如果说你想上了把锁之后才能对count进行减减访问,你可以new一个Object,所以这里锁定就是o,当我拿到这把锁的时候才能执行这段代码。是锁定的某一个对象,synchronized有一个锁升级的概念,我们一会儿会讲到

/** *synchronized关键字 *对某个对象加锁 *@author mashibing */ package com.mashibing.juc.c_001; public class T { private int count = 10; private Object o = new Object(); public void m() { synchronized(o) { //任何线程要想执行下面的代码,必须先拿到o的锁 count--; System.out.println(Thread.currentThread().getName() " count = " count); } } }

我们来谈一下synchronized它的一些特性。如果说你每次都定义个一个锁的对象Object o 把它new出来那加锁的时候太麻烦每次都要new一个新的对象出来,所以呢,有一个简单的方式就是

synchronized(this)锁定当前对象就行/**

* synchronized关键字 * 对某个对象加锁 * @author mashibing */ package com.mashibing.juc.c_002; public class T { private int count = 10; public void m() { synchronized(this) { ߳//任何线程想要执行那个下面的代码,必须先要拿到this的锁 count--; System.out.println(Thread.currentThread().getName() " count = " count); } } }

如果你要是锁定当前对象呢,你也可以写成如下方法。synchronized方法和synchronized(this)执行这段代码它是等值的

package com.mashibing.juc.c_003; public class T { private int count = 10; public synchronized void m() { //等同于在方法的代码执行时要synchronized(this) count--; System.out.println(Thread.currentThread().getName() " count = " count); } }

我们知道静态方法static是没有this对象的,你不需要new出一个对象来就能执行这个方法,但如果这个这个上面加一个synchronized的话就代表synchronized(T.class)。这里这个synchronized(T.class)锁的就是T类的对象

package com.mashibing.juc.c_004; public class T { private static int count = 10; public synchronized static void m() { //这里等同于synchronized(T.class) count--; System.out.println(Thread.currentThread().getName() " count = " count); } public static void mm() { synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以?count --; } } }

问题:T.class是单例的吗?

一个class load到内存它是不是单例的,想想看。一般情况下是,如果是在同一个ClassLoader空间那它一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那他一定就是单例

下面程序:很有可能读不到别的线程修改过的内容,除了这点之外count减减完了之后下面的count输出和你减完的结果不对,很容易分析:如果有一个线程把它从10减到9了,然后又有一个线程在前面一个线程还没有输出呢进来了把9又减到了8,继续输出的8,而不是9。如果你想修正它,前面第一个是在上面加volatile,改了马上就能得到。

/** * 分析一下这个程序的输出 * @author mashibing */ package com.mashibing.juc.c_005; public class T implements Runnable { private /*volatile*/ int count = 100; public /*synchronized*/ void run() { count--; System.out.println(Thread.currentThread().getName() " count = " count); } public static void main(String[] args) { T t = new T(); for(int i=0; i<100; i ) { new Thread(t, "THREAD" i).start(); } } }

另外这个之外还可以加synchronized,加了synchronized就没有必要在加volatile了,因为synchronized既保证了原子性,又保证了可见性。

//对比上一个小程序 package com.mashibing.juc.c_006; public class T implements Runnable { private int count = 10; public synchronized void run() { count--;System.out.println(Thread.currentThread().getName() " count = " count); } public static void main(String[] args) { for(int i=0; i<5; i ) { T t = new T(); new Thread(t, "THREAD" i).start(); } } }

如下代码:同步方法和非同步方法是否可以同时调用?就是我有一个synchronized的m1方法,我调用m1的时候能不能调用m2,拿大腿想一想这个是肯定可以的,线程里面访问m1的时候需要加锁,可是访问m2的时候我又不需要加锁,所以允许执行m2。

这些小实验的设计是比较考验功力的,学习线程的时候自己要多动手进行试验,任何一个理论,都可以进行验证。

/** *同步和非同步方法是否可以同时调用? * @author mashibing */ package com.mashibing.juc.c_007; public class T { public synchronized void m1() { System.out.println(Thread.currentThread().getName() " m1 start..."); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() " m1 end"); } public void m2() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() " m2 "); } public static void main(String[] args) { T t = new T(); /*new Thread(()->t.m1(), "t1").start(); new Thread(()->t.m2(), "t2").start();*/ new Thread(t::m1, "t1").start();new Thread(t::m2, "t2").start(); /* //1.8之前的写法 new Thread(new Runnable() { @Override public void run() { t.m1(); } }); */ } }

我们在来看一个synchronized应用的例子我们定义了一个class账户,有名称、余额。写方法给哪个用户设置它多少余额,读方法通过这个名字得到余额值。如果我们给写方法加锁,给读方法不加锁,你的业务允许产生这种问题吗?业务说我中间读到了一些不太好的数据也没关系,如果不允许客户读到中间不好的数据那这个就有问题。正因为我们加了锁的方法和不加锁的方法可以同时运行。

问题比如说:张三,给他设置100块钱启动了,睡了1毫秒之后呢去读它的值,然后再睡2秒再去读它的值这个时候你会看到读到的值有问题,原因是在设定的过程中this.name你中间睡了一下,这个过程当中我模拟了一个线程来读,这个时候调用的是getBalance方法,而调用这个方法的时候是不用加锁的,所以说我不需要等你整个过程执行完就可以读到你中间结果产生的内存,这个现象就叫做脏读。这问题的产生就是synchronized方法和非synchronized方法是同时运行的。解决就是把getBalance加上synchronized就可以了,如果你的业务允许脏读,就可以不用加锁,加锁之后的效率低下。

/** * 面试题:模拟银行账户 * 对业务写方法加锁 * 对业务读方法不加锁 * 这样行不行? * * 容易产生脏读问题(dirtyRead) */ package com.mashibing.juc.c_008; import java.util.concurrent.TimeUnit; public class Account { String name; double balance; public synchronized void set(String name, double balance) { this.name = name; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }this.balance = balance; } public /*synchronized*/ double getBalance(String name) { return this.balance; } public static void main(String[] args) { Account a = new Account(); new Thread(()->a.set("zhangsan", 100.0)).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); } }

再来看synchronized的另外一个属性:可重入,是synchronized必须了解的一个概念。

如果是一个同步方法调用另外一个同步方法,有一个方法加了锁,另外一个方法也需要加锁,加的是同一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁。比如说是synchronized可重入的,有一个方法m1 是synchronized有一个方法m2也是synchrionzed,m1里能不能调m2。我们m1开始的时候这个线程得到了这把锁,然后在m1里面调用m2,如果说这个时候不允许任何线程再来拿这把锁的时候就死锁了。这个时候调m2它发现是同一个线程,因为你m2也需要申请这把锁,它发现是同一个线程申请的这把锁,允许,可以没问题,这就叫可重入锁。

/** * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到 该对象的锁。 * 也就是说synchronized获得锁是可重入的 * synchronized * @author mashibing */ package com.mashibing.juc.c_009; import java.util.concurrent.TimeUnit; public class T { synchronized void m1() { System.out.println("m1 start");try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); System.out.println("m1 end"); } synchronized void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2"); } public static void main(String[] args) { new T().m1(); } }

模拟一个父类子类的概念,父类synchronized,子类调用super.m的时候必须得可重入,否则就会出问题(调用父类是同一把锁)。所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1,就是这么个概念。

package com.mashibing.juc.c_010; import java.util.concurrent.TimeUnit; public class T { synchronized void m() { System.out.println("m start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m end"); } public static void main(String[] args) { new TT().m(); } } class TT extends T { @Override synchronized void m() { System.out.println("child m start"); super.m(); System.out.println("child m end"); }}

下面再看,异常锁

看这个小程序,加了锁synchronized void m()while(true)不断执行,线程启动,count 如果等于5的时候认为的产生异常。这时候如果产生任何异常,就会出现什么情况呢? 就会被原来的那些个准备拿到这把锁的程序乱冲进来,程序乱入。这是异常的概念。

/** * 程序在执行过程中,如果出现异常,默认情况锁会被释放 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适, * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。 * 因此要非常小心的处理同步业务逻辑中的异常 * @author mashibing */ package com.mashibing.juc.c_011; import java.util.concurrent.TimeUnit; public class T { int count = 0; synchronized void m() { System.out.println(Thread.currentThread().getName() " start"); while(true) { count ; System.out.println(Thread.currentThread().getName() " count = " count); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if(count == 5) { int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行 catch,然后让循环继续 System.out.println(i); } } } public static void main(String[] args) { T t = new T(); Runnable r = new Runnable() { @Override public void run() { t.m(); } }; new Thread(r, "t1").start();try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(r, "t2").start(); } }

synchronized的底层实现

早期,jdk早期的时候,这个synchronized的底层实现是重量级的,重量级到这个synchronized都是要去找操作系统去申请锁的地步,这就会造成synchronized效率非常低,java后来越来越开始处理高并发的程序的时候,很多程序员都不满意,说这个synchrionized用的太重了,我没办法,

就要开发新的框架,不用你原生的了改进,后来的改进才有了锁升级的概念关于这个锁升级的概念,我写过一篇文章《我就是厕所所长一、二》,大家可以去找一下,公众号里也有,专门以小说的形式讲了这个锁升级到底是怎么样的一个概念这个锁升级的概念呢,是这样的,原来呢都要去找操作系统,要找内核去申请这把锁,到后期做了对synchronized的一些改进,他的效率比原来要改变了不少,改进的地方。当我们使用synchronized的时候HotSpot的实现是这样的:上来之后第一个去访问某把锁的线程 比如sync (Object) ,来了之后先在这个Object的头上面markword记录这个线程。(如果只有第一个线程访问的时候实际上是没有给这个Object加锁的,在内部实现的时候,只是记录这个线程的ID(偏向锁))。

偏向锁如果有线程争用的话,就升级为自旋锁,概念就是(有一个哥们儿在蹲马桶 ,另外来了一个哥们,他就在旁边儿等着,他不会跑到cpu的就绪队列里去,而就在这等着占用cpu,用一个while的循环在这儿转圈玩儿, 很多圈之后不行的话就再一次进行升级)。

自旋锁转圈十次之后,升级为重量级锁,重量级锁就是去操作系统那里去申请资源。这是一个锁升级的过程。

可以参考:

synchronized的使用

在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对 synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的 性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。通过 synchronized关键字来修饰在inc的方法上,看看执行结果。(可以自己尝试将synchronizrd去掉,看看结果得到的是不是1000)

public class Demo{ private static int count=0; public static void inc(){ synchronized (Demo.class) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count ; } } public static void main(String[] args) throws InterruptedException { for(int i=0;i<1000;i ){ new Thread(()->Demo.inc()).start(); } Thread.sleep(3000); System.out.println("运行结果" count); } }

synchronized的三种应用方式

synchronized有三种方式来加锁,分别是

修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized括号后面的对象

synchronized扩号后面的对象是一把锁,在java中任意一个对象都可以成为锁。简单来说,我们把object比喻是一 个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的

synchronized的字节码指令

通过javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令,他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。 monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(5)

这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有 一个线程获取到由synchronized所保护对象的监视器。

线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行 monitorexit 就是释放monitor的所有权。

对象在内存中的布局

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。其中对象头包含对象标记和类元信息两部分,Java对象头是实现 synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

Mawrk Word

Mark Word(对象标记)用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节, 也就是32bit)

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(6)

Monitor

什么是Monitor?

1.Monitor是一种用来实现同步的工具

2.与每个java对象相关联,所有的 Java 对象是天生携带 monitor

3.Monitor是实现Sychronized(内置锁)的基础

对象的监视器(monitor)由ObjectMonitor对象实现(C ),其跟同步相关的数据结构如下:

ObjectMonitor() { _count = 0; //用来记录该对象被线程获取锁的次数 _waiters = 0; _recursions = 0; //锁的重入次数 _owner = NULL; //指向持有ObjectMonitor对象的线程 _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 }

synchronized的锁升级和获取过程

首先来了解相关锁的概念:

自旋锁(CAS):让不满足条件的线程等待一会看能不能获得锁,通过占用处理器的时间来避免线程切换带来的开销。自旋等待的时间或次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

偏向锁:大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,偏向锁是一个可重入的锁。如果锁对象头的Mark Word里存储着指向当前线程的偏向锁,无需重新进行CAS操作来加锁和解锁。当有其他线程尝试竞争偏向锁时,持有偏向锁的线程(不处于活动状态)才会释放锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定进而升级为轻量级锁。

轻量级锁:减少无实际竞争情况下,使用重量级锁产生的性能消耗。JVM会现在当前线程的栈桢中创建用于存储锁记录的空间 LockRecord,将对象头中的 Mark Word 复制到 LockRecord 中并将 LockRecord 中的 Owner 指针指向锁对象。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,失败则表示其他线程竞争锁当前线程则尝试使用自旋的方式获取锁。自旋获取锁失败则锁膨胀升级为重量级锁。

重量级锁:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(7)

从网上找来的一张图,完美诠释了synchronized锁的升级过程。

Synchronized 结合 Java Object 对象中的 wait,notify,notifyAll

前面我们在讲 synchronized 的时候,发现被阻塞的线程什 么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢?我们就需要借助一个信号机制:在 Object 对象中,提供了wait/notify/notifyall,可以用于控制线程的状态。

wait/notify/notifyall 基本概念

wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态。

notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤醒某个竞争该对象锁的线程 X。 线程 A synchronized 代码执行结束并且释放了锁之后,线程 X 直接获得对象锁权限,其他竞争线程继续等待(即使线程 X 同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的 notify ,notifyAll 被调用)。

notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程 A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权限。

需要注意的是:三个方法都必须在 synchronized 同步关键字所限定的作用域中调用,否则会报错 java.lang.IllegalMonitorStateException 。意思是因为没有同步,所以线程对对象锁的状态是不确定的,不能调用这些方法。另外,通过同步机制来确保线程从 wait 方法返回时能够感知到 notify 线程对变量做出的修改。

常见面试题:wait/notify/notifyall为什么需要在synchronized里面?

1.wait 方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以 wait 必须要获得一个监视器锁。

2.对于 notify 来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

3.每个对象可能有多个线程调用wait方法,所以需要有一个等待队列存储这些阻塞线程。这个等待队列应该与这个对象绑定,在调用wait和notify方法时也会存在线程安全问题所以需要一个锁来保证线程安全。

wait/notify 的基本原理

如何才能真正搞懂java多线程(清华扫地僧20年功力讲解java多线程与高并发)(8)

需要注意并不是CAS的效率就一定比系统锁要高,这个要区分实际情况:

执行时间短(加锁代码),线程数少,用自旋

执行时间长,线程数多,用系统锁

关于效率方面的内容如果暂时不能理解的,等讲到CAS锁的时候再说。

内容回顾

-锁的是对象不是代码

-this XX.class

-锁定方法 非锁定方法 同时执行

-锁升级 偏向锁 记录这个线程的ID

自旋锁 如果线层争用,就升级为自旋锁(线程数量少)

重量级锁 10次(线程数量多)

本文给大家介绍的是线程的基本概念,喜欢的朋友可以转发关注一下小编!!

明天给大家更新volatile与CAS的内容,希望大家能够喜欢!!!!!

,