Java语言: 多线程

0. 进程、线程、协程

详情
进程

进程是拥有资源的基本单位。

线程

线程是调度和分配的基本单位。

协程

协程:协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。(线程内核管理,协程自己控制切换,用户态和内核态切换,比线程切换代价更小)

1. 线程调度

1.1 线程状态

线程是cpu任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈。

线程状态:创建、就绪、运行、阻塞、死亡

图片

1.2 线程状态切换

图片

图片

1.3 阻塞唤醒过程

阻塞:

这三个方法的调用都会使当前线程阻塞。该线程将会被放置到对该Object的请求等待队列中,然后让出当前对Object所拥有的所有的同步请求。线程会一直暂停所有线程调度,直到下面其中一种情况发生:

① 其他线程调用了该Object的notify方法,而该线程刚好是那个被唤醒的线程;

② 其他线程调用了该Object的notifyAll方法;

唤醒:

线程将会从等待队列中移除,重新成为可调度线程。它会与其他线程以常规的方式竞争对象同步请求。一旦它重新获得对象的同步请求,所有之前的请求状态都会恢复,也就是线程调用wait的地方的状态。线程将会在之前调用wait的地方继续运行下去。

为什么要出现在同步代码块中:

由于wait()属于Object方法,调用之后会强制释放当前对象锁,所以在wait() 调用时必须拿到当前对象的监视器monitor对象。因此,wait()方法在同步方法/代码块中调用。

1.4 wait和sleep区别

  • wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。

  • wait 方法会主动释放 monitor 锁,在同步代码中执行 sleep 方法时,并不会释放 monitor 锁。

  • wait 方法意味着永久等待,直到被中断或被唤醒才能恢复,不会主动恢复,sleep 方法中会定义一个时间,时间到期后会主动恢复。

  • wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

1.5 创建线程方式

1.5.1 实现 Runnable 接口(优先使用)

public class RunnableThread implements Runnable {
  	@Override
    public void run() {
        System.out.println('用实现Runnable接口实现线程');
    }
}

1.5.2 实现Callable接口(有返回值可抛出异常)

class CallableTask implements Callable < Integer > {
  	@Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}

1.5.3 继承Thread类(java不支持多继承)

public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println('用Thread类实现线程');
    }
}

2. 线程池

优点:通过复用已创建的线程,降低资源损耗、线程可以直接处理队列中的任务加快响应速度、同时便于统一监控和管理

2.1 参数

线程池构造函数

/*** 线程池构造函数7大参数*/
public ThreadPoolExecutor(int corePoolSize, 
    int maximumPoolSize, 
    long keepAliveTime, 
    TimeUnit unit, 
    BlockingQueue < Runnable > workQueue, 
    ThreadFactory threadFactory, 
    RejectedExecutionHandler handler) {
}

参数介绍:

参数

解释

@param corePoolSize

核心线程池中的最大线程数

@param maximumPoolSize

总线程池中的最大线程数

@param keepAliveTime

空闲线程的存活时间

@param unit

keepAliveTime的单位

@param workQueue

任务队列, 保存已经提交但尚未被执行的线程

@param threadFactory

线程工厂(用于指定如果创建一个线程)

@param handler

拒绝策略 (当任务太多导致工作队列满时的处理策略)

2.2 处理任务过程

  1. 当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  2. 当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。

  3. 当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。

  4. 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。

  5. 当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。

2.3 线程拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略如下:

2.3.1 AbortPolicy

直接抛出异常,阻止系统正常运行。可以根据业务逻辑选择重试或者放弃提交等策略。

2.3.2 CallerRunsPolicy

只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。

不会造成任务丢失,同时减缓提交任务的速度,给执行任务缓冲时间。

2.3.3 DiscardOldestPolicy

丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

2.3.4 DiscardPolicy

该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

2.4 Execuors类实现线程池

图片

  • newSingleThreadExecutor():只有一个线程的线程池,任务是顺序执行,适用于一个一个任务执行的场景

  • newCachedThreadPool():线程池里有很多线程需要同时执行,60s内复用,适用执行很多短期异步的小程序或者负载较轻的服务

  • newFixedThreadPool():拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务。

  • newScheduledThreadPool():用来调度即将执行的任务的线程池

  • newWorkStealingPool():底层采用forkjoin的Deque,采用独立的任务队列可以减少竞争同时加快任务处理

图片

因为以上方式都存在弊端:

FixedThreadPool 和 SingleThreadExecutor :允许请求的队列⻓度为 Integer.MAX_VALUE,会导致OOM。

CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE,会导致OOM。

手动创建的线程池底层使用的是ArrayBlockingQueue可以防止OOM。

2.5 Spring实现线程池

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;


@Configuration
public class ThreadConfig {

    @Bean(name = "xxxxExecutor")
    public TaskExecutor xxxxExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(50);
        executor.setKeepAliveSeconds(30);
        executor.setAllowCoreThreadTimeOut(true);
        executor.setThreadNamePrefix("job_event_executor");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

2.6 线程池大小设置

  • CPU 密集型(n+1)

CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。

CPU 密集型任务尽可能的少的线程数量,一般为 CPU 核数 + 1 个线程的线程池。

  • IO 密集型(2*n)

由于 IO 密集型任务线程并不是一直在执行任务,可以多分配一点线程数,如 CPU * 2 

也可以使用公式:CPU 核心数 *(1+平均等待时间/平均工作时间)。

3. 线程安全

3.1 乐观锁,CAS思想

java乐观锁机制:

乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。在Java中 java.util.concurrent.atomic下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁,大多是基于数据版本 (Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

CAS思想:

CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作:

需要读写内存值V;进行比较的值A;准备写入的值B

当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试。

缺点:https://www.zhihu.com/question/23281499/answer/854522984

ABA问题-知乎

高并发的情况下,很容易发生并发冲突,如果CAS一直失败,那么就会一直重试,浪费CPU资源

原子性:

功能限制CAS是能保证单个变量的操作是原子性的,在Java中要配合使用volatile关键字来保证线程的安全;当涉及到多个变量的时候CAS无能为力;除此之外CAS实现需要硬件层面的支持,在Java的普通用户中无法直接使用,只能借助atomic包下的原子类实现,灵活性受到了限制

 3.2 AQS (AbstractQueuedSynchronizer)

AQS的全称为(AbstractQueuedSynchronizer)抽象的队列式的同步器,是⼀个⽤来构建锁和同步器的框架,使⽤AQS能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器。

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

简介

  • AQS利用了模板方法模式,其中大多数方法都是final或是private的,我们把这类方法称为Skeleton Method,也就是说这些方法是AQS框架自身定义好的骨架,子类是不能覆写的。
  • 支持中断、超时
  • 支持独占模式和共享模式
  • 支持Condition条件等待

AQS方法说明

  • CAS操作
  • 等待队列的核心操作
  • 资源的获取操作
  • 资源的释放操作

三个基本问题

  • 同步状态(synchronization state)的管理
  • 阻塞/唤醒线程的操作
  • 线程等待队列的管理

Node节点

CLH队列

CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。 每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)

3.2.1 AQS与多线程的关系

  • 协调线程访问:AQS的主要职责是协调多个线程对共享资源的访问。它允许线程以有序的方式获取和释放资源,从而避免了竞态条件和不一致的状态。
  • FIFO队列:AQS内部维护了一个FIFO队列,用于存储等待获取资源的线程。当一个线程尝试获取资源失败时,它会被添加到队列的尾部,并等待其他线程释放资源。
  • 状态管理:AQS使用一个整数来表示同步状态。这个状态可以被线程获取和释放,从而控制对共享资源的访问。不同的同步器可以根据需要定义这个状态的具体含义。
  • 自定义同步器:通过继承AQS并实现其定义的方法(如tryAcquiretryRelease等),开发人员可以创建自定义的同步器。这些同步器可以满足特定的并发需求,如互斥锁、读写锁、信号量等。
  • 公平与非公平:AQS支持公平和非公平两种模式。在公平模式下,等待时间最长的线程将优先获取资源;而在非公平模式下,线程获取资源的顺序可能不确定。

3.2.2 AQS与JUC的关系

在Java的java.util.concurrent(JUC)包中,虽然AbstractQueuedSynchronizer(AQS)是一个核心框架,用于构建同步器,但并非JUC中所有的工具都是基于AQS实现的

  • AQS是一个为构建锁和同步器提供框架的类,它本身并不直接提供同步工具,而是作为其他同步组件的基础。例如,ReentrantLockCountDownLatchSemaphore等同步工具都是继承自AQS并基于其实现的。这些工具通过AQS提供的队列和状态管理机制来协调多线程对共享资源的访问。
  • JUC包还提供了许多其他工具,如ConcurrentHashMapExecutors框架CyclicBarrierExchanger等,它们并不是基于AQS实现的。这些工具采用了不同的机制和方法来满足不同的并发需求。

3.2.3 AQS运用场景

3.3 synchronized和ReentrantLock

3.3.1 synchronized

使用方法:主要的三种使⽤⽅式

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

修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员。

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

总结:synchronized锁住的资源只有两类:一个是对象,一个是

底层实现:

对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息。

锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

同步代码块是利用 monitorenter 和 monitorexit 指令实现的,而同步方法则是利用 flags 实现的。

3.3.2 ReenTrantLock

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能

3.3.2.1 使用方法

基于API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

3.3.2.2 底层实现

ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

3.3.3 和synchronized区别

  • 底层实现:synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层的锁。
  • 实现原理synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
  • 是否可手动释放:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象
  • 是否可中断: synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
  • 是否公平锁: synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。

3.3.4 使用示例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author MaoTian
 * @Classname SyncAndLockCondition
 * @Description lock可以绑定多个condition,可以精确唤醒A-B-C-D-A
 */
class ShareResource{
    private int number=0; //A=1,B=2,C=3
    //
    private Lock lock=new ReentrantLock();
    private Condition c1=lock.newCondition();
    private Condition c2=lock.newCondition();
    private Condition c3=lock.newCondition();
    //
    public void print_5(){
        lock.lock();
        try {
            while (number!=0){
                c1.await();
            }
            for (int i = 0; i <5 ; i++) {
                System.out.println(Thread.currentThread()+":"+number);
            }
            number=1;
            c2.signal();
        }catch (Exception e){
        }finally {
            lock.unlock();
        }
    }
    //
    public void print_10(){
        lock.lock();
        try {
            while (number!=1){
                c2.await();
            }
            for (int i = 0; i <10 ; i++) {
                System.out.println(Thread.currentThread()+":"+number);
            }
            number=2;
            c3.signal();
        }catch (Exception e){
        }finally {
            lock.unlock();
        }
    }
    public void print_15(){
        lock.lock();
        try {
            while (number!=2){
                c3.await();
            }
            for (int i = 0; i <15 ; i++) {
                System.out.println(Thread.currentThread()+":"+number);
            }
            number=0;
            c1.signal();
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }
}
public class SyncAndLockCondition {
    public static void main(String[] args) {
        ShareResource shareResource=new ShareResource();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                shareResource.print_5();
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                shareResource.print_10();
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                shareResource.print_15();
            }
        }).start();
    }
}

3.4 公平锁和非公平锁区别

公平锁:

公平锁自然是遵循FIFO(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待。

优点:所有的线程都能得到资源,不会饿死在队列中。适合大任务。

缺点:吞吐量会下降,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销大。

非公平锁:

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁。

图片

公平锁效率低原因:

公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次挂起和唤醒

线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

3.5 使用层面锁优化

【1】减少锁的时间:
不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

【2】减少锁的粒度:
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率,比如:

ConcurrentHashMap:

java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组:Segment< K,V >[] segments

Segment继承自ReenTrantLock,所以每个Segment是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。

【3】锁粗化:
大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度; 

假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

【4】使用读写锁:

ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可并发读,写操作使用写锁,只能单线程写;

【5】使用CAS:

如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;

3.6 系统层面锁优化

自适应自旋锁:

自旋锁可以避免等待竞争锁进入阻塞挂起状态被唤醒造成的内核态和用户态之间的切换的损耗,它们只需要等一等(自旋),但是如果锁被其他线程长时间占用,一直不释放CPU,死等会带来更多的性能开销;自旋次数默认值是10。

对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

锁消除:

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。Netty中无锁化设计pipeline中channelhandler会进行锁消除的优化。

锁升级:

偏向锁:

如果线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些monitor操作,因为在大部分情况下是没有竞争的,所以使用偏向锁是可以提高性能的;

轻量级锁:

在竞争不激烈的情况下,通过CAS避免线程上下文切换,可以显著的提高性能。

重量级锁:

重量级锁的加锁、解锁过程造成的损耗是固定的,重量级锁适合于竞争激烈、高并发、同步块执行时间长的情况。

3.7 ThreadLocal原理

ThreadLocal简介:

通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的。

专属本地变量该如何解决呢?JDK中提供的 ThreadLocal 类正是为了解决这样的问题。类似操作系统中的TLAB。

原理:

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 。

如何使用:

1)存储用户Session

private static final ThreadLocal threadSession = new ThreadLocal();

2)解决线程安全的问题

private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>()

ThreadLocal内存泄漏的场景

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

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。

图片

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。

3.8 HashMap线程安全

死循环造成 CPU 100%

HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中推荐使用线程安全同时性能比较好的 ConcurrentHashMap。

3.9 String不可变原因

  1. 可以使用字符串常量池,多次创建同样的字符串会指向同一个内存地址;

  2. 可以很方便地用作 HashMap 的 key。通常建议把不可变对象作为 HashMap的 key;

  3. hashCode生成后就不会改变,使用时无需重新计算;

  4. 线程安全,因为具备不变性的对象一定是线程安全的;

3.10 JUC

功能

实现

juc-locks 锁框架

juc-locks锁框架中一共就三个接口:Lock、Condition、ReadWriteLockReadWriteLock: 一个单独的接口(未继承Lock接口),该接口提供了获取读锁和写锁的方法。ReentrantLock:ReentrantLock内部通过内部类实现了AQS框架(AbstractQueuedSynchronizer)的API来实现独占锁的功能。ReentrantReadWriteLock:ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。写锁可以降级成读锁,读锁不能升级成写锁。

juc-atomic 原子类框架

( J.U.C之atomic框架:Unsafe类)其实底层就是通过Unsafe类实现的一种比较并交换的算法,大致的结构如下(具体入参,根据上下文有所不同): boolean compareAndSet(expectedValue, updateValue); 当希望修改的值与expectedValue相同时,则尝试将值更新为updateValue,更新成功返回true,否则返回false。Unsafe类,来源于sun.misc包。该类封装了许多类似指针操作,可以直接进行内存管理、操纵对象、阻塞/唤醒线程等操作。Java本身不直接支持指针的操作,所以这也是该类命名为Unsafe的原因之一。

juc-sync 同步器框架

juc-collections 集合框架

juc-executors 执行器框架

    • 线程池
    • Future模式,Future接口仅仅定义了5个方法。
    • Fork/Join框架

4. 内存模型

Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

图片

JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。

原子性:

在 Java 中,为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit。这两个字节码,在 Java 中对应的关键字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。

可见性:

Java 中的 Volatile 关键字修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用 Volatile 来保证多线程操作时变量的可见性。除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性。只不过实现方式不同

有序性:

在 Java 中,可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性。区别:Volatile 禁止指令重排。Synchronized 保证同一时刻只允许一条线程操作。

4.1 volatile底层实现

作用:

保证数据的“可见性”:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致。

底层实现:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

单例模式中volatile的作用:

防止代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。

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;
    }
}

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值