线程基础
线程的状态
根据java官方的定义,线程一共有五种状态NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED。需要注意的是:
- 其中RUNNABLE表示将运行和就需两个状态合并成一个状态。
- BLOCKED状态是指线程在进入synchronized代码块/方法时等待获取锁时的状态。
-
WAITING状态是因为调用了Object.wait()、Thread.join()、LockSupport.park()
线程状态
线程的方法
Thread.yield() 让出时间片,放弃cpu执行权。让线程处于ready,所以有可能会立即获得cpu的执行权。同时与sleep一样并不会释放锁。
Thread.sleep(0),释放cpu时间片,让线程马上回到就绪队列而非等待队列。通过释放当前线程锁剩余的时间片,从而达到让操作系统来执行其他线程,提升效率。
Thread.join() 在Main线程中调用了t.join()方法,会使t线程执行完Main线程才会继续往下走,内部通过wait()实现。
sleep和wait的区别
- sleep是Thread的方法
- wait是Object的方法
- sleep不会释放锁
- wait会释放锁 然后会将锁加到对象监视器的等待队列中去
- 因此wait必须要在同步方法/同步代码块中使用,而sleep则不需要
- 被wait的同步方法/同步代码块必须要由notify来唤醒
多线程运行时捕获线程异常
线程安全
同步容器
- Vector、Stack
- HahsTable
- Collections.synchronizedXXX(List,Set,Map)
并发容器
- CopyOnWriteArrayList
在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。CopyOnWriteArrayList的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需确保数组内容的可见性。因此,多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改容器的线程相互干扰。“写时复制”容器返回的迭代器不会抛出ConcurrentModificationException并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时,仅当迭代操作远远多于修改操作时,才应该使用“写入时赋值”容器。 - CopyOnWriteArraySet ConcurrentSkipListSet
- ConcurrentHashMap ConcurrentSkipListMap
(SkipListXXX 表示有序)
同步
使用synchronized关键字
- synchronized用在方法上使用ACC_SYNCHRONIZED来完成
- synchronized用在代码块上是用monitorenter 和 monitorexit指令
- 其实本质都是对一个对象的监视器进行获取
- jdk1.6的优化在于将锁升级成偏向锁->轻量级锁->重量级锁
ReentrantLock
公平锁和非公平锁的区别
- 非公平锁在调用lock后,会立即进行一次CAS抢锁,如果锁没有被占用,泽直接获取锁并返回。
- 如果非公平锁CAS失败了,会和公平锁一样进入tryAcquire,在tryAcquire方法中如果发现锁被释放(state==0),非公平锁会直接CAS抢锁,但是公平锁会判断锁wait set是否有线程等待,如果有则进行排队
- 以上两点就是它们的区别,所以实际上如果非公平锁两次CAS都不成功,那么后面和公平锁一样进入阻塞队列等待唤醒。所以非公平锁拥有更好的性能,因为他的吞吐量大,但是他也有可能让获取锁的时间不确定从而导致阻塞队列的线程线程饥饿。
ReentrantLock和synchronized的区别
- ReentrantLock提供中断的获取锁
- ReentrantLock提供尝试的获取锁
- ReentrantLock提供超时的获取锁
- ReentrantLock提供公平的获取锁
- ReentrantLock提供同时绑定多个Condition对象,而在synchronized中,锁对象的wait、notify、notifyAll方法可以实现一个隐含条件,如果要和多于一个的条件关联的对象,就不得不额外的添加一个锁,而ReentrantLock则只需要newCondition即可。
阻塞队列BlockingQueue
方法 | 抛出异常 | 返回特殊值 | 可能阻塞等待 | 可设定等待时间 |
---|---|---|---|---|
入队 | add(e) | offer(e) | put(e) | offer(e,timeout,unit) |
出队 | remove() | poll() | take | poll(timeout,unit) |
查看 | element() | peek() |
线程池
- new Thread的弊端
- 每次通过new创建线程,性能差
- 缺乏统一管理,如果无限制创建线程,会导致占用过多系统资源从而发生死机或者OOM
- 缺乏灵活性,比如定期执行、线程中断等特性
- 线程池的优势
- 线程复用,减少开销
- 有效控制并发线程数量,提高系统资源利用率,避免过多资源竞争,避免阻塞
- 提供定时执行、定期执行、单线程、并发数控制等功能
- 线程池的生命周期
线程池的生命周期.png
- shutdown 和 shutdownNow()
- shutdown() 当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
- shutdownNow() 执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,shutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
- 合理配置线程池大小
- CPU密集型任务,尽量充分利用CPU,设置成 CPU核数+1
- IO密集型任务,参考值2*CPU核数
死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下4个条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序做操作来避免死锁。
支持定时的锁 boolean tryLock(long timeout, TimeUnit unit)
通过ThreadDump来分析找出死锁
总结