并发编程相关知识
1:什么是线程?什么是进程?
进程:进程是程序允许的最基本的单位,是程序的一次执行过程,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程:线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
2:进程于线程的关系,区别,优缺点。
进程是程序运行的最基本单位,线程是比进程更细小的单位,一个进程可能包含多个线程。多个线程之间共享堆和方法区的资源。但是每个线程用友自己的程序计数器、虚拟机栈和本地方法区。
总之:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
3:堆和本地方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
4:关键词解读
并发:两个或两个以上的作业在同一时间段运行。
并行:两个或两个以上的作业在同一时刻运行。区别在于是否是同一时刻。
同步:程序运行,知道拿到结果才结束。发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步:程序运行,无法拿到程序结果就结束。调用在发出之后,不用等待返回结果,该调用直接返回。
5:线程的生命周期。
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。 - RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 - BLOCKED :阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
java线程状态变迁图
6:Wait、Sleep、Join、Notify、NotifyAll之间的关系和区别。
sleep():当前线程调用Thread.sleep(1000)陷入休眠,进入TIMED_WAITING状态,同时系统内核中会根据sleep中的参数设置一个定时器,定时器倒计时结束后,内核会重新唤醒线程,线程状态进入RUNNABLE状态;
yield():线程状态在RUNNABLE状态下,由系统cpu决定是否执行,所以该状态下,线程在内核中实际有“运行中”和“就绪”两种状态,当前线程在“运行中”时,调用Thread.yield(),会立即让出cpu的使用权,让cpu执行优先级更高的或其它同优先级的线程,线程从RUNNABLE状态下的“运行中”变为“就绪”。
wait():当前线程获取Object锁后,调用Object的wait方法,则会使当前线程进入WAITING或TIMED_WAITING状态,并释放Object的持有锁,当前线程会被放入等待队列中,直到超时或者被其他线程调用锁对象的notify方法唤醒。wait的用法,必须配合synchronized使用,且使用的必须为同一个对象:synchronized (A)配合A.wait()使用,当线程执行到object.wait()时,此线程会同时释放锁synchronized (object);当它结束了wait后,此线程又会重新去争抢锁synchronized (object)。
示例代码:
private static Object object = new Object(); public static void main(String[] args) { ThreadTest threadTest = new ThreadTest(); for (int i = 1;i<=5;i++){ Thread01 thread01 = threadTest.new Thread01("thread"+i); thread01.start(); } } class Thread01 extends Thread{ public Thread01(String threadName){ super(threadName); } @Override public void run(){ synchronized (object){ // System.out.println("线程"+super.getName()+"开始"); try { if(super.getName().contains("5")){ object.notify(); object.notifyAll(); }else{ object.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程"+super.getName()+"执行完成"); } } }
join():内部其实就是wait方法,不同于wait的是,它会主动等使用了Object的锁对象的线程彻底执行结束后,自动从WAITING状态进入RUNNABLE状态。
notify()/notifyAll(): 当前线程获取锁后,调用Object的notify/notifyAll方法,会使此前调用了该Object的wait线程从WAITING状态进入RUNNABLE状态,notify只会唤醒一个线程,而notifyAll方法可以唤醒所有线程。
sleep和wait的区别:
1:sleep是Thread的方法,wait的是Object的方法
2:Thread.sleep智慧让出CPU,不会导致锁行为的改变,Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
3:sleep可以在任意地方使用,wait必须结合synchronized,比如结合notify/notifyAll使用,并且锁的钥匙必须为同一个对象。
7:死锁
什么是死锁,死锁产生的必要条件?
在使用多线程以及多进程时,两个或两个以上的运算单元(进程、线程或协程),各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,就称为死锁。
public static void main(String[] args) throws InterruptedException { Object resource1 = new Object(); Object resource2 = new Object(); new Thread(() ->{ synchronized (resource1){ String name = Thread.currentThread().getName(); try { System.out.println(name+"拿到了第一把锁resource1"); Thread.sleep(1000); System.out.println(name+"等待获取第二把锁resource2"); synchronized (resource2){ System.out.println(name+"拿到了第二把锁resource2"); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(() ->{ synchronized (resource2){ String name = Thread.currentThread().getName(); try { System.out.println(name+"拿到了第二把锁resource2"); Thread.sleep(1000); System.out.println(name+"等待获取第一把锁resource1"); synchronized (resource1){ System.out.println(name+"拿到了第二把锁resource1"); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }
output
Thread-0拿到了第一把锁resource1 Thread-1拿到了第二把锁resource2 Thread-1等待获取第一把锁resource1 Thread-0等待获取第二把锁resource2
死锁的必要条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
8:volatile 关键字。
1:可见性。在 Java 中,volatile
关键字可以保证变量的可见性,如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
2:禁止指令重排。应用单例模式的实现(双重锁模式)
public class SingleModel { private static volatile SingleModel instance; public static SingleModel getInstance(){ if(instance == null){ synchronized (SingleModel.class){ instance = new SingleModel(); } } return instance; } private SingleModel(){ } }
3:不保证原子性。
9:synchronized 关键字
修饰实例方法(锁当前对象实例),给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
修饰静态方法,给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
静态 synchronized
方法和非静态 synchronized
方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
修饰代码块(锁指定对象/类)
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁。
10.Lock、ReentrantLock
可重入锁:也叫做递归锁,指同一个线程外层函数获取锁以后,内部递归锁仍然可以获取该锁的代码,同一个线程在外层方法获取锁的时候,在进入内层方法,会自动获取锁,也就是说线程可以进入任何一个他已经拥有锁的代码块。
不可重入锁:不重入锁不能重复进入。
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
隐式锁(即synchronized关键字使用的锁)默认是可重入锁,显式锁(即Lock)也有ReentrantLock这样的可重入锁。
可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。
不可重入锁也叫自旋锁。
11.ThreadLocal
作用是使每一个线程都拥有自己的专属变量。让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
内部方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
实现原理:数据存储在ThreadLocalMap中,ThreadLocal是对ThreadLocalMap的封装,传递了变量值。ThreadLocalMap是Thread的一个成员变量。
ThreadLocal内存泄漏的问题:因为ThreadLocalMap的key是ThreadLocal的弱引用,而Value是强引用。Key在ThreadLocal没有被其他对象强引用的情况会被垃圾回收机制自动回收。导致Entry会出现key未null的数据,从而导致这部分数据永远不会被垃圾回收机制回收,导致内存泄露。所以在使用ThreadLocal的时候需要在用完以后手动调用remove方法,避免内存泄露的发生。
ThreadLocal衍生子类:
InheritableThreadLocal:
父线程传递本地变量到子线程的解决方式及分析,用于子线程继承父线程的数值。将通过重写initialValue() 与childValue(Object parentValue)两个方法来展示。实质是因为线程在init的时候复制了inheritableThreadLocals到子线程,线程池不可用,因为线程池的线程是复用的不会去调用Thread的init方法。
TransmittableThreadLocal:实现线程池中创建好的线程可以进行值传递,无法传递值的问题。为什么呢?线程池的中的线程只能创建一次,回到InheritableThreadLocal上面说的步骤,传递的时机只有new Thread()才会出现。队列未满,线程池到了最大核心线程数就会停止创建,在这些线程未销毁前,父线程更新InheritableThreadLocal定义的变量,线程池中的线程拿的还是之前的InheritableThreadLocal变量的值。
12:线程池
作用:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
创建方式:
通过Executor框架的Executors创建
通过构造方法ThreadPoolExecutor 创建(推荐)。构造函数中的参数详解
public ThreadPoolExecutor(int corePoolSize, //核心线程数 核心线程数定义了最小可以同时运行的线程数量 int maximumPoolSize, //最大线程数 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数 long keepAliveTime,//当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,
核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; TimeUnit unit, //keepAliveTime的单位
BlockingQueue<Runnable> workQueue, //缓存队列 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,
如果达到的话,新任务就会被存放在队列中。 ThreadFactory threadFactory,//executor 创建新线程的时候会用到 RejectedExecutionHandler handler) {//饱和策略 if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
线程池饱和策略:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
: 抛出 RejectedExecutionException
来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy
: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
核心线程数:也是最小线程数,是指初始化一个线程池会初始化的线程数。
缓存队列:数据提交到线程池,核心线程数是否满了,满了的话进入缓存队列,采取先进先出的原则。如果缓存队列没有满之前会现在缓存队列中排队,如果队列满了,则新建线程来处理,直到线程数达到最大线程数。
最大线程数:缓存队列满了以后,新来的任务会新建线程执行,当达到最大线程数,会触发设定的饱和策略。
13:Atomic原子类
Atomic
翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。