多线程并发
线程和进程
进程
进程是程序的一次执行过程,是系统运行程序的基本单位。
线程
线程是一个比进程更小的执行单位,一个进程在执行的过程中可以产生多个线程。
同类的多个线程共享进程的堆和方法区的资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
线程和进程最大的不同在于基本上各进程是独立的,而同一进程中的线程极有可能会互相影响。
程序计数器为什么是私有的?
程序计数器私有是为了多线程情况下,线程切换后能恢复到正确的执行位置。----一个线程对应一个程序计数器
虚拟机栈和本地方法栈为什么是私有?
虚拟机栈: 每个Java方法在执行的同时会创建一个栈帧用于存储局部变量、操作数栈、常量池引用等信息;本地方 法栈与虚拟机栈相似;所以为了避免多线程的情况下局部变量被其他线程所访问到,采用线程私有。
并发和并行
并发:
两个及两个以上的作业在同一时间段内执行
并行:
两个及两个以上的作业在同一时刻执行
同步和异步
同步:
发出一个调用后,在没有得到结果之前,该调用就不可以返回,一直等待;
异步:
调用发出之后不用等待返回结果,该调用直接返回
多线程可能带来的问题:内存泄漏、死锁、线程不安全
线程的生命周期和状态
New:新建状态
Runnable:运行中状态,包括就绪状态和运行状态;调用start()方法后处于CPU分配资源阶段,当就绪的线程获得CPU资源时进入运行状态。
Waiting:等待状态,执行wait()方法后,进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态。
Timed_Waiting:超时等待状态,相当于在等待状态的基础上增加了超时限制,当超时时间结束后,线程会回到运行中状态
Blocked:阻塞状态,当线程进入synchronized方法/块或者wait被notify后重新进入synchronized方法/块,但锁依然被其他线程占有时进入阻塞状态。(当线程未到获取锁或者调用wait被notify后重新获取锁失败时,进入阻塞状态)
Terminated:终止状态,线程执行完成或异常终止时进入终止状态,销毁线程
上下文切换
当发生线程切换时,需要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场,并加载下一个将要占用CPU的线程上下文。
线程死锁
多个线程同时被阻塞,它们中的一个或全部都在等待某个资源被释放。
如图:线程A持有资源2,线程B持有资源1,他们都想申请对方的资源,所以两个线程就会互相等待而进入死锁状态。
产生死锁的四个必要条件
- 互斥条件:该资源任意一个时刻只能由一个线程占有
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:线程已获得的资源在未使用完之前不能被剥夺,只能自己使用完毕后才释放资源
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
预防和避免死锁
预防死锁
破坏产生的必要条件即可:
- 一次性申请所有资源
- 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
- 按序申请资源,释放资源则反序释放。破坏循环等待条件
避免死锁
在资源分配时,借助于算法(比如银行家算法)对资源分配进行算法评估,使其进入安全状态
sleep()方法和wait()方法对比
共同点:都可以暂停线程的执行
区别:
1. sleep()方法没有释放锁,而wait()方法释放了锁。
2.wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法;而sleep()方法执行完成后会自动苏醒。
3.wait()是Object类的本地方法,sleep()是Thread类的静态本地方法;
因为wait操作的是对象(每个对象都有对象锁)而非当前线程,而sleep()是让当前线程暂停,不涉及对象。
可以直接调用Thread类的run方法吗?
不可以
多线程的工作流程:new一个Thread,线程进入新建状态,调用start() 方法,启动线程进入就绪状态,当分配到时间片之后开始执行run()方法。但如果直接执行run方法,会把run()方法当成一个main线程下的普通方法执行,这并不是多线程工作。
volatile关键字
volatile可以保证变量的可见性但不能保证变量操作的原子性
当new一个对象的时候,这一条代码分为三步:
- 为对象分配内存空间
- 初始化对象
- 将对象指向分配的内存地址
由于JVM具有指令重排的特性,执行顺序可能编程1->3->2。指令重排在单线程环境下不会出现问题,但多线程环境下 会导致获得一个没有初始化的实例。
synchronized 关键字
synchronized 中文翻译是同步的意思,主要解决的是多个线程之间访问资源的同步性,保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。(保证了数据的可见性和原子性)
使用方式:
- 修饰实例方法(锁当前对象实例):synchronized void method() {}
- 修饰静态方法(锁当前类):synchronized static void method() {}
- 修饰代码块(锁当前对象/类):synchronized(object/类.class){}
synchronized底层原理
synchronized 同步语句块的实现使用的是monitorenter 和monitorexit指令,其中monitorenter指令指向同步代码块开始位置,monitorexit指令指向同步代码快结束位置。
在执行monitorenter时会获取对象的锁,如果锁的计数器为0 ,则表示可以被获取,获取后锁计数器+1,在执行monitorexit指令释放后锁计数器-1,当为0时表示其他线程可以获取锁。(可重入锁—即一个线程获取某个对象的锁,还未释放,当其再次获得这个对象的锁时还是可以获得的,每次计数器+1,为零时才释放锁-----与ReentrantLock一样)
(当执行monitorenter指令时线程试图获取锁也就是获取对象监视器monitor的持有权)
synchronized 修饰的方法并没有monitorenter指令和monitorexit指令,取而代之的是ACC_SYNCHRONIZED 标识,这个标识指明该方法是一个同步方法。
(两者的本质都是对 对象监视器monitor的获取)
单例模式:
双重校验锁实现对象单例:
第一次判空是因为单例模式只创建一次实例,当多次调用getInstance方法时,直接返回之前创建的实例,就可以不再执行同步方法里的代码,提高性能
第二次判空,是为了防止多线程创建多个实例。
静态内部类:
ThreadLocal
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,防止自己的变量被其他线程篡改。
Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量
线程池
线程池提供了一种限制和管理资源的方式。
优点:降低资源消耗;提高响应速度;提高线程的可管理性。
Runnable接口和Callable接口的区别
Runnable接口不会返回结果或抛出异常,但Callable接口可以。
工具类Executors可以实现将Runnable对象转换成Callable对象。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object result))
execute()和submit()方法的区别?
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过Future对象可以判断任务是否执行成功。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
如何创建线程池
方式一:通过构造方法
方式二:通过Executor框架的工具类Executors来实现
- FixedThreadPool:该方法返回一个固定线程数量的线程池;
- SingleThreadExecutor:该方法返回一个只有一个线程的线程池;
- CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。
以上三个方法内部实际上是调用了ThreadPoolExecutor的构造方法。
七个重要参数分析
corePoolSize:核心线程数
核心线程数定义了最小可以同时运行的线程数量,不会被回收
maximumPoolSize:最大线程数
线程池允许创建的最大线程数
workQueue:工作队列
当提交的任务数超过核心线程数后,再提交的任务就存放到工作队列
keepAliveTime:空闲线程存活时间
当线程池中的线程数量大于核心线程池数,直到等待时间超过keepAliveTime才会被回收销毁。
unit:时间单位
keepAliveTime的时间单位
threadFactory:线程工厂
executor创建新线程的时候用到
handler:饱和(拒绝)策略
定义:当线程池线程数已满,并且工作队列达到限制时所定义的一些策略:
ThreadPoolExecutor.AbortPolicy:(默认)抛出RejectedExecutionException来拒绝新任务的处理
**ThreadPoolExecutor.CallerRunsPolicy:**由调用线程处理该任务,会导致数据处理顺序不一致
**ThreadPoolExecutor.DiscardPolicy:**不处理新任务,直接丢弃
**ThreadPoolExecutor.DiscardOldestPolicy:**丢弃最早未处理的任务