并发相关知识点总结

并发基础知识点总结

1 什么是线程和进程?

1.1 进程

进程是程序的一次执行过程,是系统运行程序的基本单位。进程是动态的,系统运行一个程序即是一个进程从创建,运行到消亡的过程。

1.2 线程

线程是CPU调度的最小单元,与进程类似,但比进程更小。一个进程在执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间切换工作,负担要比进程小的多。因此线程也被称为轻量级进程。

2 线程和进程的区别和优缺点?

2.1 进程和线程的关系

在这里插入图片描述

从上图可看出,线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而个线程则不一定,因为同一进程中的线程极有可能互相影响。线程执行开销小,但不利于资源的管理和保护;而进程则相反。

2.2 程序计数器为什么是私有的

​ 程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用来记录当前线程执行的位置,从而当线程被切换回来的时候就能知道线程上次运行到哪儿了。

注意:如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令的地址。所以程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

2.3 虚拟机栈和本地方法栈为什么是私有的

  • 虚拟机栈:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直到执行完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。
  • 本地方法栈:和虚拟机栈所发挥的作用类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟栈使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。

所以,为了保证线程中局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

2.4 简单说一下堆和方法栈

​ 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,几乎所有的对象都在这里分配内存,方法区主要用于存放已被加载的类信息、常量、静态变量等数据。

3 并发和并行的区别

  • 并发:同一时间段,多个任务都在执行(单位时间内不一样同时执行)
  • 并行:单位时间内,多个任务同时执行。

4 为什么要使用多线程呢

从总体上:

  1. 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了上下文切换的开销。
  2. 从当代互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是高并发系统的基础,利用好多线程机制可大大提高系统整体的并发能力以及性能。

深入计算机底层:

多核时代多线程主要是为了提高cpu利用率。

5 使用多线程可能带来什么问题?

​ 并发编程的目的是提高程序的执行效率和执行速度,但也会遇到各种问题:内存泄露、死锁、线程不安全。

6 什么是上下文切换?

​ 多线程编程中一般线程的个数都大于CPU核心的个数,CPU采用的策略是为每一个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括地说就是当前任务在时间片用完切换到其他任务之前会保存当前的状态,以便下一次切换过来时继续执行,这样从保存到切换回来的过程就是一次上下文切换。

7 什么是线程死锁?如何避免死锁?

​ 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。简而言之,死锁就是由于资源的互相抢占导致的线程一直等待。

在这里插入图片描述

死锁的四个必要条件:

  1. 互斥条件:该资源任何时候只能由一个线程占用。
  2. 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待:若干个进程形成一种头尾相接的循环等待资源关系。

7.1 如何预防和避免线程死锁

​ 预防很简单,只需破坏死锁产生的任何一个必要条件即可:

  1. 破坏请求和保持条件:一次性申请所有资源。
  2. 破坏不可剥夺条件:占用部分资源的线程在申请其他资源时,如果申请不到则可以主动释放它占有的资源。
  3. 破坏循环等待条件:靠按序申请资源来预防。

如果避免死锁呢?

​ 避免死锁就是在资源分配时,借助算法(如银行家算法)对资源分配进行计算评估,使其进入安全状态。安全状态指的是系统能够按照某种进行顺序来为每个进程分配所需资源。

8 sleep()方法和wait()方法的区别和共同点?

  • 两者最主要的区别是sleep方法不释放锁,而wait方法释放了锁。
  • 两者都可以暂停线程的执行。
  • wait方法通常用于线程间交互/通信,sleep通常被用于暂停执行。
  • wait方法被调用后线程不会自动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程自动苏醒。

并发进阶知识点总结

1 synchronized关键字

1.1 说一说自己对synchronized关键字的理解

答:Java中的保证同步的关键字,可以保证线程访问的时候其他线程不能访问。在Java早期版本中,synchronized属于重量级锁,效率低下。但是在Java6以后Java官方在jvm层面对synchronized锁进行了优化,jdk1.6对锁的实现引入了大量的优化,如自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

1.2 synchronized关键字的使用方法

对象锁是用于对象实例方法或者一个对象实例上的。一个对象一把锁,多个对象多把锁。

类锁是用于类的静态方法或者一个类的class对象上的。所有对象共用一个锁

synchronized关键字加到类上或者static静态方法上,都是给Class类加锁,加到实例方法上是给对象加锁。

1.2 synchronized的应用——单例模式

手写一个单例模式(DCL)

public class Singleton {
    // volatile关键字修饰对象
    private volatile static Singleton instance;

    // 构造方法
    public Singleton() {}

    // DCL
    public static Singleton getUniqueInstance() {
        if (instance == null) {
            synchronize(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

1.3 构造方法可以使用synchronized关键字修饰么?

答:构造方法不可以,因为它本身就是线程安全的,不存在同步的构造方法一说。

1.4 讲一下synchronized关键字的底层原理

详见另一篇文章:Synchronized锁总结

2 volatile关键字

详见另一篇文章:synchronized锁总结

3 ThreadLocal

3.1 ThreadLocal简介

​ 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己专属的本题变量该如何解决?JDK提供的ThreadLocal类正是为了解决这样的问题。ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成一个存放数据的盒子,盒子中可以存储每个线程的私有数据。

​ 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用get和set方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

3.2 与volatile的区别

通过简介我对ThreadLocal和volatile产生一个疑惑:ThreadLocal是通过在每个线程中保存一个变量的本地副本,然后可以通过get,set方法来进行操作。保存副本这不是volatile的可见性的操作吗,为什么ThreadLocal又来?通过阅读相关博客进行一个总结:

  • volatile主要用于在多线程间同步变量,保证线程间拿到的是同一个内容,不加volatile关键字,变量也是共享的,只是不保证同步。
  • ThreadLocal是想让线程间对某个变量不相互干扰,隔离开来。

即volatile是为了在共享变量时保证变量的可见性,而ThreadLocal是为了避免变量共享。

3.3 示例

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

在这里插入图片描述

从结果中可以看出,Tread0已经改变了formatter的值,但其他线程还是默认的格式,起到了隔离的作用。

3.4 原理

​ ThreadLocal提供了get和set方法来访问与其他线程的变量。查看这个方法的源码如下:

ThreadLocal.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;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}

可以看出,最终的变量是存放在当前线程的ThreadLocalMap中的。

在这里插入图片描述

3.5 ThreadLocal内存泄露问题

​ ThreadLocal中使用的Key为ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。假如我们不做任何措施的话,value永远无法被GC回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑这种情况了,在调用set、get、remove方法的时候会清理掉key为null的记录。使用完ThreadLocal方法后最好手动调用remove方法。

4 线程池

详见另一篇文章:线程池详解

5 Atomic原子类

​ 所谓原子类,简而言之就是将具有原子/原子操作特征的类。并发包的原子类都存放在java.util.concurrent.atomic下。

5.1 JUC包中的原子类是哪4类?

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型

使用原子方法更新数组中的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。
  • AtomicMarkableReference:原子更新带有标记位的引用类型。

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

5.2 AtomicInteger的使用

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

使用AtomicInteger之后,不用对increment()方法加锁也能保证线程安全。

class AtomicIntegerTest {
    private AtomicInteger count = new AtomicInteger();
    public void increment() {
        count.incrementAndGet();
    }
    
    public int getCount() {
        return count.get();
    }
}

5.3 AtomicInteger类的原理

​ AtomicInteger类主要是利用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。(具体单独总结)

6 AQS

​ AQS全称AbstractQueueSynchronizer抽象队列同步器。AQS是用来构建同步器的框架,使用AQS能简单且高效的构造出应用广泛的大量同步器,如:ReentrantLock、Semaphore等都是基于AQS的。

6.1 AQS概述

​ AQS顾名思义,抽象队列同步器,本意就是对锁的抽象,通过队列+LockSupport+state来保证同步的。锁分为共享锁和独占锁,独占锁就是在同一时刻只能被一个线程获得的锁,这两个锁都牵扯到互斥问题,这样就总有线程拿不到锁,为了避免CPU资源浪费(无关的CAS),所以我们需要将线程阻塞再唤醒。AQS就是这样一个工具。

6.2 实现原理

​ AQS是通过一个CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。它是一个虚拟的双向队列,不存在队列实例,仅存在节点之间的关联关系。原理图如下:

在这里插入图片描述

AQS通过使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作修改其值。

6.3 AQS对资源的共享方式

AQS的定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。为可分为公平锁和非公平锁:
    • 公平锁:按照线程的在队列的排队顺序来获取锁
    • 非公平锁:管他三七二十一,抢就完了
  • Share(共享):多个线程可以同时执行,如CountDownLatch、Semaphore、CycliBarrier、ReadWriteLock。

6.4 CountDownLatch的应用场景

)]

AQS通过使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作修改其值。

6.3 AQS对资源的共享方式

AQS的定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。为可分为公平锁和非公平锁:
    • 公平锁:按照线程的在队列的排队顺序来获取锁
    • 非公平锁:管他三七二十一,抢就完了
  • Share(共享):多个线程可以同时执行,如CountDownLatch、Semaphore、CycliBarrier、ReadWriteLock。

6.4 CountDownLatch的应用场景

​ CountDownLatch的作用就是允许count个线程阻塞在一个地方,直至所有线程的任务都执行完毕。可以使用CompletableFuture类来改进它。

参考:javaguide
Java并发编程的艺术
Java并发编程实战

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值