Java并发编程学习(二)
多线程部分
什么是线程与进程
进程:内存中运行的运用程序,每个进程都有自己独立的内存空间,一个进程可以由多个线程,例如在Windows系统中,xxx.exe就是一个进程。
线程:进程中的一个控制单元,负责当前进程中的程序执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。
进程和线程的区别
- 根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位。
- 资源开销:每个进程都有自己独立的代码和空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行的过程不是一条线的,而是多条线(多个线程),共同完成;线程是进程的一部分,可以把线程看作是轻量级的进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- Executors工具类创建线程池
Thread类和Runnable接口方式区别 - 后者可以共享一个非静态的成员变量
1 | private int sum=10;//非静态的 |
Runnable接口和Callable接口有何区别
相同点:
- Runnable和Callable都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
- Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。
- Runnable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。
注:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方方法堵塞主线程继续往下执行,如果不调用就不会堵塞。
1 | public class CallableDemo implements Callable<Integer { |
1 | //第一种方式 |
run()方法和start()方法有和区别
每个线程都是通过某个特定的Thread对象对于的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。
start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。
sleep()和wait()有什么区别
两者都可以使线程进入等待状态
- 类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
- 是否释放锁:sleep()不释放锁,但是会让出cpu时间片,wait()释放锁,让出cpu时间片
- 用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
- 用法不同:wait()用完后,线程不会自动执行,必须由其他线程调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。
线程sleep和yield方法有什么区别
- yield[线程的礼让] - 让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。
- 线程调用sleep()方法进入堵塞状态,醒来后因为(没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
- sleep()方法使用时需要处理InterruptException异常,而yield没有。
- sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。
线程的状态
初始化状态 - 就绪态 - 运行态 - 阻塞状态 - 死亡态.
- 新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
- 可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
- 运行态
- 被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
- 计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
- 无线等待状态:获取锁对象后,调用wait()方法,释放锁进入无线等待状态
锁堵塞状态:wait(参数)时间到或者其它线程调用notify后没有获取到锁对象都会进入堵塞状态,只要一获取到锁对象就会进入可运行状态。
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入 可运行(runnable) 状态,才有机会再次获得cpu timeslice 转到 运行(running) 状态。阻塞的情况分三种:
(一). 等待阻塞: 运行(running) 的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞: 运行(running) 的线程在获取对象的同步锁时**,若该同步锁被别的线程占用,则 JVM会把该线程放入锁池(lock pool)中**。
(三). 其他阻塞: 运行(running) 的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O 请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或 者I/O处理完毕时,线程重新转入 可运行(runnable) 状态。
什么是线程死锁
死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态。
形成死锁的四个必要条件
- 互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
- 请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
- 不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
- 循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。
如何避免死锁
我们只需破坏形参死锁的四个必要条件之一即可。
- 破坏互斥条件:无法破坏,我们的🔒本身就是来个线程(进程)来产生互斥
- 破坏请求与保持条件:一次申请所有资源
- 破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:按序来申请资源。
项目中是否出现过死锁的场景
volatile和synchronized区别 ⭐️⭐️⭐️
JMM - Java内存模型,不是指JVM模型
1.volatile仅能使用在变量级别; synchronized则可以使用在变量、方法、和类级别的
2.volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
3.volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞。
4.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
Lock和synchronized区别 ⭐️⭐️⭐️
Lock
- lock是接口,synchronized它是一个关键字
- lock锁是一个显示锁(手动申请锁,手动释放锁),synchronized隐式锁(自动申请/释放锁)
- lock手动申请锁**(对象锁 - 不同的对象竞争的是不同的锁)**
- lock是锁代码块
- lock出现异常的时候,是不会主动释放资源的.
synchronized
- 原子性
- 可见性
- 有序性
- 可重入性
synchronized和lock都是属于独占锁.
实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可中断、可公平和不公平、细分读写锁提高效率
synchronized使用方式
修饰普通方法 - 线程申请到的是一把**”对象锁”**,不同的对象拥有独立的一把锁.并且每个对象的锁是不冲突的.
修饰静态方法 - “类锁” - 作用于这个类下的所有的对象 - 这个类实例化出来的所有的对象竞争的是同一把锁
修饰代码块
3-1. “对象锁”
1
2
3
4
5public void method(){
synchronized(this){
//同步代码块
}
}3-2. “类锁”
1
2
3
4
5public void method(){
synchronized(类class实例){
//同步代码...
}
}
synchronized底层
synchronized有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。他们的底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来,一个是通过方法flags标志,一个是monitorenter和monitorexit指令操作。
线程池的好处
定义:线程池的基本思想是一种对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
优势
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
5种线程池 - 面试
newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程 -
固定的线程数
,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
线程池参数 - 自定义线程池参数
在使用线程池时,为了获取最佳的性能,常常需要手动指定线程池的参数,ThreadPoolExecutor是最常用的线程池执行器,它有四个构造方法,参数最多的构造方法有7个参数,下面将详细介绍这7个参数的含义及作用.
源码
:
1 | public ThreadPoolExecutor(int corePoolSize, |
参数详解
核心线程数:corePoolSize
线程池中活跃的线程数,即使它们是空闲的,除非设置了allowCoreThreadTimeOut为true。allowCoreThreadTimeOut的值是控制核心线程数是否在没有任务时是否停止活跃的线程,当它的值为true时,在线程池没有任务时,所有的工作线程都会停止
最大线程数:maximumPoolSize
线程池所允许存在的最大线程数。
多余线程存活时长:keepAliveTime - newCachedThreadPool() - 默认是60
线程池中除核心线程数之外的线程(多余线程)的最大存活时间,如果在这个时间范围内,多余线程没有任务需要执行,则多余线程就会停止。(注意:多余线程数 = 最大线程数 - 核心线程数).
时间单位:unit
多余线程存活时间的单位,可以是分钟、秒、毫秒等。
任务队列:workQueue
线程池的任务队列,使用线程池执行任务时,任务会先提交到这个队列中,然后工作线程取出任务进行执行,当这个队列满了,线程池就会执行拒绝策略。
1
2
3
4
5
6
7
8
9
10
11如果corePoolSize=10,maximumPoolSize=20,如果来了25个线程 怎么办???
先达到 corePoolSize,然后 优先放入队列,然后在到MaxPollSize;然后拒绝;
先启动线程到10个,然后把剩下的15个放到阻塞队列里面,并开始在线程池里面创建线程,直到最大MaximumPoolSize;
当一个任务通过execute(Runnable)方法欲添加到线程池时:
1、 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2、 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
3、如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,再有新的线程,开始增加线程池的线程数量处理新的线程,直到maximumPoolSize;
4、 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程 maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
5、 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。线程工厂:threadFactory
创建线程池的工厂,线程池将使用这个工厂来创建线程池,自定义线程工厂需要实现ThreadFactory接口。
拒绝执行处理器(也称拒绝策略):handler
当线程池无空闲线程,并且任务队列已满,此时将线程池将使用这个处理器来处理新提交的任务。
线程池的四种拒绝策略
所谓拒绝策略,就是当线程池满了、队列也满了的时候,我们对任务采取的措施。或者丢弃、或者执行、或者其他…
jdk自带4种拒绝策略,我们来看看。
CallerRunsPolicy
// 在调用者线程执行 - 当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。AbortPolicy
// 直接抛出RejectedExecutionException
异常,异常处理DiscardPolicy
// 任务直接丢弃,不做任何处理DiscardOldestPolicy
// 丢弃队列里最旧的那个任务,再尝试执行当前任务
这四种策略各有优劣,比较常用的是DiscardPolicy
,但是这种策略有一个弊端就是任务执行的轨迹不会被记录下来。所以,我们往往需要实现自定义的拒绝策略, 通过实现RejectedExecutionHandler
接口的方式。
什么是同步
synchronized - lock
同步用来控制共享资源在多个线程间的访问,以保证同一时间内只有一个线程能访问到这个资源。在非同步保护的多线程程序里面,一个线程正在修改一个共享变量的时候,可能有另一个线程也在使用或者更新它的值。同步避免了脏数据的产生。
线程安全是什么?线程不安全是什么?
线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。(Vector,Hashtable,StringBuffer)
线程不安全:不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。(ArrayList,LinkedList,HashMap,StringBuilder等)