Loading

Java并发

并发三大特性:原子性、有序性、可见性。

 

请简要描述线程与进程的关系,区别及优缺点?

线程是比进程更小的一个执行单元,各个线程共用进程的方法区(Hotspot元空间)和堆,而线程的程序计数器、本地方法栈和虚拟机栈是私有的。各进程是独立的,而各线程极有可能会相互影响。线程执行切换开销小,但不利于资源的管理和保护;而进程正相反。 进程只是线程的容器,执行任何工作都依赖于线程。

线程是任务调度执行的最小单元,进程是资源分配的最小单元。进程之间相互隔离。

协程是线程内部的时分复用。

 

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

程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

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

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

 

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

  • 虚拟机栈: 每个 Java 方法(字节码)在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 为虚拟机使用到的 Native 方法

所以,为了保证线程中的局部变量不被别的线程访问到

 

并发&并行?

前者是通过时间片轮转,在宏观上达到同时运行多个任务的效果。

后者是严格意义地同时运行多个任务,在CPU多个核心上运行多个任务。

 

为什么要使用多线程呢?

  • 从计算机底层来说: 线程是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远小于进程。另外,多核 CPU多个线程可以同时运行,这减少了线程上下文切换的开销。
    • 单核时代: 单核多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进统资源的整体效率。
    • 多核时代: 多核时代多线程主要是为了利用多核 CPU 的能力。。。。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以提高系统并发能力以及高并发条件下的系统性能。

当然多线程技术也存在问题,主要是安全问题:内存泄漏 死锁 线程不安全等

 

线程状态:

线程在new之后会处于new状态,调用start()后进入runnable(首先进入ready状态,随后获取CPU 空闲的时间片后进入running,实际上一个线程一次仅仅占用几十毫秒时间,随后就会进入ready等待下一时间片,所以可以不区分这两种状态)。

  • 调用wait()后进入等待(waiting),此时该线程等待其他线程调用notify()通知后回到运行状态。
  • 超时状态是运行状态调用带超时参数的sleep(long millis)或 wait(long millis)后进入,在达到超时时间后返回运行状态。
  • 执行synchronized块或方法后,若没有获取到锁,线程进入阻塞状态,获取到锁后回运行状态。
  • 线程执行完毕后进入终止状态,线程完成。

 

什么是上下文切换?

就是一个线程在主动或被动退出运行状态,比如:

通过sleep或者wait,或者在阻塞状态中没获取到锁,或者当前时间片用完,

此时这个线程会保存自己当前的运行状态,如程序计数器、虚拟机栈、本地方法栈,这就是上下文,以备线程下次运行时恢复加载。

但因为每次切换都要使用CPU和内存保存或加载上下文,导致了额外的开销,所以频繁切换会导致性能下降。

 

线程死锁是什么?

//同步监视器,俗称:锁。任何一个对象,都可以充当锁。

多个线程以不同的顺序请求对方的资源,导致全部在阻塞状态无限期等待某个资源被释放的状态。

死锁产生的条件:

  1. 互斥:一个资源同时只能被一个线程所使用。
  2. 不剥夺:任何一个资源在被一个线程占用时都无法被其他线程剥夺。
  3. 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  4. 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。

 

如何避免死锁?

  1. 破坏互斥:比如像ConcurrentHashMap以前那样,做分段锁,那么就可以多线程去访问不同的段。
  2. 破坏请求与保持条件 :一次性申请所有的资源。
  3. 破坏不剥夺条件 :占用锁线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按相同顺序申请资源,释放资源则反序释放。破坏循环等待条件。

 

 

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

二者都可以暂停进程,但sleep没有释放锁,而wait释放了锁。wait是Object类的方法,而sleep是Thread类的方法。

wait之后,该线程必须等到其他线程进行Notify后才会恢复运行。而sleep后,线程会在等待时长后自动恢复运行。

因此wait主要用于线程之间的通信。sleep主要用于暂停进程。

 

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

因为Run方法的本质是在本线程的内部对Runnable对象调用Run方法,直接调用Run方法相当于是在主线程中去调用一个普通方法,而并没有用到多线程技术,因为子线程还根本就没有Start。

而调用Start方法,是启动一个子线程,再去调用该对象的run方法。的因此我们要在Start中去启用子线程。

 

synchronized关键字

synchronized的作用:

保证不同线程在使用同一资源的同步性,使得它所修饰的方法或代码块在同一时间仅仅有一个线程可以执行。

synchronized使用方法:

  1. 修饰普通方法:

synchronized void method(),作用于当前的实例对象,方法执行前要获得当前对象的锁

  1. 修饰静态方法:

synchronized static void method(), 作用于当前类的所有对象,方法执行前要获得当前类的锁。因为静态成员是不属于对象而属于类的,因此若A线程调用该类某对象非静态sync方法,而B线程调用该类静态sync方法,是不会发生阻塞的。因为二者占用的是不同的锁。

  1. 修饰代码块:

synchronized(Obj){//业务代码},作用于指定对象Obj,代码块执行前要获取该对象的锁,也可以synchronized(类名.class)作用于当前类

底层原理: 

synchronized修饰代码块:在JVM层面调用了monitorenter和monitorexit,这两个命令是基于锁计数器的。

monitorenter:若当前锁计数器为0,就获取锁,然后锁计数器加1,表示当前锁对其他线程不可用。

monitorexit:若当前进程为锁的持有者,尝试释放锁后锁计数器减1,表示当前锁已被释放。

synchronized修饰方法:在JVM层面读取一个方法是否被标志为同步方法,之后去获取对象的锁或者class的锁。

 

单例模式?

单例模式就是一个类只用来创建一个实例,并且该实例是该类的一个成员,添加成员getinstance方法,判断若该类不存在实例,调用构造函数,并且在构造之前,并且给创建外加上一个synchronized的代码框给该类上锁。

双重校验:

 

synchronized ReentrantLock 的区别

  • 都是可重入锁,即一个线程可以获取两次某个对象的锁。
  • 实现:sync是JVM底层实现,而ReentrantLock是一个类,由实现。
  • ReentrantLock可以中断等待,使当前线程放弃等待锁而去处理其他事情。
  • ReentrantLock可以指定公平锁或非公平锁,而sync不能。也就是先阻塞的进程先获得锁。
  • ReentrantLock锁可以绑定多个条件,以选择性地通知其他进程解除等待。

 

volatile

保证数据可见性。JVM会将经常使用的变量保存在线程私有的缓存中,在多线程情况下, 这会产生数据不一致。volatile修饰的变量允许线程直接在共享内存中读取该变量,从而避免数据不一致。也禁止了JVM的指令重排,使多线程能够正常运行。

 

ThreadLocal

ThreadLocal是一个类,作用是表达线程内部的变量,不进行同步以实现线程安全,也就是数据隔离。创建一个ThreadLocal变量,那么访问这个变量得所有线程都会在线程本地内存中拥有一个该变量的副本,并仅仅对这个副本进行操作, 从而保证了数据隔离。

原理:每个线程保存了一个特殊的ThreadLocalMap,默认为null,调用get或set后,键是ThreadLocal实例,值是通过set设置的变量值。变量的实际值是被放在当前线程的ThreadLocalMap中。

内存泄露:在使用完一个ThreadLocal之后,最好手动调用其remove方法,因为Map的key是ThreadLocal的弱引用,而value是强引用,可能会出现key被垃圾回收,而value则没有被回收,出现泄露。

 

创建线程的几种方式:

  • 继承Thread类并重写run方法,在主线程中调用对象的start方法。
  • 实现Runnable并实现run方法,在主线程中调用对象的start方法。
  • Callable接口
  • 线程池,无需关心线程的创建、销毁,只需要给他提交任务。不过线程池内部addWorker还是调用的Threadstart方法

 

Callable接口:call方法相比于Runnable的run方法,可以返回值、返回异常

 

线程池:

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

如何创建线程池?

通过ThreadPoolExectutor的构造函数或Executors类创建。

前者更规范更安全,支持相当多的参数。

 

ThreadPoolExecutor本质是线程的容器,用来执行任务(如Runnable或Callable):

  • corePoolsize:核心线程数量,也就是同时运行线程的最低数量。
  • workQueue:任务队列,若已经达到了核心线程数量,任务将进入队列。
  • maxPoolsize: 任务队列满后,继续创建线程,最大线程数量,相当于救急线程。
  • keepAliveTime 核心线程以外的闲置线程,Time Unit时间单位

拒绝策略:达到最大线程数量后,丢弃抛出/不抛出异常、喜新厌旧、由提交任务的线程执行该任务。

原理:通过Runnable接口的对象创建任务,并用线程池的execute方法执行任务,若满足刚才说得条件,则创建线程并启动。

 

原子类:具有不可中断性质的类。不需要人为加锁也可以实现线程安全。分为四类:

  • 基本数据类:整型、长整、布尔
  • 引用:引用类、带版本号得引用类、带标记位的引用类
  • 数组:整型数组、长整数组、引用数组。
  • 更新器:更新上述三类原子类的更新器

 

AtomicInteger

常用方法:获取值、设置值、CAS、增加值等等

原理:使用CAS(比较并赋值,当内存值与期望值相同时,再更新赋值),调用了native方法,并且使用volatile,保证直接从内存中取值。不需要synchronized也可以线程安全。减少了sync的开销

 

CAS: 比较并赋值,当内存值与期望值相同时,再更新赋值

是一个本地方法,原理就是比如线程1想要修改某变量的值,但是线程2抢先修改了该变量,这就导致线程1在调用cas的时候,预期值与内存值不同,也就无法修改,实现了类似于线程阻塞的效果。比sync开销更小,CAS只能对变量保证原子性,而不能如sync一样对方法。

 

AQS 介绍

是用来自定义构建锁和同步器的工具,比如ReentrantLock。

原理:是相当自然的,如果某线程去请求一个未被占有的资源,则将该线程设置为有效线程,该资源设置为锁定。若请求一个已经锁定的资源,那么该线程会由CLH锁队列去进行一系列如阻塞、等待、唤醒等机制去处理,类似于一个自定义版本的synchronize机制。通过CLH队列可以保证各线程排队访问某上锁资源。

CountDownLatch计数锁,当该计数锁的计数值下降为0时,因为调用该锁await方法而处于等待中的线程就会恢复运行。

  • 某一线程调用await方法,等待n个线程执行完毕后计数值降为0,开始运行。
  • 例如下载一个几十G的游戏,通常都是压缩包分卷,等所有分卷下载完后countdownlatch下降为0,安装程序开始安装。

CyclicBarrier: 给一组线程使用,线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有线程同时运行。相当于一组线程在等待到达同一个同步点后再同时运行。相当于赛跑,等运动员都各就位了到达同步点了,所有运动员再同时起跑。

  • 区别:countdownlatch是一个线程等待多个任务完成,而cyclicbarrier是多个线程等待到一个同步点后再同时开始。

Semaphore信号量

 

java锁类型

  • 乐观锁/悲观锁:乐观锁,假定不存在冲突,而更新时会,类似CAS与Mysql版本链。悲观:读或者写都会上锁,类似sync
  • 独享锁/共享锁: 能否一次被多个线程持有,ReadWriteLock。读共享,写独享。
  • 可重入锁:公平锁/非公平锁:分段锁:
  • 自旋锁:当某线程获取不到锁不会立即阻塞,而是循环尝试获取
  • 偏向锁/轻量级锁/重量级锁:指的是sync的一种优化机制。是对象的状态。
  • 偏向锁:某资源一直被一个线程所持有,当前线程多次访问不会频繁释放获取。
  • 轻量级锁:当其他线程请求该资源,升级为轻量级。底层使用CAS保证线程安全
  • 重量级:处于自旋状态的锁在自旋一定次数后,升级为重量级锁。使用管程确保线程安全
  • 锁消除:JVM在检测到没有多线程竞争某资源的情况时,会消除掉该资源的锁
  • 锁粗化:若频繁对同一个对象加锁释放锁,那么会在该对象的外层加锁。
 

 

 

 
posted @ 2022-04-02 18:08  吉比特  阅读(65)  评论(0编辑  收藏  举报