什么是线程和进程
进程是程序的一次执行过程,是系统运行程序的基本单位,是动态的。在Java中启动main函数其实就是启动了JVM的进程,而main函数所在的线程就是这个进程的一个线程,主线程。
线程是一个比进程更小的执行单位,同类的多个线程共享进程的堆和方法区资源,每个线程都有自己的程序计数器、虚拟机栈、本地方法栈。
程序计数器为什么是私有的
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
- 字节码解释器通过改变程序计数器依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 多线程的情况下,程序计数器用户记录当前线程执行的位置,从而当线程切换回来的时候能够知道该线程上次运行到哪个位置。
虚拟机栈和本地方法栈为什么是私有的
虚拟机栈:每个Java方法在执行的时候,会创建一个栈帧来存储局部变量表、操作数栈、常量池引用等信息。从方法调用直到执行完成,就对应一个栈帧在Java虚拟机栈中入栈和出栈的过程。
本地方法栈:和虚拟机栈发挥作用相似,区别:虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈是虚拟机使用到的Native方法服务。
因为为了保证线程中的局部变量不被其他线程访问到,虚拟机栈和本地方法栈是私有的。
了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象在这路分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
为什么使用多线程
- 计算机底层来说:线程是程序执行的最小单位,线程之间的切换和调度的成本远远小于进程。多核CPU意味着多个线程可以同时运行,减少了线程上下文切换的开销。
- 单核时代:只有一个线程的话,CPU进行计算时,IO设备空闲;进行IO时,CPU设备空闲。
- 多核时代:创建多个线程可以让多个CPU被利用到
使用多线程可能带来什么问题
- 内存泄漏
- 上下文切换
- 死锁
线程的生命周期和状态
NEW:初始状态,线程被构建,但是还没调用start()
Runnable: 运行状态,Java线程就绪和运行都称为运行中
Blocked:阻塞状态,表示线程阻塞于锁
Waiting:等待状态,线程进入等待状态,该状态表示当前线程需要等待其他线程做出一些特定动作
Time_Waiting:超时等待,改状态不同于Waiting,他可以在指定的时间自行返回
Terminated:终止状态,表示当前线程已经执行完毕
什么是上下文切换
一个CPU核心在任意时刻只能被一个线程使用,为了这些线程都能得到有效执行,CPU的策略是给每个线程分配时间片并轮转。一个时间片用完就会重新处于就绪状态让给其他线程使用,这个过程就是一次上下文切换。
当前任务执行完CPU时间片切换到另外一个任务之前会先保存自己的状态,方便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换对系统来说,意味着消耗大量的CPU时间,Linux的优点就是上下文切换和模式切换的时间消耗非常少。
死锁
四个条件:
- 互斥条件:该资源任意时刻只能由一个线程占用
- 请求与保持:一个进程因请求资源而阻塞的时候,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁
- 互斥条件:这个没办法破坏,用锁本来就是要让他们互斥的(临界资源需要互斥访问)
- 请求于保持:一次性申请所有的资源
- 不剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放他占有的资源
- 循环等待:按序申请资源来预防。按照某一顺序申请资源,释放资源则反序释放。
sleep()方法和wait()方法
sleep()方法没有释放锁,wait()方法释放了锁
两者都可以暂停线程的执行
wait()通常是用于线程间通信,sleep()通常是用于暂停执行
wait()方法调用后,线程不会自动苏醒,需要其他线程调用同一对象上的notify()或者notifyAll()方法,也可以使用wait(long timeout)超时后线程自动苏醒。sleep()方法执行完成后,线程会自动苏醒。
为什么调用start()方法会执行run()方法,为啥不直接调用run()呢
调用start(),会启动一个线程并进入就绪状态,当分配到时间片就可以开始运行了。start()会执行线程的准备工作,然后自动自行run()的内容,这是真正的多线程工作。但是直接执行run()方法的话,会把run()方法当做main线程下的一个普通方法去执行,并不会在某个线程下执行它。
synchronize关键字的了解
synchronize解决多个线程之间反问资源的同步性,synchronize关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
自己是怎么使用synchronize关键字的
主要三种使用方式:
- 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method(){
//业务代码
}
-
修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员,所以如果一个线程A调用一个实例对象的非静态synchronize方法,而线程B需要调用这个实例对象所属类的静态方法,是运行的,不会出现互斥对象,因为访问静态synchronize方法占用的锁是当前类的锁,访问非静态synchronize方法占用的锁是当前实例对象的锁。
-
修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁。
总结:
- synchronize关键字加到静态方法和synchronize(class)代码块上都是给Class类上锁。
- synchronize关键字加到实例方法上是给对象实例加锁
- 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!
双重校验锁实现对象单例(线程安全)
1.为uniqueInstance 分配内存空间
2.初始化uniqueInstance
3.将uniqueInstance指向分配的内存地址
由于JVM具有指令重排的特效,多线程环境下可能导致一个线程获得还没有初始化的实例。
public class Singleton {
//使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
public volatile static Singleton uninitialized;
private Singleton() {
}
public static Singleton getUninitialized(){
//先判断对象是否已经实例化过,没有实例化过才进入加锁代码
if (uninitialized == null){
//类对象加锁
synchronized (Singleton.class){
if (uninitialized == null){
uninitialized = new Singleton();
}
}
}
return uninitialized;
}
}
构造方法可以用synchronize修饰吗
不能,因为构造方法本来就属于线程安全的,不存在同步的构造方法
synchronize关键字的底层原理
为什么要弄一个CPU高速缓存
类比开发网站后台系统使用的缓存redis,是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。CPU缓存是为了解决CPU处理速度和内存处理速度不对等的问题。可以把内存看做外存的高速缓存,程序运行的时候吧外存的数据复制到内存,由于内存的处理速度远远高于外存,这样子提高了处理速度。
总结:CPU Cache 缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。
synchronize 和 volatile关键字
- volatile关键字是线程同步的轻量级实现 所以性能比synchronize好,但是volatile关键字只能用户变量而synchronize关键字可以修饰方法以及代码块
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronize关键字都能保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronize关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
通常情况下,我们创建的变量可以被任何一个线程访问并修改的,如果想实现每一个线程都有自己的专属本地变量应该如何解决?JDK提供的ThreadLocal类正是为了解决这样子的问题。ThreadLocal类主要解决的是让每个线程绑定自己的值,可以将ThreadLocal比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
ThreadLocal内存泄漏
ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用。所以如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样子ThreadLocalMap就会出现key为null的Entry。
假如我们不做任何措施的话,value永远无法被GC回收,这个时候就会产生内存泄漏。ThreadLocalMap实现中已经考虑了这种问题,在调用set、get、remove方法的时候,会清理掉key为null的记录。使用完ThreadLocal后最好手动调用一下remove()方法。
线程池
为什么要用线程池
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
- 降低资源消耗:通过重复利用已创建的线程减低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达的时候,任务可以不需要等到线程创建就能立刻执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
实现Runnable接口和Callable的区别
Runnable接口不会返回结果或抛出检查异常,但是Callable接口可以。所以如果任务不需要返回结果或者抛出异常的话推荐实验Runnable接口,代码更加简洁。
执行execute()方法和submit()方法的区别是什么
execute()方法用于提交不需要返回值的任务
submit()方法用于提交需要返回值的任务。
ThreadPoolExecutor类
ThreadPoolExecutor构造函数重要参数
三个最重要的参数:
- corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量
- maximumPoolSize:当队列中存放的任务打到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workqueue:当新任务来的时候,会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会存放在队列中
其他常见的参数: - keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时候没有新的任务提交,核心线程外的线程不会立刻销毁,而是会等待,直到等待的时间超过了才会被回收销毁。
- unit:keepAliveTime参数的时间单位
- threadFactory:executor创建新线程的时候会用到。
- handler:饱和策略
ThreadPoolExecutor饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列已经被放满了的时候。ThreadPoolTaskExecutor定义一些策略:
- 抛出异常来拒绝新任务的处理。
- 调用执行自己的线程运行任务。这种策略会降低对新任务提交速度,影响程序的整体性能。这种策略喜欢增加队列容量,如果应用程序可以承受此延迟并且不能任务任何一个任务请求的话,可以选择这个策略。
- 不处理新任务,直接丢弃掉。
- 丢弃最早的未处理任务请求