→ 并发与并行

什么是并发、什么是并行

并发:指应用程序能够交替执行不同的任务,并发有点类似于多线程的原理,多线程并非同时执行多个任务,如果你开两个线程执行,就是在你几乎察觉不到的速度不断去切换这两个线程,已达到“同时执行的效果”,其实只是计算机以察觉不到的速度在切换不同的线程。
并行:指应用能够同时执行不同的任务。
区别:一个是交替执行,一个是同时执行。

→ 线程与进程的区别

线程的实现、线程的状态、优先级、线程调度、守护线程

线程实现的三种方式:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。
    • 1.创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
    • 2.创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
    • 3.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
    • 4.调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

      java.util.concurrent.Callable是一个泛型接口,只有一个call()方法。 call()方法抛出异常Exception异常,且返回一个指定的泛型类对象

  1. // 创建实现类 实现call()方法
  2. public class ThreadCall implements Callable<String> {
  3. @Override
  4. public String call() throws Exception {
  5. // TODO Auto-generated method stub
  6. System.out.println("=====");
  7. return "9999";
  8. }
  9. }
  10. // 使用FutureTask包装Callable对象,并启动
  11. public class TestThread {
  12. public static void main(String[] args) {
  13. FutureTask<String> ft = new FutureTask<>(new ThreadCall());
  14. new Thread(ft).start();
  15. }
  16. }

线程的状态:
新建状态:使用new关键字创建一个Thread类对象,该线程处于新建状态,进入运行状态执行start()方法。
就绪状态:处于就绪状态,等待JVM线程调度, 进入running状态。调用yield()(让步)方法转回非运行状态。
运行状态:运行状态,调用sleep()或者join()可以使当前线程进入一种阻塞状态,等待自行结束返回就绪状态。
使用synchronized同步代码使其进入锁池状态,即同步阻塞,锁结束后可以返回就绪状态。使用wait()方法也可以使当前线程进入等待阻塞,需要调用notify()或notifyAll()方法或者interrupt()方法。
阻塞状态:阻塞状态大致分为一般阻塞,等待阻塞,同步阻塞三种,需要等调用方法结束或调用恢复方法使其进入就绪状态。
销毁状态:运行状态run()方法结束后线程就会进入死亡状态。
Java多线程-线程池 - 图1
线程优先级:Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
线程调度:如果一个计算机只有一个CPU,那么在任意时刻只能执行一条指令,每个线程只能获得CPU使用权才能执行指令。从宏观上看,多线程的并发运行是每一个线程轮流获得CPU的使用权,分别执行各自的任务。但在运行池中,会有多个处于就绪状态的线程在等待CPU,JVM的一项任务就是负责线程的调度,即按照特定的机制为多个线程分配CPU使用权。调度模型分为分时调度模型和抢占式调度模型两种。
分时调度模型是让所有线程轮流获取CPU使用权,平均分配每一个线程占用CPU的时间片。抢占式调度模型是优先让可运行池中优先级高的线程占用CPU,若运行池中线程优先级相同,则随机选择一个线程使用CPU,当它失去CPU使用权时,再随机选取一个线程获得CPU使用权。JAVA默认使用抢占式调度模型。
守护线程:又称后台线程,JVM的垃圾回收线程就是典型的后台线程。即如果前台线程都死亡, 后台线程会自动死亡。当整个虚拟机中只剩下后台线程,程序就没有继续运行的必要了, 所以JVM就退出了。
可以调用Thread中的setDaemon(true)。将线程设置为守护线程。另外,Thread类还提供了isDaemon()用于判断线程是否是守护线程。
【参考链接】https://blog.csdn.net/qq_41946557/article/details/101107004

线程与进程的区别

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程。
每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。 线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化。
区别:

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
  • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。
  • 两者均可并发执行。

优缺点:
线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。
使用场景:
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
【参考链接】https://blog.csdn.net/feiBlog/article/details/85397287

如何停止一个正在运行的线程

1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
3、使用interrupt方法中断线程。

为什么wait和notify方法要在同步块中调用

在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对
象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。

java 中守护线程和本地线程区别

java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(boolon);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon() 必须在 Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果 全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可 以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的 线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产 生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。
扩展:
Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护 进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程。

Java 中用到的线程调度算法

采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优 先级上,如非特别需要,尽量不要用,防止线程饥饿。

→ 线程池

自己设计线程池、submit() 和 execute()、线程池原理

线程池: 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
作用:1. 避免大量的线程强占资源 2.避免大量的线程创建和销毁带来的开销
结构:1、线程池管理器(ThreadPoolManager):用于创建并管理线程池
2、工作线程(WorkThread):线程池中线程
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。
4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。
Java多线程-线程池 - 图2
submit()和execute()的区别:
JDK5往后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,它们的区别是:
1、接收的参数不一样

  • 都可以是Runnable
  • submit 也可以是Callable

2、submit有返回值,而execute没有

  • execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。——实现Runnable接口
  • submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。——实现Callable接口

3、submit方便Exception处理

  • 如果你在你的task里会抛出checked或者unchecked exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。

【参考链接】https://blog.csdn.net/debugging_bug/article/details/78409683

为什么不允许使用 Executors 创建线程池

阿里巴巴java开发手册中明确指出,不允许使用Executors创建线程池。
Java多线程-线程池 - 图3
线程池体系结构
java.util.concurrent.Executor:负责线程使用和调度的根接口
——>ExecutorService :Executor的子接口,线程池的主要接口
————>ThreadPoolExecutor:线程池的实现类
————>ScheduledExcutorService:子接口:负责线程的调度
——————>ScheduledThreadPoolExecutor:继承ThreadPoolExecutor 实现ScheduledExcutorService

工具类 : Executors ExecutorService newFixedThreadPool() : 创建固定大小的线程池 ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。 ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

我们可以通过ThreadPoolExecutor来创建一个线程池。 创建一个线程池需要输入几个参数: corePoolSize - 线程池核心池的大小。 maximumPoolSize - 线程池的最大线程数。 keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。 unit - keepAliveTime 的时间单位。 workQueue - 用来储存等待执行任务的队列。 threadFactory - 线程工厂。 handler - 拒绝策略。

newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会消费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool:主要问题是线程最大数是Integer.MAX_VALUES,可能会创建数量非常多的线程,甚至OOM。
【参考链接】https://blog.csdn.net/fly910905/article/details/81584675

→ 线程安全

死锁、死锁如何排查、线程安全和内存模型的关系

死锁:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
排查:1.使用jdk自带的工具。首先进入cmd命令,进入jdk安装目录下的bin目录下,执行jps命令查看端口号,得 到运行的线程的id,再执行jstack命令,查看结果。(或者使用idea Terminal终端运行命令)
2.使用jconsole,在window打开 JConsole,JConsole是一个图形化的监控工具! (在windons命令窗口 ,输出 JConsole)
3.使用Java Visual VM
Java内存模型
Java多线程-线程池 - 图4
Java内存模型简称JMM,控制线程之间的通信
当多个线程对共享属性据同时进行操作时,由于本地内存数据不会立即更新到主内存,所以会导致两个线程拿到的同一数据不一样,即线程不可见性,volatile可以解决该问题,但是volatile无法保证原子性,
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
然而,指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到多线程的执行结果。
内存屏障可以解决, 内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
内存屏障共分为四种类型:
LoadLoad屏障
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
LoadStore屏障:
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
volatile做了什么?
在一个变量被volatile修饰后,JVM会为我们做两件事:
1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
volatile特性之一:
保证变量在线程之间的可见性。可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。
volatile特性之二:
阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
线程安全和内存模型https://blog.csdn.net/mikeoperfect/article/details/79133899

→ 锁

CAS、乐观锁与悲观锁、偏向锁、轻量级锁、重量级锁、可重入锁、自旋锁、阻塞锁

CAS:Compare and Swap,即比较再交换。
jdk5增加了并发包java.util.concurrent.,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁:悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
偏向锁:Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果一段同步代码一直被一个线程访问, 那么该线程会自动获取锁,降低获取锁的代价,即给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,偏向锁会升级到轻量级锁。
轻量级锁: 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁: 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
可重入锁: 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
自旋锁: 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
*阻塞锁:
多个线程同时调用同一个方法的时候,所有线程都被排队处理了。让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
Java多线程-线程池 - 图5
【参考链接】https://www.cnblogs.com/hustzzl/p/9343797.html
https://www.cnblogs.com/kubidemanong/p/10777928.html

数据库相关锁机制、锁优化、锁消除、锁粗化

数据库相关锁机制:数据库锁是事务对某个数据库中的资源(如表和记录)存取前,先向系统提出请求,封锁该资源,事务获得锁后,即取得对数据的控制权,在事务释放它的锁之前,其他事务不能更新此数据。当事务撤消后,释放被 锁定的资源。
Java多线程-线程池 - 图6
共享锁: 又叫S锁或者读锁,加了共享锁的数据对象可以被其他事务读取,但不能修改, 通常是该数据对象被读取完毕,锁立即被释放。
排他锁: 又叫X锁或者写锁,当数据对象被加上排它锁时,一个事务必须得到锁才能对该数据对象进行访问,一直到事务结束锁才被释放。 在此之间其他的事务不能对它读取和修改。
行锁: 行锁,字面意思理解,就是给某一行加上锁,也就是一条记录加上锁。比如之前演示的共享锁语句。
SELECT from city where id = “1” lock in share mode; 由于对于city表中,id字段为主键,就也相当于索引。执行加锁时,会将id这个索引为1的记录加上锁,那么这个锁就是行锁。
锁优化
Java多线程-线程池 - 图7
1、减少锁持有时间
例如:对一个方法加锁,不如对方法中需要同步的几行代码加锁;
2、减小锁粒度
例如:ConcurrentHashMap采取对segment加锁而不是整个map加锁,后来又对node加锁,提高并发性;
3、锁分离
根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性。
4、锁粗化
这看起来与思路1有冲突,其实不然。思路1是针对一个线程中只有个别地方需要同步,所以把锁加在同步的语句上而不是更大的范围,减少线程持有锁的时间;
而锁粗化是指:在一个间隔性地需要执行同步语句的线程中,如果在不连续的同步块间频繁加锁解锁是很耗性能的,因此把加锁范围扩大,把这些不连续的同步语句进行一次性加锁解锁。虽然线程持有锁的时间增加了,但是总体来说是优化了的。
5、锁消除
锁消除是编译器做的事:根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程(即不会影响线程空间外的数据),那么可以认为这段代码是线程安全的,不必要加锁。
*锁消除:
锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。 比如,StringBuffer类的append操作:

  1. @Override
  2. public synchronized StringBuffer append(String str) {
  3. toStringCache = null;
  4. super.append(str);
  5. return this;
  6. }
  7. 1
  8. 2
  9. 3
  10. 4
  11. 5
  12. 6

从源码中可以看出,append方法用了synchronized关键词,它是线程安全的。但我们可能仅在线程内部把StringBuffer当作局部变量使用: 因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
锁粗化: 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
【参考链接】https://www.cnblogs.com/xdecode/p/9137804.html
https://blog.csdn.net/hefenglian/article/details/82421770

分布式锁、monitor

分布式锁: 为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
分布式锁具备的条件:

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁的实现有哪些:

  • Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。
  • Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。
  • Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
  • Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

monitor:在JVM的规范中,有这么一些话:
“在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的”
“为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁”
“锁住了一个对象,就是获得对象相关联的监视器”
从这些话,看出监视器和对象锁好像是一回事,那为何要定义两个东西,若不一样,他们的关系如何?
lock与monitor的区别
1.lock的底层本身是Monitor来实现的,所以Monitor可以实现lock的所有功能
2.Monitor有TryEnter的功能,可以防止出现死锁的问题,lock没有。
3.Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,当然在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
5.Lock关键字实际上是一个语法糖,它将Monitor对象进行封装,给object加上一个互斥锁,A进程进入此代码段时,会给object对象加上互斥锁,此时其他B进程进入此代码段时检查object对象是否有锁?如果有锁则继续等待A进程运行完该代码段并且解锁object对象之后,B进程才能够获取object对象为其加上锁,访问代码段。
【参考链接】分布式锁:https://www.jianshu.com/p/a1ebab8ce78a
monitor:https://www.cnblogs.com/wangyonglai/p/8241724.html