JAVA中的并发知识介绍

目录

一、关于并发的基本概念

二、关于线程的介绍

1.线程的两种创建方式

2 线程的同步方法

2.1 使用同步代码块解决多线程安全问题

 2.2 使用同步方法解决多线程安全问题

3 JAVA线程中的几种状态

3.1 JAVA中sleep()和wait()的区别

3.2 JAVA中run()和start()的区别

三、线程池的介绍

1.关于ThreadPoolExecutor类的介绍

1.1ThreadPoolExecutor类的构造方法

1.2 关于线程池的使用

2.线程池的execute()方法

四、JAVA中锁的分类

1. 关于ReentrantLock的介绍

2. 关于AQS的介绍

2.1 AQS的原理概述

2.2 AQS对资源的共享模式有哪些

3. AQS 底层使⽤了模板⽅法模式,AQS重写的方法的介绍

3.1 模板方法模式介绍

3.2 AQS需要重写的方法

五、ConcurrentHashMap的作用和做法 

1. ConcurrentHashMap的基本原理

2. ConcurrentHashMap的get()操作

3. ConcurrentHashMap的put()操作

六、关于synchronized的介绍

1. synchronized关键字

1.1 synchronized关键字的底层原理

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

七、Volatile关键字

1. volatile的作用

1.1 保证变量的可见性

1.2 保证赋值操作的原子性

1.3 禁止指令重排

2. volatile的实现机制

2.1 什么是缓存一致性协议

2.2 什么是内存屏障

2.2.1 内存屏障的由来

2.2.2 内存屏障的实现思路

八、CAS算法和AQS同步器的介绍

1.CAS算法

2. AQS同步器

九、JAVA中原子类的介绍

1. 原子类的分类

2. Atomic的原理

3.synchronized、lock、volatile的区别

3.1 synchronized和volatile之间的区别

3.2 synchronized和lock区别

十、ThreadLocal的使用

1.ThreadLocal的简介

2. ThreadLocal的方法


一、关于并发的基本概念

并发情况主要会引出三个基本概念,分别是原子性、可见性、有序性三个基本概念,接下来我们将分别介绍这三个基本概念。

原子性:一个或者多个操作(赋值也好,运算也好)不能被线程调度打断,要么一次性执行完,要么就不执行。

可见性:现代处理器是多核心的,或者多CPU的,但是主存(通常意义上的操作系统内存,或者物理内存)却是在CPU之间共享的。多核心处理的优势在于,从机器级别支持多线程并发,而且为了弥补主存与CPU核心之间的速度差异,便有了CPU核心缓存,因此,每个CPU核心(或者说每个线程)是有独立的内存的。这样就带来了可见性的问题,同一个变量c,A线程操作的是c在A线程的缓存中的值,B操作的是c在B的缓存中值,也就是说最新的变量的值对于其他线程是不可见的,这就有了可见性的问题。

有序性:对于单线程来说,程序的执行顺序就是按照代码的书写顺序,从上到下,从左到右(分号分隔写在同一行时)。但是多线程情况就不一定了,线程调度器随时可能打断某一程,执行其他线程。这就导致了,程序并不是按照预期的顺序执行的,导致结果跟预期不一致。 注意:这里的顺序,并不是严格的指令执行的顺序,而且从结果正确性的角度来看的。

二、关于线程的介绍

1.线程的两种创建方式

第一种创建方式是通过直接集成Thread类,重写run方法进行创建线程。具体的如下图所示:

 我们使用自己定义的线程类可以创建3个线程,观察线程共享资源时出现的并发问题(多线性并发问题的解决,我们将在下面的篇幅进行讨论)如下图所示:

 

 创建线程的第二种方式是通过实现java.lang.Runnable接口实现线程,具体的创建过程如下所示:

测试情况如下图所示:

2 线程的同步方法

由线程的测试过程,我们可以看到三个线程在访问一个共享资源时,会导致数据错误,即上面显示的。我们可以使用同步代码块和同步方法两种方式进行线程的同步设置。

2.1 使用同步代码块解决多线程安全问题

如下图所示,我们使用synchronized关键字对被需要同步的代码块进行同步控制,我们这里需要注意的是,synchronized关键字使用的锁(即object)对于多个线程来说是同一个。使用的对象锁可以有三种规格,第一种是是实例级别对象锁,第二种是类型级对象锁(即我们下面使用的这一种),第三种是进程(虚拟机)级对象锁。

 采用同步代码块的方式获取的测试结果如下所示:

 2.2 使用同步方法解决多线程安全问题

同步方法如下所示:

 测试结果如下所示:

3 JAVA线程中的几种状态

Java中线程的状态分为6种,如下所示:

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

线程状态的转移图如下所示:

3.1 JAVA中sleep()和wait()的区别

1、sleep() ⽅法正在执⾏的线程主动让出 cpu(然后 cpu 就可以去执⾏其他任务),在 sleep 指定时间后 cpu 再回 到该线程继续往下执⾏(注意:sleep ⽅法只让出了 cpu,⽽并不会释放同步资源锁);⽽ wait() ⽅法则是指当前 线程让⾃⼰暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进⽽运⾏,只有调⽤了 notify() ⽅ 法,之前调⽤ wait() 的线程才会解除 wait 状态,可以去参与竞争同步资源锁,进⽽得到执⾏。(注意:notify 的 作⽤相当于叫醒睡着的⼈,⽽并不会给他分配任务,就是说 notify 只是让之前调⽤ wait 的线程有权利重新参与线 程的调度);

2、 sleep() ⽅法可以在任何地⽅使⽤,⽽ wait() ⽅法则只能在同步⽅法或同步块中使⽤;

3、 sleep() 是线程类(Thread)的⽅法,调⽤会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到 时间⾃动恢复;wait() 是 Object 的⽅法,调⽤会放弃对象锁,进⼊等待队列,待调⽤ notify()/notifyAll() 唤醒指定 的线程或者所有线程,才会进⼊锁池,不再次获得对象锁才会进⼊运⾏状态。

3.2 JAVA中run()和start()的区别

1、每个线程都是通过某个特定 Thread 对象所对应的⽅法 run() 来完成其操作的,⽅法 run() 称为线程体。通过调 ⽤ Thread 类的 start() ⽅法来启动⼀个线程;

2、start() ⽅法来启动⼀个线程,真正实现了多线程运⾏。这时⽆需等待 run() ⽅法体代码执⾏完毕,可以直接继 续执⾏下⾯的代码;这时此线程是处于就绪状态,并没有运⾏。然后通过此 Thread 类调⽤⽅法 run() 来完成其运 ⾏状态,这⾥⽅法 run() 称为线程体,它包含了要执⾏的这个线程的内容,run() ⽅法运⾏结束,此线程终⽌。然后 cpu 再调度其它线程;

3、 run() ⽅法是在本线程⾥的,只是线程⾥的⼀个函数,⽽不是多线程的。如果直接调⽤ run(),其实就相当于是 调⽤了⼀个普通函数⽽已,直接待⽤ run() ⽅法必须等待 run() ⽅法执⾏完毕才能执⾏下⾯的代码,所以执⾏路径 还是只有⼀条,根本就没有线程的特征,所以在多线程执⾏时要使⽤ start() ⽅法⽽不是 run() ⽅法。

yield执行后线程进入就绪状态。通知调度器,主动让出对cpu的占用,让别的线程执行,但是不一定能够保证别的同样优先级的线程能够执行。线程会从运行态切换到就绪态,但是也有可能马上从就绪态又切换到运行状态。yield方法使用的注意事项如下所示:

(1)yield是一个静态的本地方法(native)

(2)调用yield后,yield告诉当前线程把运行机会交给线程池中有相同优先级的线程。

(3)yield不能保证,当前线程迅速从运行状态切换到就绪状态。

(4)yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。

三、线程池的介绍

线程池就是管理一系列线程的资源池,提供了限制和管理线程资源的方式,它可以降低资源消耗,提高响应速度,提高线程的可管理性。

1.关于ThreadPoolExecutor类的介绍

1.1ThreadPoolExecutor类的构造方法

如下图所示为java1.8中ThreadPoolExecutor类的源码,从这里我们可以看到ThreadPoolExecutor类的构造器共有7个参数。

第一个参数corePoolSize是线程池中核心线程的最大数量,表示的是任务队列未达到队列容量时,当前最大可以同时运行的线程数量。

第二个参数maximumPoolSize是线程池的最大线程数,表示的是当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数为最大线程数。

第三个参数keepAliveTime是当线程数大于核心线程数时,多余的空闲线程存活的最长时间,线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。、

第四个参数unit是参数的时间单位。

第五个参数workQueue是任务队列,用来储存执行任务的队列。主要的队列有接下来集中:

        1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
        2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
        3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool(5)使用了这个队列。
        4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列

第六个参数是线程工厂用来创建线程。 

第七个参数是拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处置。

  当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:
        AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
        CallerRunsPolicy:由调用线程处理该任务。
        DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
        DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

1.2 关于线程池的使用

首先我们创建一个MyThread类继承Thread类,并在run方法打印该线程的创建时间表示线程已经加入线程池。

我们通过ThreadPoolExecutor类创建线程池,并将线程加入线程池。具体的代码如下所示:

 我们运行该程序得到的测试结果如下所示,可以看到共有五个核心线程在线程池中执行。

2.线程池的execute()方法

  execute()方法的执行原理主要分为下面四步:

1、如果当前运行的线程,少于corePoolSize,则创建一个新的线程来执行任务。
2、如果运行的线程等于或多于 corePoolSize,将任务加入 BlockingQueue。
3、如果 BlockingQueue 内的任务超过上限,则创建新的线程来处理任务。
4、如果创建的线程数是单钱运行的线程超出maximumPoolSize,任务将被拒绝策略拒绝。

四、JAVA中锁的分类

 java中锁的种类很多,类如乐观锁&悲观锁、独享锁&共享锁、互斥锁&读写锁等。

对于乐观锁主要是通过版本号机制实现,悲观锁使用synchronized关键字实现。

独享锁和共享锁通过AOS实现不同的方法来实现独享或者共享。其中synchronized和可重入锁ReentrantLock都是独享锁。

互斥锁的具体实现就是synchronized、ReentrantLock。ReentrantLock锁想比较于synchronized关键字更加灵活。读写锁的具体实现是读写锁ReadWriteLock。

从上面的分析我们可以看出,使用范围最广的是可重入锁ReentrantLock和synchronized关键字。ReentrantLock和synchronized关键字都是独占锁,也都可重入,但是ReentrantLock的加锁和解锁都需要手动进行,synchronized关键字的加锁解锁是自动进行的。ReentrantLock的实现方式如下所示:

Lock lock=new ReentrantLock();
try{
    lock.lock();
}finally{
    lock.unlock();
}

对于ReentrantLock的构造我们可以在源码中观察,如下所示ReentrantLock类主要是通过继承AbstractQueuedSynchronizer类实现的。

 

1. 关于ReentrantLock的介绍

ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

从类图可以看到,ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。

获取锁:
1、void lock()方法

当一个线程调用该方法时,说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。

2、void lockInterruptibly()方法

该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。

3、boolean tryLock()方法

尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。值得注意的是,该方法不会引起当前线程阻塞。

4、boolean tryLock(long timeout, TimeUnit unit)方法

尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到,没有获取到该锁则返回false。

释放锁:
1、void unlock()方法

尝试释放锁,如果当前线程持有该锁,则调用该方法会让线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。

关于使用ReentrantLock的示例如下所示:

public static class ReentrantLockList {
    // 线程不安全的list
    private ArrayList<String> array = new ArrayList<String>();
    // 独占锁
    private volatile ReentrantLock lock = new ReentrantLock();
    
    // 添加元素
    public void add(String e) {
        lock.lock();
        try {
            array.add(e);
        }finally {
            lock.unlock();
        }
    }
 
    // 删除元素
    public void remove(String e) {
        lock.lock();
        try {
            array.remove(e);
        }finally {
            lock.unlock();
        }
    }
 
    // 获取数据
    public String get(int index) {
        lock.lock();
        try {
            return array.get(index);
        }finally {
            lock.unlock();
        }
    }
}

 如上代码通过在操作array元素前进行加锁保证同一时间只有一个线程可以对array数组进行修改,但是也只能有一个线程对array元素进行访问。ReentrantLock的底层是使用AQS实现的可重入独占锁,在这里AQS状态值为0表示当前锁空闲,为大于等于1的值则说明该锁已经被占用。该锁内部有公平与非公平实现,默认情况下是非公平的实现。另外,由于该锁是独占锁,所以某时只有一个线程可以获取该锁。

2. 关于AQS的介绍

AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器, 比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

2.1 AQS的原理概述

AQS 核⼼思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个 机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中,具体的实现如下图所示:

CLH队列:CLH(Craig, Landin, and Hagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列 实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成⼀个 CLH 锁队列的⼀个结点 (Node)来实现锁的分配。

AQS 使⽤⼀个 int 成员变量 (state) 来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队⼯作。AQS 使⽤ CAS 对该同步状态进⾏原⼦操作实现对其值的修改。具体的代码如下所示:

// 共享变量,使⽤ volatile 修饰保证线程可⻅性
private volatile int state;

2.2 AQS对资源的共享模式有哪些

1. Exclusive(独占):只有⼀个线程能执⾏,如:ReentrantLock,⼜可分为公平锁和⾮公平锁: 2. Share(共享):多个线程可同时执⾏,如:CountDownLatch、Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock。

3. AQS 底层使⽤了模板⽅法模式,AQS重写的方法的介绍

3.1 模板方法模式介绍

在程序开发中,经常会遇到这种情况:某个方法要实现的算法需要多个步骤,但其中有一些步骤是固定不变的,而另一些步骤则是不固定的。为了提高代码的可扩展性和可维护性,模板方法模式在这种场景下就派上了用场。

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。

AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。

ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模板方法定义在抽象类中,并由子类不加以修改地完全继承下来。模板方法是一个具体方法,它给出了一个顶层逻辑框架,而逻辑的组成步骤在抽象类中可以是具体方法,也可以是抽象方法。

基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。

抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。

具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

钩子方法:可以与一些具体步骤 "挂钩" ,以实现在不同条件下执行模板方法中的不同步骤

3.2 AQS需要重写的方法

使⽤者继承 AbstractQueuedSynchronizer 并᯿写指定的⽅法。将 AQS 组合在⾃定义同步组件的实现中,并调⽤ 其模板⽅法,⽽这些模板⽅法会调⽤使⽤者᯿写的⽅法。

1. isHeldExclusively() :该线程是否正在独占资源。只有⽤到 condition 才需要去实现它。

2. tryAcquire(int) :独占⽅式。尝试获取资源,成功则返回 true,失败则返回 false。

3. tryRelease(int) :独占⽅式。尝试释放资源,成功则返回 true,失败则返回 false。

4. tryAcquireShared(int) :共享⽅式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可⽤资源;正数表示成功,且有剩余资源。

5. tryReleaseShared(int) :共享⽅式。尝试释放资源,成功则返回 true,失败则返回 false。

五、ConcurrentHashMap的作用和做法 

1. ConcurrentHashMap的基本原理

ConcurrentHashMap是属于JUC工具包中的并发容器之一,在多线程开发中很经常会使用到这个类,它与HashMap的区别是HashMap是线程不安全的,在高并发的情况下,使用HashMap进行大量变更操作容易出现问题,但是ConcurrentHashMap是线程安全的。
JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,采用的数据结构(数组+链表+红黑树)。对于红黑树数据结构中的Node节点的val和next数据都用volatile保证,保证可见性,查找,替换,赋值操作都是用CAS算法。在未曾扩容或者hash情况下,此时并发情况并不是十分激烈,我们使用CAS算法比使用Synchronized和ReentrentLock的效率要高,但是如果在并发情况较为激烈时,需要奖CAS转换成Synchronized来保证线程安全。
ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。

2. ConcurrentHashMap的get()操作

get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。

在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。

1.计算hash值,定位到Node数组中的位置

2.如果该位置为null,则直接返回null

3.如果该位置不为null,再判断该节点是红黑树节点还是链表节点

如果是红黑树节点,使用红黑树的查找方式来进行查找。如果是链表节点,遍历链表进行查找

3. ConcurrentHashMap的put()操作

1.先判断Node数组有没有初始化,如果没有初始化先初始化initTable();

2.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入

3.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁

4.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。

5.如果是红黑树,就按照红黑树的结构进行插入。

六、关于synchronized的介绍

1. synchronized关键字

1.1 synchronized关键字的底层原理

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证他修饰的方法或者代码块在任意时刻只能由一个线程执行。在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的 操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原⽣线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,⽽操作系统实现线程之间的切换时需要从⽤户态转换到内核态,这个状态之间的转 换需要相对⽐较⻓的时间,时间成本相对较⾼,这也是为什么早期的 synchronized 效率低的原因。

庆幸的是在 JDK6 之后 Java 官⽅对从 JVM 层⾯对synchronized 较⼤优化,所以现在的 synchronized 锁效率也优化得很不错 了。JDK6 对锁的实现引⼊了⼤量的优化,如⾃旋锁、适应性⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术 来减少锁操作的开销。 synchronized 关键字底层原理属于 JVM 层⾯。

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

修饰实例⽅法:作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁;

修饰静态⽅法:作⽤于当前类对象加锁,进⼊同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作 ⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员(static 表明这是该类的⼀个静态资 源,不管 new了多少个对象,只有⼀份,所以对该类的所有对象都加了锁)。所以如果⼀个线程 A 调⽤⼀个实例 对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁;

修饰代码块:指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。和 synchronized ⽅ 法⼀样,synchronized(this) 代码块也是锁定当前对象的。synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class 类上锁。这⾥再提⼀下:synchronized 关键字加到⾮ static 静态⽅ 法上是给对象实例上锁。另外需要注意的是:尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具 有缓冲功能。

七、Volatile关键字

JAVA编程语言中允许多线程访问共享变量,为了确保共享变量可以被一致和可靠的更新,线程必须确保它是排他性的使用此共享变量。通常由两种实现方式,一种是获得对这些共享变量强制排他性的同步锁,还有一种是使用volatile域变量的方式,上面我们介绍了锁,接下来介绍volatile的使用。

1. volatile的作用

1.1 保证变量的可见性

volatile关键字的作用是保证共享变量的可见性。当一个线程读取变量时,总是能读取到他在内存中的最新的值,也就是说不同的线程看到的一个变量的值是相同的。

1.2 保证赋值操作的原子性

原子性就是不能被线程调度打断的,是线程安全的操作,对于原子性的操作,即使在多线程环境下,也不用担心线程安全或者数据不一致的问题。有些变量的赋值本身就是原子性的,比如对boolean,对int的赋值,但是像对于long或者double则不一定,如果是32位的处理器,对于64位的变量的操作可能会被分解成为二个步骤:高32位和低32位,由此可能会发生线程切换,从而导致线程不安全。如果变量声明为volatile,那么虚拟机会保证赋值是原子的,是不可被打断的。

1.3 禁止指令重排

正常情况下,虚拟机会对指令进行重排,当然是在不影响程序结果的正确性的前提下。volatile能够在一定程度上禁止虚拟机进行指令重排。还有就是对于volatile变量的写操作,保证是在读操作之前完成,假设线程A来读变量,刚好线程B正在写变量,那么虚拟机会保证写在读之前完成。

如果线程A和线程B同时处理一个变量,线程A对变量进行修改操作,线程B对变量查询操作,对于一般的变量,我们无法保证B能读到A设置的值,因为他们之间执行的顺序是未知的。但是加上volatile修饰以后,虚拟机会保证,线程A的写操作在线程B的读操作之前完成,换句话,B能读到最新的值。当然了,用锁机制也能达到同样的效果,比如在方法前面都加上synchronized关键字,但是性能会远不如使用volatile。

2. volatile的实现机制

volatile是由虚拟机层引入的,他可以解决语言层面的问题,他的实现是靠着下一层的支持,也就是需要汇编或者是处理器指令的支持来实现的,volatile是靠着内存屏障和MESI(缓存一致性协议)来达到他的作用的。

2.1 什么是缓存一致性协议

CPU 在执行指令的时候需要从 memory 中获取指令和需要的数据,但是 CPU 的速度要比 memory 快很多,这就导致了 CPU 大部分时间都不是在做运算上而是用在了和 memory 进行数据的 I/O 过程中,这样性能是很低的。这就导致了 CPU cache 的产生,CPU 将数据从 memory 读取到 cache 中,在 cache 中对数据进行读写的速度是很快的,这样就提高了性能。CPU 执行运算时不可能需要某一个数据就去读取一次,这样就增加了 I/O 的频率,导致性能低下。所以会一次性读取一块内存的数据存放到 cache 中, 这个块称为 “cache line” , cache line 的大小是固定的, 通常是 2 的 N 次方,在 16 ~ 256 byte 不等。当某个数据首次被 CPU 访问时, cache 中不存在,这称为 “cache miss” (或 “startup” 或 “warmup” cache miss)。这意味着 CPU 必须要去 memory 中读取该数据,这时候 CPU 必须等待(stalled)。当 cache 装满后,后续的 cache miss 需要置换 cache 中现有的数据,这样的 cache miss 被称为 “capacity miss” 。如果随后又访问了被替换的 cache line ,这时的 cache miss 被称为 “associativity miss” 。

当 CPU 写数据的时候,需要保证该数据在多个 CPU cache 之间的一致性。在写之前必须先让其他 CPU cache 中的该数据失效,之后才可以安全的写数据。如果这个数据已经存在于要执行写指令的 CPU cache 中,但是是 read only 的,这个过程被称为 "write miss" ,一旦其他 CPU cache 中的该数据都失效后,该 CPU 可以不断的写或读其 cache 中的数据。如果另外某一个 CPU 尝试访问该数据,会形成一次 cache miss ,这时称为 “communication miss” 。因为这通常是多个 CPU 之间使用缓存通信造成的。

缓存一致性协议有四个状态,分别为M : modified,E : exclusive,S : shared,I : invalid。

协议在每一个 cache line 中维护一个两位的状态 “tag” ,这个 “tag” 在 cache line 的物理地址或者数据后。

modified 状态的 cache line 是由于相应的 CPU 进行了写操作,同时也表示该数据不会存在于其他 CPU 的 cache 中。也就说明这个最新的数据目前是被执行写操作的 CPU cache line 独占的。因为如此当前 cache 需要对该数据负责,比如将该数据传递给其他 CPU cache ,或者是将该数据写回到 memory 中。而这些操作都需要在 reuse cache line (置换)之前完成。

exclusive 状态和 modified 状态类似,区别是 CPU 还没有修改 cache line 中的数据。在 exclusive 状态下 CPU 可以不通知其他 CPU 而直接 cache line 进行操作。也就是说 exclusive 状态是该 CPU 对这个数据的独占。此时由于 memory 中和 cache line 中的数据都是最新的,所以不需要对 exclusive 状态的 cache line 执行写回 memory 的操作就可以直接 reuse。

shared 状态的 cache line ,其数据可能在一个或者是多个 CPU cache 中,此时 CPU 不能直接修改 cache line 中的数据,而是需要先通知其他 CPU cache 。和 exclusive 一样, shared 状态的 cache line 对应的 memory 中的数据也是最新的,因此也可以直接 reuse cache line。

invalid 状态时 cache line 是空的。当新的数据要进入 cache 的时候优先选择 invalid 的 cache line ,之所以如此是因为如果选中其他状态的 cache line 会涉及到 cache line 中数据的置换 , 而之后如果这个被替换的数据被访问到会造成 cache miss ,这样造成很大的开销。

2.2 什么是内存屏障

2.2.1 内存屏障的由来

对于CPU的写,目前主流策略有两种:

1、write back:即CPU向内存写数据时,先把真实数据放入store buffer中,待到某个合适的时间点,CPU才会将store buffer中的数据刷到内存中,而且这两个操作是异步的。这在多线程环境中,有些情况下是可以接受的,但是有些情况是不可接受的,为了让程序员有能力根据业务需要达到同步完成,就设计了内存屏障。

2、write through:即CPU向内存写数据时,同步完成写store buffer与内存。

当前CPU大多数采用的是write back策略。可能有童鞋要问了:为什么呢?因为大多数情况下,CPU异步完成写内存产生的部分延迟是可以接受的,而且这个延迟极短。只有在多线程环境下需要严格保证内存可见等极少数特殊情况下才需要保证CPU的写在外界看来是同步完成的,需要借助CPU提供的内存屏障实现。如果直接采用策略2:write through,那每次写内存都需要等待数据刷入内存,极大影响了CPU的执行效率。

看到这里,有没有童鞋有这个疑问:CPU中为什么要加入load buffer、store buffer?有兴趣的同学可以先去研究研究,后面有空我会写文章讲解这块。

2.2.2 内存屏障的实现思路

内存屏障是为了解决业务层面不能接收store buffer与刷回内存异步操作产生的极少的延迟,即对内存可见性要求极高。

内存屏障是一个抽象的概念,可以把内存屏障理解为一堵墙,这堵墙正面和反面的指令无法被CPU乱序执行,而且这堵墙的正面和反面的读写操作都需要有序执行。

CPU提供了三个汇编指令串行化运行读写指令达到实现保证读写有序性的目的:

SFENCE:在该指令前的写操作必须在该指令后的写操作前完成

LFENCE:在该指令前的读操作必须在该指令后的读操作前完成

MFENCE:在该指令前的读写操作必须在该指令后的读写操作前完成

何谓串行化?你可以理解成CPU把读、写、读写请求放入了一个队列,按照先进先出的顺序执行下去;何谓读操作完成,即CPU执行一次读操作,把值读到了寄存器中;何谓写操作完成,即CPU执行一次写操作,数据刷到内存中了。

八、CAS算法和AQS同步器的介绍

1.CAS算法

CAS(compare and swap比较与交换)算法是一种有名的无锁算法,主要用于乐观锁的实现。CAS算法可以在不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步,CAS算法涉及到三个操作的数,需要读写的内存值V,进行比较的值A,拟写入的新值B。

当且仅当V值等于A值时,CAS算法通过原子方式用新值B更新内存中的值V,否则不会执行任何操作(比较和替换是一个原子操作)。通俗的理解就是CAS操作需要我们提供一个期望值A,当期望值A与当前线程的变量值V相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。一般情况下是一个自旋操作,即不断的重试。

对于CAS导致的ABA问题,比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。可以通过版本号的方式来解决该问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

2. AQS同步器

队列同步器(AbstractQueuedSynchronizerAQS)是用来构架锁或者其他同步组件的基础框架。它是面向锁的实现者的,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待和唤醒等底层操作,大大降低了实现一个可靠的锁或者同步组件的门槛。

AQS是基于 模板方法模式 设计的。使用者需要继承同步器并重写指定的方法来操作同步状态,使用的时候要调用同步器提供的模板方法。

九、JAVA中原子类的介绍

原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。他的作用和锁有类似之处,是为了保证并发情况下的线程安全。所以,所谓原子类简单来说就是具有原子操作特征的类。并发包 java.util.concurrent 的原⼦类都存放在 java.util.concurrent.atomic 下。

1. 原子类的分类

根据操作的数据类型,可以将 JUC 包中的原⼦类分为 4 类:

基本类型原子类(使用原子的方式更新基本类型):AtomicInteger:整型原⼦类、 AtomicLong:⻓整型原⼦类、 AtomicBoolean :布尔型原⼦类

数据类型原子类(使用原子的方式更新数组中的某个元素):AtomicIntegerArray:整型数组原⼦类、 AtomicLongArray:⻓整型数组原⼦类 、AtomicReferenceArray :引⽤类型数组原⼦类

引用类型类:AtomicReference:引⽤类型原⼦类 、AtomicStampedReference:原⼦更新引⽤类型⾥的字段原⼦类 、AtomicMarkableReference :原⼦更新带有标记位的引⽤类型

对象的属性修改类型:AtomicIntegerFieldUpdater:原⼦更新整型字段的更新器、 AtomicLongFieldUpdater:原⼦更新⻓整型字段的更新器 、AtomicStampedReference :原⼦更新带有版本号的引⽤类型。该类将整数值与引⽤关联起来,可⽤于解决原⼦ 的更新数据和数据的版本号,可以解决使⽤ CAS 进⾏原⼦更新时可能出现的 ABA 问题。

2. Atomic的原理

Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引⽤类型)变量进 ⾏操作时,具有排他性,即当多个线程同时对该变量的值进⾏更新时,仅有⼀个线程能成功,⽽未成功的线程可以 向⾃旋锁⼀样,继续尝试,⼀直等到执⾏成功。 Atomic 系列的类中的核⼼⽅法都会调⽤ unsafe 类中的⼏个本地⽅法。我们需要先知道⼀个东⻄就是 Unsafe 类, 全名为:sun.misc.Unsafe,这个类包含了⼤量的对 C 代码的操作,包括很多直接内存分配以及原⼦操作的调⽤, ⽽它之所以标记为⾮安全的,是告诉你这个⾥⾯⼤量的⽅法调⽤都会存在安全隐患,需要⼩⼼使⽤,否则会导致严 ᯿的后果,例如在通过 unsafe 分配内存的时候,如果⾃⼰指定某些区域可能会导致⼀些类似 C++ ⼀样的指针越界 到其他进程的问题

3.synchronized、lock、volatile的区别

3.1 synchronized和volatile之间的区别

1. volatile关键字解决的是变量在多个线程之间的可见性;而sychronized关键字解决的是多个线程之间访问共享资源的同步性。
2. volatile只能用于修饰变量,而synchronized可以修饰方法,以及代码块。(volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,并且随着JDK新版本的发布,sychronized关键字在执行上得到很大的提升,在开发中使用synchronized关键字的比率还是比较大)
3. 多线程访问volatile不会发生阻塞,而sychronized会出现阻塞。
4. volatile能保证变量在多个线程之间的可见性,但不能保证原子性;而sychronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。

线程安全包含原子性和可见性两个方面。
对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
一句话说明volatile的作用:实现变量在多个线程之间的可见性。

3.2 synchronized和lock区别

1)Lock是一个接口,经常使用的锁的类是有实现该接口的。而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率(读写锁)。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

十、ThreadLocal的使用

1.ThreadLocal的简介

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

(1)因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
(2)既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

2. ThreadLocal的方法

1. ThreadLocal的set()方法

 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
    }

  从上面的代码可以看出,ThreadLocal  set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
 

  static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value 

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

2. ThreadLocal的get方法

    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
    }
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

 3 Threadlocal的remove方法

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

 remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

3. ThreadLocal的代码示例

public class ThreadLocaDemo {
 
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
               
            }
        },"A").start();
 
        Thread.sleep(1000);
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_B");
                print("B");
                System.out.println("after remove : " + localVar.get());
              
            }
        },"B").start();
    }
}
 
A :local_A
after remove : null
B :local_B
after remove : null
 

从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Java并发核心知识体系是指在Java编程语言,用于处理多线程编程的一系列核心知识和技术。Xmind是一种思维导图工具,可以帮助我们清晰地组织和呈现这些知识体系。 Java并发核心知识体系包括以下内容: 1.线程基础知识:了解线程概念、线程创建和启动、线程状态转换等基本概念和操作。 2.线程安全:学习如何确保多个线程访问共享资源时的线程安全性,如使用锁、同步关键字、volatile关键字等。 3.锁和同步:深入研究各种锁的实现原理,比如synchronized关键字、ReentrantLock、ReadWriteLock等,并学习如何正确使用它们。 4.并发集合:了解Java提供的线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等。 5.线程通信:学习线程之间的协作和通信,包括使用wait()、notify()、notifyAll()等方法实现等待、通知机制。 6.线程池:学习如何使用线程池来管理和调度线程,提高线程的执行效率和资源利用率。 7.并发工具类:研究一些常用的并发工具类,如Semaphore、CountDownLatch、CyclicBarrier等。 8.原子操作:了解Java提供的原子操作类,如AtomicInteger、AtomicLong等,可以保证某些操作的原子性。 9.并发模型:掌握几种常用的线程并发模型,如生产者消费者模型、读写者模型等。 Xmind可以帮助我们将以上知识整理成一张思维导图,以便更好地理解和记忆。我们可以用心主题为“Java并发核心知识体系”,然后分支出各个子主题,如“线程基础知识”、“线程安全”、“锁和同步”等,再进一步细分为各个具体的知识点。通过这样清晰的组织结构,我们可以更加系统地学习和理解Java并发编程的核心知识。 ### 回答2: Java并发核心知识体系精讲xmind是一份专门用于讲解Java并发编程的思维导图。它通过图形化的方式系统地呈现了Java并发编程的核心知识,方便学习者理解和记忆。以下是对Java并发核心知识体系精讲xmind的回答: Java并发核心知识体系精讲xmind是一份非常有价值的学习资料,它对Java并发编程的相关知识进行了详细的整理和总结。通过该xmind文件,学习者可以快速了解并发编程的基本概念、原理和常用工具类,深入了解多线程、线程安全和锁机制等重要的内容。 该xmind文件首先介绍并发编程的基本概念,如进程、线程和并发的概念,并讲解了线程的生命周期和线程的创建、启动、暂停、终止等操作。接着,该文件详细讲解了Java提供的并发编程的核心类,包括Thread、Runnable、Callable、Lock、Condition等,以及线程池和计数器等常用的并发工具类。 该xmind文件还深入讨论了Java并发编程的一些重点内容,比如线程安全、原子性、可见性和有序性等问题。它解释了线程安全的概念,以及Java如何实现线程安全,如使用同步机制、锁机制和原子类等方式。此外,该文件还介绍了线程间的通信方式,包括共享内存和消息传递。 在最后,该xmind文件还介绍了一些高级的并发编程技术,比如并发集合类、并发控制和并发算法等。它详细讲解了Java提供的并发集合类,如ConcurrentHashMap和ConcurrentLinkedQueue等,并解释了它们的设计原理和使用方法。此外,该文件还介绍了一些常见的并发控制和并发算法,如信号量和读写锁等。 综上所述,Java并发核心知识体系精讲xmind是一份非常有价值的学习资料,对于掌握Java并发编程知识和提高多线程编程能力非常有帮助。通过系统地学习该xmind文件,可以更好地理解并发编程的原理和应用,提高并发编程的技术水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值