多线程
1.继承Thread类
线程只能单继承,不能再继承其他类(受限于Java类的单继承)
业务实现在run方法中,业务和线程耦合
多个子类之间业务不能共享
2.实现Runnable接口
解决了单继承问题
业务实现和线程实现解耦
业务可以共享
作为一个函数接口,可以使用lambda表达式和匿名内部类的匿名对象的方式进行创建Runnable
run方法返回值是void,无法处理有返回值结果的业务(可以通过数据库等方式进行弥补)
多线程共享,线程不安全,
3.实现Callable接口
可以处理返回结果
使用Callable的业务对象时,通过FutureTask来获取执行结果
主程序的线程---main
thread.currentThread.getName();
修改线程名称:
setName/Thread构造方法
线程名称要设置请避免重复,同时中间不要修改
java程序运行,创建jvm进程,并至少创建一个线程--主线程
程序运行结束,所有线程结束,进程随之结束
GC(线程)---自动回收内存
//阻塞和就绪---挂起和唤醒
sleep--线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。(以毫秒为单位)
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当
前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象
yield--线程让步:意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放
锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间
的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时
间,这一点是和sleep方法不一样的。
Thread.sleep(ms)
线程暂停执行,进入阻塞状态
交出cpu
不释放占用的资源锁
休眠时间到了回到就绪状态
Thread.yield()(不需要抛出异常)
线程暂停执行,进入就绪状态
交出cpu,但不确定具体时间
不释放占用的资源锁
相同优先级的线程拥有交出的CPU时间片的权限
thread.join()
在线程a中调用线程b的join方法,线程a暂停执行,直到线程b执行完毕,线程a才能继续执行
Date date=new Date();
SimpleDateFormat formate=new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
LocalDateTime now =LocalDateTime.now();
System.out.println(now.formate
线程中有三种方式可以停止线程。
1. 设置标记位,可以是线程正常退出。(boolean类型的标识)myThread.setFlag(false);
2. 使用stop方法强制使线程退出,但是该方法不太安全所以已经被废弃了。
3. 使用Thread类中的一个interrupt() 可以中断线程。
*1.改变线程的中断标识
*2.如果非阻塞,true
*3.如果阻塞,wait/sleep/join引起,抛出InterruptedExeception,清理中断标识
false,通过触发中断,开发者决定如何处理线程业务(退出或是继续或是其他处理措施)
中断在java中主要有3个方法,interrupt(),isInterrupted()和interrupted()。
interrupt(),在一个线程中调用另一个线程的interrupt()方法,即会向那个线程发出信号——线程中断状态已被设置。
至于那个线程何去何从,由具体的代码实现决定。
isInterrupted(),用来判断当前线程的中断状态(true or false)。
interrupted()是个Thread的static方法,用来恢复中断状态。
线程的优先级指的是,线程的优先级越高越有可能先执行,但仅仅是有可能而已。
在Thread类中提供有如下优先级方法(1--5--10 低--中--高):
主线程优先级(默认优先级):5 主线程中创建的线程默认:5
子线程优先级和父线程优先级相同
设置优先级:
public final void setPriority(int newPriority)
取得优先级:
public final int getPriority()
对于优先级设置的内容可以通过Thread类的几个常量来决定
1. 最高优先级:public final static int MAX_PRIORITY = 10;
2. 中等优先级:public final static int NORM_PRIORITY = 5;
3. 最低优先级:public final static int MIN_PRIORITY = 1;
线程 isDaemon()
thread.setDaemon(true);//在线程启用之前调用
用户线程(之前所有包括主线程)
守护线程(陪伴用户线程)(如GC)(一般都是死循环)
*当一个JVM进程中最后一个用户线程退出,守护线程将伴随JVM一起结束
*run执行完成/run执行任务周期性死循环
synchronized处理同步问题:
使用synchronized关键字处理有两种模式:同步代码块、同步方法
使用同步代码块 : 如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象:this
synchronized(对象)->对象
全局锁:synchronized(xxx.class)->锁类(只要是该类实例化的对象,全部加锁)
synchronized(Object object)->对任意对象进行加锁,String之类等等
同步方法(成员)synchronized->类的实例化对象->this
如果加在了静态方法上,也相当于加了全局锁
*1*锁中锁的加锁过程不受上一层锁的限制
通过monitorenter-exit实现
重复锁时,锁对象有一个计数器
*2*加锁但未释放锁会导致死锁状态
ReentrantLock进行同步处理(重入锁)
this.lock.lock->业务逻辑->this.lock.unlock **易因为函数体异常而导致无法释放锁
此时,使用finally{this.lock.unlock();} **使用finally可以使释放锁在任何情况下都需执行
**加锁,解锁,异常情况正常解锁
**开始事务 CURD (回滚、提交) -> 提交事务
synchronized(内建锁)的优化
CAS(无锁操作 compare and swap/比较和交换**依赖CPU指令集CMPXCHG实现两步绑定以保证两步同时成功或者失败):
V(堆上的实际值)O(预期值)N(更新的值)
**V==O 实际值和期望值相等,没有被其他值修改 V=N
**V!=O 实际值和期望值不相等,被其他线程修改了V,导致V!=O,返回V
**当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试(retry),当然也可以选择挂起线程。
CAS三个问题:ABA、自旋的资源浪费(自适应自旋)、公平性
//JVM通过自适应自旋(根据经验进行动态判定)来决定到底自旋多少次
//不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。
//内建锁无法实现公平机制,而lock体系可以实现公平锁。
对象头:在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,
那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位
/*锁一共有四种状态: Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、
轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁
升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。*/
//线程、时间、资源(锁)**三种维度区分锁
偏向锁是四种状态中最为轻量(最乐观)的一种锁:从始至终只有一个线程请求某一把锁。
/*如何关闭偏向锁:
偏向锁在JDK6之后是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延
迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过
JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。*/
轻量级锁:多线程在不同时间访问同一把锁
//轻量级锁+偏向锁足够应付大多数情况
重量级锁:多线程同一时间访问同一个锁
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。
**1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自
旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
**2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象
原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
**3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程
中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
锁粗化:减少高频次的无端加锁,将多个连续的锁扩展成为一个范围更大的锁
//使用同步对象时一般加上final
锁消除:代码逃逸技术,堆上的对象不会被除当前线程以外的其他线程访问,不需要加锁
//StringBuffer->StringBuilder
//使用一个API时需注意该API是否线程安全,以及是否被多个线程访问。(尽量在哪使用,在哪实例化对象)
死锁:多线程要执行完成,相互之间等待对方持有的锁,便会发生死锁,死锁一旦出现之后,整个程序就将中断执行,
所以死锁属于严重性问题。过多的同步会造成死锁,不要让多线程访问资源成“环”
ThreadLocal:通过创建变量副本,实现该变量在各线程中独享。//set:ThreadLocal.ThreadLocalmap->(ThreadLocal.value)
/*在 set,get,initialValue 和 remove 方法中都会获取到当前线程,然后通过当前线程获取到 ThreadLocalMap,如果
ThreadLocalMap 为 null,则会创建一个 ThreadLocalMap,并给到当前线程*/
/*在 ThreadLocalMap 的 set(),get() 和 remove() 方法中,都有清除无效 Entry 的操作,这样做是为了降低内存泄漏发生的可能。
Entry 中的 key 使用了弱引用的方式,这样做是为了降低内存泄漏发生的概率,但不能完全避免内存泄漏。
在调用 ThreadLocal 的 get(),set() 和 remove()的时候都会清除当前线程 ThreadLocalMap 中所有 key
为 null 的 value。这样可以降低内存泄漏发生的概率。所以我们在使用 ThreadLocal 的时候,每次用完
ThreadLocal 都调用 remove() 方法,清除数据,防止内存泄漏。*/
生产者与消费者模型
1.生产和消费的速率相等
2.生产速率>消费速率,容器元素到达一定容量,停止生产,增加消费者
3.生产速率<消费速率,容器为空,停止消费
容器用队列实现
生产者生产的商品数量超过容器限制,停止生产
消费者消费商品时,容易为空,停止消费
生产者:多个线程 Producer
消费者:多个线程 Consumer
容器:队列 Queue
元素:Object Goods(编号,名称,价格)
生产者 等待 消费者 wait
消费者 通知 生产者 notify
wait和notify成对出现,除非用notifyAll
出现阻塞的情况大体分为如下5种:
1. 线程调用 sleep方法,主动放弃占用的处理器资源。
2. 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4. 线程等待某个通知。
5. 程序调用了 suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。
/*可配置: 1.scanner键盘输入
2.file
3.properties(key=value)
4.command line arguments 命令行参数 java Main.class a b class
5.数据库 mysql(JDBC)
6.System.env("")*/
请解释sleep()与wait()的区别:
1. sleep()是Thread类中定义的方法,到了一定的时间后该线程自动唤醒,不会释放对象锁。
2. wait()是Object类中定义的方法,要想唤醒必须使用notify()、notifyAll()方法才能唤醒,会释放对象锁
线程池
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。开发中
使用线程池的三个优点如下:
1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
2. 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
3. 提高线程的可管理性:使用线程池可以统一进行线程分配、调度和监控。
/*当向线程池提交了一个任务之后,线程池是如何处理这个任务的呢?下面来看线程池的主要处理流程如下:
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心
线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队
列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满
了,则交给饱和策略来处理这个任务。*/
/*ThreadPoolExecutor执行execute()方法的流程 如下:
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取
全局锁)。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用
RejectedExecutionHandler.rejectedExecution()方法。*/
线程池的创建
我们可以通过ThreadPoolExecutor来创建一个线程池:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
long keepAliveTime,
TimeUnit unit,
/*创建一个线程池时需要输入几个参数,如下:
1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其
他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调
用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
·ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
·LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于
ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
·SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操
作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool
使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小
于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么
效果。
4) keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且
每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
5) TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫
秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微
秒)。
6) RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种
策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线
程池框架提供了以下4种策略。
·AbortPolicy:直接抛出异常。(默认采用此策略)
·CallerRunsPolicy:只用调用者所在
线程来运行任务。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。*/
向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判
断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用
get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
关闭线程池
以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后
逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别。
**shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行
任务的列表。(暴力的)
**shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。(优雅的)
**只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。
**当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。
至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,
如果任务不一定要执行完,则可以调用shutdownNow方法。
合理配置线程池
想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。
任务的执行时间:长、中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程
的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,
如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,
那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
/*优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
**如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。*/
执行时间不同的任务可以交给不同规模的线程池来处理(优先),或者可以使用优先级队列,让执行时间短的任务先执行。
/*依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越
长,那么线程数应该设置得越大,这样才能更好地利用CPU。*/
Executor框架的两级调度模型
任务---Executor框架---线程池(线程)---OSKernel(操作系统)---CPU
|-----------应用层---------|---------------操作系统层----------|
Executor有一个工具类Executors:
Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类。通过Executor框架的工具类Executors,可
以创建3种类型的ThreadPoolExecutor。
1. 创建无大小限制的线程池:public static ExecutorService newCachedThreadPool()
/*CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器。
这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。
用来帮助销毁无用任务以防内存不够或者线程池线程数达到Intager.MAX_VALUE*/
2. 创建固定大小的线程池:public static ExecutorService newFixedThreadPool(int nThreads)
/*FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场合,适用于负载比较重的服务器*/
3. 单线程池:public static ExecutorService newSingleThreadExecutor()
/*SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。*/
/*FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列。
CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的
maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,
CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资
源。*/
4. 周期性线程池:ScheduledThreadPoolExecutor
/*DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中没
有什么意义(设置maximumPoolSize的大小没有什么效果)。ScheduledThreadPoolExecutor的执行主要分为两大部分。
1)当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会
向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
2)线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下的修改。
a.使用DelayQueue作为任务队列。
b.获取任务的方式不同
c.执行周期任务后,增加了额外的处理*/