JavaGuide知识点整理——并发进阶知识点(上)

源代码 2024-9-7 03:05:07 83 0 来自 中国
synchronized关键字

说一说对synchronized关键字的相识

synchronized关键字办理的是多线程之间访问资源的同步性。synchronized关键字可以包管被它修饰的方法大概代码块在恣意时刻只能有一个线程实行。
别的在java早期版本中,synchronized属于重量级锁,服从低下。
由于监视器锁是依赖于底层的使用体系的Mutex Lock来实现的,java的线程是映射到使用体系的原生线程之上的。如果要挂起大概唤醒一个线程,都必要使用体系帮助完成,而使用体系实现线程之间的切换必要从用户态转换到内核态,这个状态之间的转换都必要相对比力长的时间,时间本钱相对较高。
光荣的是在java6之后java官方对jvm层面临synchronized较大优化,以是如今synchronized锁服从优化的也不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁,顺应性自旋锁,锁消除,锁粗化,方向锁,轻量级锁等技能来淘汰锁使用的开销。
以是你会发现如今的话,岂论是各种开原框架照旧JDK源码都大量使用了synchronized关键字。
说说是怎么使用synchronized关键字的

synchronized关键字最主要的三种使用方式:

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要得到当前对象实例的锁。
  • 修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要得到当前class的锁。由于静态成员不属于任何一个实例对象,是类成员(static修饰的是静态资源,不管new了多少个对象,只有一份)。以是,如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B必要调用这个实例对象所属的类的静态synchronized方法,是答应的,不会发生互斥现象。由于访问静态synchronized方法占用的锁是类的锁,而访问非静态synchronized方法占用的锁是当前实例对象的锁。
  • 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object)体现进入同步代码块前要得到给定对象的锁。synchronized(类.class)体现进入同步代码块前要获适当前class的锁。
总结:

  • synchronized关键字加到static静态方法和synchronized(class)代码块上都是给Class类上锁。
  • synchronized关键字加到实例方法上是给对象实例上锁。
  • 只管不要用synchronized(String str),由于JVM中,字符串常量池具有缓存功能。
下面说一个常见的 双重检测锁机制实现对象单例(线程安全)
public class Singleton {    private volatile static Singleton uniqueInstance;    private Singleton() {    }    public  static Singleton getUniqueInstance() {       //先判定对象是否已经实例过,没有实例化过才进入加锁代码        if (uniqueInstance == null) {            //类对象加锁            synchronized (Singleton.class) {                if (uniqueInstance == null) {                    uniqueInstance = new Singleton();                }            }        }        return uniqueInstance;    }}必要留意的是上面的uniqueInstance 必要采取volatile关键字修饰。
这里必要的不是volatile的可见性,而是防止指令重排。
本质上new XXX()是分为三步的

  • 为实例对象分配内存空间
  • 初始化实例对象
  • 将对象指向分配的内存地址
但是由于jvm大概指令重排,也就是1,2,3大概实际上是1,3,2实行的。指令重排单线程下不会出题目,但是多线程的话大概在1,3实行完有线程调用这个对象,也就是实际上固然对象还没初始化,但是由于不为空但是被调用了。就会出题目。
构造方法可以使用synchronized关键字修饰么?

构造方法不能使用synchronized关键字修饰。
由于构造方法本身就属于线程安全的,不存在同步的构造方法一说。
讲一下synchronized关键字的底层原理

synchronized关键字底层原理属于JVM层面。
synchronized修饰同步语法块的实现主要是实用monitorenter和monitorexit指令。此中monitorenter指令指向同步代码块开始的位置,monitorexit指令则指明同步代码块竣事的位置。
当实行monitorenter指令时,线程试图获取锁也就是获取对象监视器monitor的持有权。
在java假造机(HotSpot)中,monitor是基于C++实现的,由 ObjectMonitor实现,每个对象中都内置了一个ObjectMonitor对象。
别的,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块大概方法中才可以调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
在实行monitorenter时,会实行获取对象的锁,如果锁的计数器为0则体现锁可以被获取,获取后锁的计数器设为1.也就是加1。


对象锁的拥有者线程才可以实行monitorexit指令来开释锁。在实行monitorexit指令后,将锁计数器设置为0,体现锁开释,其他线程可以实行获取该锁了。

2.png
如果获取对象锁失败,那么当火线程就要壅闭等待,直到锁被别的一个线程开释为止。synchronized修饰的方法并没有monitorenter和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识、该标识指明白该方法是一个同步方法,JVM通过该访问标识来辨别一个方法是否是同步方法,从而实行相应的同步调用。
如果是实例方法,JVM会实行获取实例对象的锁,如果是静态方法,JVM会实行获取当前class的锁。
不外两者的本质都是对对象监视器monitor的获取。
JDK1.6之后的synchronized关键字底层做了哪些优化?

jdk1.6对锁的实现引入了大量的优化,如方向锁,轻量级锁,自旋锁,顺应性自旋锁,锁消除,锁粗化等技能来淘汰锁使用的开销。
锁主要存在四种状态,依次是无锁状态,方向锁状态,轻量级锁状态,重量级锁状态。他们会随着竞争的激烈而渐渐升级。留意锁可以升级不可以降级。这种计谋是为了进步得到锁和开释锁的服从。
大概简述一下锁升级
起首无锁状态就不说了,我们说下方向锁:
方向锁是针对于一个线程而言的,线程获取锁后就不会再有解锁等使用了。如许可以省去许多开销。如果有两个线程来竞争该锁的话,那么方向锁就失效了。进而升级成轻量级锁了。
然后具体的实现分为方向锁获取锁:

  • 方向锁标志是未方向状态,使用CAS将MarkWord 中的线程id设置为本身的线程id

    • 如果乐成,获取方向锁乐成
    • 如果失败,举行锁升级

  • 方向锁标志是已方向状态

    • MarkWord 中线程id是本身的线程id,获取锁乐成
    • MarkWord 中线程id不是本身的线程id,举行锁升级

方向锁升级必要举行方向锁取消(条件是要是方向锁)

  • MarkWord 中指向的线程不存活

    • 答应重新方向
    • 不答应重方向,变为无锁状态

  • MarkWord 中线程存活

    • 线程id指向的线程仍拥有锁,则升级为轻量级锁
    • 线程id指向的线程不拥有锁,答应方向则退回到可方向但未方向的状态,不答应方向就变为无锁状态。

这个方向锁可以用生存中的一个例子来明白:如果有个图书馆可以借书。第一个方案是任意谁都可以拿,只要拿的时间留个姓名就行了。这种环境也不必要管理员费心,很省时间。假设书还在图书馆,这个时间我们可以明白这个书是可方向但未方向的状态。
然后如果或人以为这本书不错,留个姓名,把书拿走了。  我们可以以为这个人获取方向锁乐成了(这里有个状态,就是这个人有点毛病,整天把这本书带来图书馆看,但是书着实照旧在这个人名下的,以是不必要再次去借可以直接看书)。
重点是又来了一个人以为这本书不错,想要借这本书。此时是有两种大概的:

  • 书在图书馆。
  • 书在第一个人家。
    如果是第一种大概的话,就分析之前谁各人看完了还返来了,如果书答应反复借的话(答应重方向),那么着实第二个人是可以借到这本书的。也就是第二个人获取到了方向锁。如果这本书不答应反复借的话,那么这个事就得升级处置处罚了,由于管理员看这本书太抢手了,放养不可,升级成轻量级锁。
    如今说第二种大概,就是当第二个人来借这本书的时间,这本书在第一个人家里。遇到这种环境管理员就不能再图省事不管了,他得品级一个人还书的时间再关照第二个人来借。我们可以以为第二个人酿成了等关照的状态,随时等待书返来。这也就是锁升级成轻量级锁,第二个人在自旋请求得到锁。
然后轻量级锁膨胀成重量级锁就比力好明白了。简朴一句话:轻量级锁是线程自旋等待获取锁,当自选次数到达边界值就会膨胀成重量级锁。就是等的时间太久,不绝在那自旋卡着cpu不是那么回事,以是用重量级锁使用使用体系互斥量来实现的传统锁。
重量级锁线程不自旋了,直接堵塞状态,不斲丧cpu。
必要留意的是锁膨胀成重量级锁后,就不会退回到轻量级了。
synchronized和ReentrantLock的区别


  • 两者都是可重入锁
    可重入锁值的是本身可以再次获取本身的内部锁。同一个线程获取了某个对象的锁,此时锁还没开释的话,可以再次获取到这个对象的锁。如果是不可重入的锁就会造成死锁。
    同一个线程每次获取锁,锁的计数器上都加一,以是要等锁的计数器降落为0的时间才华开释锁。
  • synchronized依赖于JVM,而ReentrantLock依赖于API
    synchronized是依赖于JVM实现的,上面也讲了原理。甚至1.6的那么多优化都是在假造机层面实现的,并没有直接袒露给我们。而ReentrantLock是JDK层面实现的,以是我们可以检察它的源码,看它是怎样实现的。
  • ReentrantLock比synchronized增长了一些高级功能
    相比于synchronized,ReentrantLock有一些高级功能,主要有三点:

    • 等待可制止:ReentrantLock提供了一种可以制止等待锁的线程的机制。也就是说正在等待的线程可以选择放弃等待。通过lock.lockInterruptibly()来实现。
    • 可实现公平锁:ReentrantLock可以指定公平锁还好坏公平锁,而synchronized只能好坏公平锁。
    • 可以实现选择性关照(锁可以绑定多个条件):synchronized关键字可以和wait/notify/notifyAll等方法连合实现等待关照机制。但是ReentrantLock可以借助Condition接口和newCondition()方法实现指定关照。

如果想实现上述三点,那么选择ReentrantLock是一个不错的选择, 性能不是选择尺度。
volatile关键字

CPU缓存模子

为什么要弄一个CPU高速缓存呢?
类比我们开发项目时用Redis缓存办理步调处置处罚速度和访问通例关系型数据库速度不对等的关系。cpu缓存是为了办理CPU处置处罚速度和内存处置处罚速度不对等的题目。
我们可以把内存看作外存的高速缓存,步调运行的时间我们把外存的数据复制到内存,由于内存的处置处罚速度远远高于外存,如许进步了处置处罚速度。
总结:CPU Cache缓存的是内存数据用于办理CPU处置处罚速度和内存不匹配的题目。内存缓存的是硬盘数据用于办理硬盘访问速度过慢的题目。


CPU Cache的工作方式:
先复制一份数据到CPU Cache中,当CPU必要用到的时间可以直接从CPU Cache中读取数据,当运算完成后,再将运算得到的数据写回Main Memory中。但是如许会存在内存缓存不同等性题目。比如我实行一个i++使用,同事两个线程实行大概会得到2,准确结果应该是3.
CPU为了办理内存缓存不同等性题目可以通过定制缓存同等协议大概其他本领来办理。
JMM(Java内存模子)

java内存模子抽象了线程和主内存之间的关系。就比如说线程之间的共享变量必须存储在主内存中。Java内存模子主要目的是为了屏蔽体系和硬件的差别。克制一套代码在不同的平台下产生的结果不同等。
JDK1.2之前,java的内存模子实现总是从主内存读取变量,是不必要特殊留意的。
但是在当前的java内存模子下,线程可以把变量生存在本地内存中,而不是直接在主内存中读写。如许可以造成一个线程在主内存中修改了一个变量的值,而另一个线程还在用它在寄存器中的变量值的拷贝,造成数据的不同等。
要办理这个不同等题目,就必要把变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都要到主内存中举行读取。
以是volatile关键字除了防止指令重排,尚有一个紧张的作用就是包管变量的可见性。
并发编程的三个紧张特性


  • 原子性:一次使用大概多次使用,要么所有的使用全部都实行而且不受任何因素的干扰而制止。要么都不实行。synchronized可以包管代码片断的原子性。
  • 可见性:当一个线程对共享变量举行了修改,那么别的的线程都是立即可以看到修改后的最新值。volatile关键字可以包管共享变量的可见性。
  • 有序性:代码在实行的过程中的先后序次。由于java在编译期以及运行期间的优化。代码的实行序次未必是编写代码时间的序次。volatile关键字可以克制指令重排优化。
synchronized关键字和volatile关键字的区别

synchronized关键字和volatile关键字是两个互补的存在,而不是对立。

  • volatile关键字是线程同步的轻量级实现,以是volatile性能比synchronized关键字要好。但是volatile关键字只能修饰变量,而synchronized可以修饰方法和代码块
  • volatile关键字可以包管数据的可见性,但是不能包管数据的原子性。synchronized关键字两者都可以包管。
  • volatile关键字主要用于办理变量在多线程之间的可见性。而synchronized关键字办理的是多个线程之间访问资源的同步性。
ThreadLocal

ThreadLocal简介

通常环境下,我们创建的变量是可以被任何一个线程访问而且修改的。如果想要实现每一个线程都有本身的专属本地变量该怎样办理呢?JDK中提供了ThreadLocal类正是为了办理如许的题目。ThreadLocal类主要办理的就是让每个线程绑定本身的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用get()和set()方法来获取默认值大概将其值更改为当火线程所存的副本的值,从而克制了线程安全题目。
下面是ThreadLocal使用的demo:
public class Test {    public static final ThreadLocal<String> str = ThreadLocal.withInitial(()->"初始数据!");    public static void main(String[] args) throws Exception {        for(int i  = 0;i<5;i++){            Thread thread = new Thread(()->{                System.out.println(DateUtils.format(new Date(),"yyyy-MM-dd HH:mm:ss") +str.get());                str.set(DateUtils.format(new Date(),"yyyy-MM-dd HH:mm:ss")+"被线程"+Thread.currentThread().getName()+                    "修改了!");                System.out.println(str.get());            });            Thread.sleep(1000l);            thread.start();        }    }}
从运行结果可以看出来,前面的线程显着都已经改变了字符串的值,但是背面获取的照旧初始值。
ThreadLocal原理

这个要从Thread类源码入手:

6.png 从截图的代码中可以看出Thread类中有个threadLocals 变量,它是ThreadLocalMap范例的,我们可以把ThreadLocalMap明白为ThreadLocal类实现的定制化的HashMap,默认环境下它是null,只有当火线程调用ThreadLocal的set大概get方法时才创建他们。实际上调用这两个方法的时间,我们调用的是ThreadLocalMap类对应的get/set方法。
下面是get/set的源码:
    public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);    }   ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }留意看这个set着实就是获取当火线程的threadLocals、然后如果为null 的话分析这个线程没调用过ThreadLocal 类的get/set方法,以是初始化这个map,而且把这次修改后的值存进去。如果之前有的话直接修改这个ThreadLocal 的值。
然后get源码就更简朴了:
    public T get() {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null) {            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")                T result = (T)e.value;                return result;            }        }        return setInitialValue();    }从map里获取这个对象对应的值,没有的话返回null。
通过上面的源码我们可以得出结论:终极的变量是放在了当火线程的ThreadLocalMap中。并不是存在ThreadLocal上。ThreadLocal可以明白为是ThreadLocalMap的封装,转达了变量值。
ThreadLocal通过Thread.currentThread()获取当火线程对象,直接通过getMap访问该线程的ThreadLocalMap对象。
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key,Object对象为value的键值对。
ThreadLocal内存走漏题目

ThreadLocalMap中使用key为ThreadLocal的弱引用。而value是强引用。以是如果ThreadLocal没有被外部强引用的环境下,在垃圾采取的时间,key会被清算掉,而value不会被清算掉。如许一样,ThreadLocalMap中就会出现key为null的Entry.
如果不做任何步调的话,value永世无法被GC采取。这个时间就会产生内存走漏。
ThreadLocalMap已经考虑了这种环境,以是最好的办法是使用完这个ThreadLocal方法后,调用remove()方法。


必要留意的是,这个remove方法固然是ThreadLocal的,但是remove见效是指删除当火线程中这个ThreadLocal的键值对。
本篇条记就记到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝各人工作顺顺利利,生存健康健康~!
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-19 04:32, Processed in 0.208055 second(s), 35 queries.© 2003-2025 cbk Team.

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