Java多线程并发
一、 java多线程创建方式
-
继承Tread类
将自己的类继承Tread类,并重写run()方法。
Tread类的start()方法是一个native方法
-
实现Runable接口
将自己的类实现Runable接口,重写run方法。
实例出一个task。再实例化一个Thread,并传入task。
-
实现Callable接口(有返回值)
执行callable task 可以返回一个Future 对象,通过get方法能获得任务的返回值。
二、 4种线程池
-
newCachedTreadPool
适用于短期异步任务。
- 当任务申请线程,若有空闲线程,则重用这个空闲线程。
- 若无空闲线程,则创建新线程。
- 会终止并从缓存移出60秒未被使用的线程。
-
newFixedTreadPool
固定数量的线程池,用队列排队。
适用于子啊任意点大多数线程都处于活动状态的程序。
有任务申请线程- 队列满,排队等待,不满,得到线程。
- 若某个线程异常终止 , 创建新线程继续任务。
- 在线程被显式关闭前,线程将一直存在于线程池。
-
newScheduledThreadPool
可设置内部线程延迟执行或定期执行。
-
newSingelThreadExcutor
线程池只有单个线程。
三、 线程生命周期
它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
新建状态。JVM分配内存,并初始化成员变量。
就绪状态。调用start()方法后,处于就绪状态。JVM创建方法栈和PC,等待调度运行。
运行状态。得到CPU控制权,开始执行run方法。
-
阻塞状态
让出CPU使用权
- 等待阻塞 object.wait。JVM将线程放入该对象的等待锁池。
- 同步阻塞 lock 。同步锁被别的线程占用,JVM将线程放入锁池(look pool) ;
- 其他阻塞 sleep/join 或IO ,不释放锁,当超时,或IO结束,线程重新变成Runable。
四、 终止线程的4种方式
正常结束
使用退出标志
对于一些守护线程,当外部条件满足时才能退出。这个时候可以用类似于While(!biaozhi) 循环 的方式控制线程是否终止。-
Interrupt()方法
该方法实质是设置中断标志位为true ,程序员通过检测中断标志位来结束线程。线程处于阻塞状态: 如调用了sleep、wait等方法后线程阻塞。此时调用线程的interrupt方法会抛出InterruptException异常。通过捕获这个异常然后break跳出循环,终止线程。
-
未阻塞 :通过调用isInterrupted() 判断线程中断标志位来退出循环。
while (!isInterrupted())
{to do}
注意 interrupted()方法返回状态后,会重置状态。
public class ThreadSafe extends Thread { public void run() { while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出 try{ Thread.sleep(5*1000);//阻塞过程捕获中断异常 来退出 }catch(InterruptedException e){ e.printStackTrace(); break;//捕获到异常之后,执行break 跳出循环 } } } }
-
使用stop方法.不安全
线程会突然释放所有的锁,可能导致数据的破坏。
五、 sleep 和 wait 的区别
sleep属于 Thread 类 . wait 属于Object类
sleep后会自动恢复运行,而wait需要notify
sleep不会释放锁,wait释放锁
六、 start和run的区别
start() 是线程启动方法。run是线程业务代码方法
start() 之后创建线程处于就绪状态,如果直接调用run()。会在主线程直接运行业务代码。
七、 守护线程
为用户线程提供服务的线程
设置方法setDaemon(true)
特性
守护线程可以不依赖于控制终端(应用)而依赖于系统。
在所有用户线程结束之后再自动结束。
八、java锁
-
乐观锁
读数据的时候不上锁, 写数据的时候上锁。写入的时候先读出版本号V1,然后进行CAS原子操作。比较V1和加锁后的V2是否相同,若相同,更新;若不同,更新失败。
悲观锁
读写数据的时候都加锁。
比如: Synchronized 同步 和 RetreenLock 锁-
自旋锁
让未获得锁的线程不挂起,仍然保持内核态。等待锁的释放。此时会消耗cpu。
适用于持有锁的线程会在短时间释放锁的情况。
优点:
减少线程阻塞,提高性能。自旋消耗小于线程阻塞挂起和幻想的消耗。
若持有锁的线程长时间占有锁。则自旋所需的cpu性能白白消耗,同时是的其他需要cpu的线程没有获得cpu造成浪费。
自旋锁时间阈值
自旋周期jad1.5的时候固定的,jdk1.6的时候是自适应的。 -
Synchronized同步锁
悲观锁。独占式、可重入。
- 作用于方法, 锁实例。当调用实例中被锁方法,阻塞
- 作用于静态方法, 锁Class实例。Class相关数据只存在于方法区(元空间) ,即锁住所有调用该方法的线程。
- 作用于对象时,当调用被锁实例中任何方法,阻塞。
核心组件
1) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中; 2) Wait Set:哪些调用wait 方法被阻塞的线程被 放置在这里; 3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到Entry List 中; 4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck; 5) Owner:当前已经获取到所资源的线程被称为Owner; 6) !Owner:当前释放锁的线程。
Synchronized 实现
- 当 Owner 线程 unlock时, JVM将EntryList中的一个线程移入OnDeck 。同时,将ContentionList中的一些线程移入EnryList。
- onDeck中的线程有竞争锁的权利,而不是直接得到锁。它与处于自旋状态的线程竞争锁。
- 持有锁的线程调用wait方法, 该线程释放锁,并进入Wait Set--等待池。当调用notify时,将Wait Set 中线程加入EnryList--有资格竞争锁的等待队列。
- Synchronized是非公平锁,线程在进入ContentList之前,会先尝试获得自旋锁,并可能直接与OnDeck中的线程产生竞争,抢占锁资源。
- 加锁实际上是在竞争monitor对象
-
ReentrantLock 可重入锁
ReentrantLock 和 synchronized都是可重入锁.
可重入锁与不可重入锁的区别不可重入锁:只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单 可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。 设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。
关键源码 (上锁):
while(isLocked && lockedBy != thread){ wait(); }
判断了是否上锁,并且请求锁的线程是否是当前线程。ReentrantLock 与 synchronized相比:
- 优势:可中断、公平锁、多个锁。
- 使用lock 和 unlock 加锁解锁。解锁必须在finally控制块中完成。
Condition 类和Object类锁的区别
- Condition.await 和Object.wait等效
- signal 和 notify 等效
- ReentrantLock可以唤醒指定条件线程。
Lock 和 tryLock
- tryLock 没获得锁返回false。
- lock 没获得锁阻塞 ,等待获得锁.
Lock 和 lockIterrupibly
- lock被中断不会抛出异常, 而 lockIterruptibly会抛出异常.
熟悉的方法
1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经 被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁. 2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和 lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一 直等待, 在未获得锁之前,当前线程并不继续向下执行. 3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程 并不持有锁, 却执行该方法, 可能导致异常的发生. 4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定, 当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。
不熟悉的方法
1. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock 方法的次 数。 2. getQueueLength():返回正等待获取此锁的线程估计数,比如启动10 个线程,1 个 线程获得锁,此时返回的是9 3. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线 程估计数。比如10 个线程,用同一个condition 对象,并且此时这10 个线程都执行了 condition 对象的await 方法,那么此时执行此方法返回10 4. hasWaiters(Condition condition) : 查询是否有线程等待与此锁有关的给定条件 (condition),对于指定contidion 对象,有多少线程执行了condition.await 方法 5. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁 6. hasQueuedThreads():是否有线程等待此锁 7. isFair():该锁是否公平锁 8. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行lock 方法的前后分 别是false 和true 9. isLock():此锁是否有任意线程占用 10. lockInterruptibly():如果当前线程未被中断,获取锁 11. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁 12. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持, 则获取该锁。
非公平锁 : JVM随机或就近分配锁的机制。ReentrantLock(false) 构造函数设置公平/非公平
公平锁 : 先申请锁的先得到锁。ReentrantLock(true)
-
Semaphore 信号量
阈值设置,运行多少个线程同时操作资源。
阈值设置为 1 即互斥信号量 减1 : Semaphore.acquire()
信号量 加1 : Semaphore.release()
-
AtomicInteger 原子操作Integer
还可以通过AtomicReference
将对象的所有操作转化为原子操作。 保证操作的原子性 ,效率比lock高
-
ReadWriteLock 读写锁
读锁: 读取的时候保证多个人读,不能同时写
写锁 : 写入的时候保证一个人写,且不能读
-
锁优化
- 减少锁持有时间
- 减少锁粒度 , 降低锁的竞争
- 锁分离 , 例如读写锁
- 锁粗化 如果对一个锁不停请求,同步释放也会作大量
- 锁消除 编译器干的事
十、 线程的基本方法
-
wait
线程阻塞 , 释放锁,进入wait set 线程等待池
-
sleep
线程休眠,不释放锁 , 进入time-wating状态
-
yeld
线程让步 , 让出cpu使用权,与其他线程竞争时间片
-
interrupt()
线程中断 , 但不直接中断线程。
- 给出通知信号 , 使得time-waiting状态的线程抛出interruptException异常。中断位会清零。
- 设置中断位为 1
-
join等待其他线程终止
其他线程调用join方法,当前线程阻塞 。 不放弃锁 , 等到相关线程结束,转为就绪状态。
-
Object.notify
线程唤醒 , 唤醒对象监视器上等待的单个线程,竞争cpu使用权。
为什么notify和wait都在object类上?
答:因为notify和 wait需要作用在同一个锁上。而锁可以是任意对象,可以被任意对象调用的方法是定义在object类中。
-
其他方法:
1. sleep():强迫一个线程睡眠N毫秒。 2. isAlive(): 判断一个线程是否存活。 3. join(): 等待线程终止。 4. activeCount(): 程序中活跃的线程数。 5. enumerate(): 枚举程序中的线程。 6. currentThread(): 得到当前线程。 7. isDaemon(): 一个线程是否为守护线程。 8. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线 程依赖于主线程结束而结束) 9. setName(): 为线程设置一个名称。 10. wait(): 强迫一个线程等待。 11. setPriority(): 设置一个线程的优先级。 12. getPriority()::获得一个线程的优先级。
十一线程上下文切换
线程上下文切换,同一颗cpu上 ,任务状态的保存和再加载
-
上下文
某一时间点,cpu寄存器和计数器内容
-
PCB 进程控制块-切换帧
表示进程状态的信息块。
-
上下文切换活动
- 挂起进程(线程) , 将进程信息存储在内存
- 内存中检索下一个进程上下文,并在CPU寄存器中恢复
- 跳转到PC ,运行程序
-
引起上下文切换原因
- 时间片用完
- IO阻塞
- 多任务抢占锁,未获得锁
- 硬件中断
十二线程池原理
主要特点 : 线程复用 , 控制最大并发数 , 线程管理
-
线程复用: ?
在Thread strat() 方法中不断循环调用传递过来的Runnable对象 , 并使用Queue组织这些Runnable对象。
-
ThreadPoolExecutor
构造方法如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit , workQueue, Executors.defaultThreadFactory(), defaultHandler); } 1. corePoolSize:指定了线程池中的线程数量。 2. maximumPoolSize:指定了线程池中的大线程数量。 3. keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多少时间内会被销毁。 4. unit:keepAliveTime的单位。 5. workQueue:任务队列,被提交但尚未被执行的任务。 6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。 7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
-
线程池工作过程
1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面 有任务,线程池也不会马上执行它们。 2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断: a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务; b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列; c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要 创建非核心线程立刻运行这个任务; d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池 会抛出异常RejectExecutionException。 3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。 4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运 行的线程数大于 corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后,它 终会收缩到 corePoolSize 的大小。