概览
- 基础概念
- 基本概念解释
- 线程状态
- 线程
- 创建线程
- Thread中的start()和run()的区别
- 线程如果遇到异常
- 线程同步
- synchronized
- suspend() resume() stop()(废弃)
- wait() notify() notifyAll()
- sleep()和wait()比较
- ReentrantLock
- sychronized和ReentrantLock
- 死锁
- 乐观锁和悲观锁
- 偏斜锁、自旋锁、轻量级锁、重量级锁
- synchronized
- 线程间数据共享
- 线程池
- concurrent包
- ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet
- atomic
- ForkJoinPool
- CyclicBarrier CountDownLatch
- 其他
- threadLocal
- 多线程开发好习惯
- 重排序、原子性、可见性
- 重排序
- as-if-serial
- 原子性
- 可见性
- volatile
- synchronized
- 重排序
基本概念
基本概念解释
- 进程:
- 程序运行的最基本的单位,一个程序最起码一个进程
- 一个进程有独立的内存单元。
- 创建进程的代价很高
- 一个进程的崩溃不会影响其他进程
- 多进程程序可以跨机器部署
- 线程:
- 多个线程共享一个进程内存单元
- cpu调度是按线程调度
- 一个进程至少一个线程
- 创建线程的开销很小,线程本身占有的资源很少
- 一个线程死掉后整个进程挂掉
- 多个线程只能运行在一个机器上
- 守护线程:jvm不会等待守护线程,非守护线程全部退出后,jvm退出。守护线程结束。eg:gc线程
- 非守护线程:jvm会等待所有的非守护线程运行结束
- 多线程上下切换:cpu的控制权从一个正在执行的线程切换到一个就绪等待的线程
- 线程调度算法:抢占式。cpu会根据线程优先级、饥饿度为线程排序,给一个线程一个时间片段执行
- Thread.sleep(0)手动触发系统分配时间片操作
线程的状态
- 新建-》就绪:new Thread().start()
- 就绪-》运行:获取到处理器资源
- 运行-》死亡:任务执行完成、运行报错、Thread.stop()
- 运行-》阻塞:Thread.sleep()、io阻塞、等待锁、等待唤醒、suspend()
- 阻塞-》就绪:sleep结束、io返回、获得锁、唤醒、resume()
- 运行-》就绪:yield()、失去到处理器资源
线程
创建
- 继承Thread,使用start()启动,一个Thread实例只能启动一次
- 实现Runnable,放入Thread执行
- 由于java是单继承,所以使用实现Runable的方式更好,相比继承Thread
- 可以多个线程同时执行一个实例,数据共享
- 实现Callable,放入FutureTask执行
- 相比Runnable。1、Callable中的call()是有返回值的,通过和Future配合可以取到执行结果。2、可以抛出异常。3、Callable实现call(),Runnable实现run()。4、可以通过future获取Callable的运行状态,取消任务
FutureTask<String> future = new FutureTask<>(new Callable<String>() {
public String call() throws Exception {
return "";
}
});
new Thread(future).start();
- 使用线程池
FutureTask
- FutureTask(Callable)
- RunnableFuture
- Runnable
- Future
- 实现Runnable Future接口。Runnable使得可以放入到Thread中,Future使得可以使用get()来获取返回值
- Runnable、Callable作为FutureTask构造参数传入
- 最终FutureTask放入Thread中启动线程.
- 可以放入线程池
- FutureTask相比于Runable特殊的功能
- 内部维护着任务的运行状态
- 内部存储着返回值,可以在任何时刻获取
- 可以有返回值、抛异常
- 一个FutureTask只能运行一次(因为里面维护了运行状态),如果需要多次运行,需要创建多个FutureTask然后传入同一个Callable
调用Thread中的start() 和run()
- start():启动一个新线程运行run()中的逻辑
- run():在当前线程运行run()
Thread.holdsLock(obj)
- 检查当前线程是否持有obj监视器
线程如果遇到异常
- 如果没有捕获,线程停止
- 线程释放获取的锁
线程同步
- 多个线程访问同一个数据时,容易偶然性的出现线程安全的问题
synchronized
- synchronized关键字是java内建的同步机制,也是java5前仅有的方式
- 通过该关键字可以制造同步方法和同步代码块
- 如果一个线程获取到锁,其他线程只能阻塞等待
suspend() resume() stop()(废弃)
- 持有锁时suspend,线程进入阻塞状态但并不释放所占用的锁
- suspend所以如果使用不当会造成对公共对象的独占,使得其他线程无法访问公共对象,严重的话造成死锁
thread.suspend();
- stop() 终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象
- 这样可能会造成数据的不一致
https://blog.youkuaiyun.com/xingjiarong/article/details/47984659
wait() notify() notifyAll()
- 这三个方法都必须在获取到锁的情况下才能调用,所以放在synchronized中
- wait()会立刻释放对象监视器;notify()和notifyAll()会在剩余代码执行完后释放
- 为什么不放在Thread上:java的锁时对象级的锁,每个对象都是锁。如果定义在Thread上就不能清楚反应释放获取哪个锁
- wait()和while 还是 if配合:与while配合,唤醒后应该在检查一下条件,如果不满足再次wait。
synchronized (monitor) {
// 判断条件谓词是否得到满足
while(!locked) {
// 等待唤醒
monitor.wait();
}
// 处理其他的业务逻辑
}
wait()和while:https://blog.youkuaiyun.com/yiifaa/article/details/76341707
sleep() 和wait()
- sleep()不会释放对象锁,wait()会
- sleep()睡眠后不会让出cpu,wait()会
ReentrantLock
- 再入锁,在java5中提供
- 通过lock()方法获取锁
- 显示的加锁解锁
- 能够实现比synchronized更加细化的锁控制
- 和synchronized相比,在早起版本有较大的优势,但是在后续版本中,低竞争环境下性能不如synchronized
- lockInterruptibly():提供了中断响应,可以再必要的时候interrupt(),来解决死锁问题
- tryLock():申请锁,等待一段时间,如果没有成功获取到锁,则返回false
- new ReentrantLock(true):创建公平锁,所有获得锁的请求都排队等待,按顺序获取锁
- Condition cond=lock.newCondition();condition上提供了await() signal() signalAll()。可以更加精细的控制锁的唤醒
参考:https://blog.youkuaiyun.com/Somhu/article/details/78874634
sychronized和ReentrantLock
- sychronized是关键字,ReentrantLock是类
- ReentrantLock可以灵活的实现多路通知、公平锁、tryLock()这种方法
死锁
- 多个线程竞争统一资源
- 获取到资源的线程不释放资源
- 等待资源的线程只能堵塞
public class DeadLock {
private static ReentrantLock lock1=new ReentrantLock();
private static ReentrantLock lock2=new ReentrantLock();
private static class Task implements Runnable{
private boolean flag=false;
public Task(boolean flag){
this.flag=flag;
}
@Override
public void run() {
try {
if (this.flag) {
lock1.lock();
Thread.sleep(100);//休眠100ms增加死锁的概率
lock2.lock();
} else {
lock2.lock();
Thread.sleep(100);
lock1.lock();
}
}catch (Exception e){
}finally {
if(lock1.isHeldByCurrentThread())lock1.unlock();
if(lock2.isHeldByCurrentThread())lock2.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Task task1=new Task(false);
Task task2=new Task(true);
Thread t1=new Thread(task1);
Thread t2=new Thread(task2);
t1.start();
t2.start();
}
}
偏斜锁、自旋锁、轻量级锁、重量级锁?
- 自旋锁:1.6后默认开启,基于之前的观察,锁定状态只会持续很短的时间,线程挂起恢复很浪费,所以就不放弃cpu执行权,让线程执行一个忙循环
- 偏向锁:锁会倾向于第一个获取它的线程,如果后面该锁没有被其他线程获取,则持有偏向锁的线程将不用进行同步
锁膨胀、降级
乐观锁和悲观锁
- 悲观锁:悲观的认为竞争会发生,所有操作先上锁。eg:synchronized
- 乐观锁:乐观的认为不会有竞争,将比较替换作为一个原子操作,如果失败表示有冲突,则重试
- CAS(Compare And Swap)是一种常见的“乐观锁”,大部分的CPU都有对应的汇编指令,它有三个操作数:内存地址V,旧值A,新值B。只有当前内存地址V上的值是A,B才会被写到V上,否则操作失败。
乐观锁:https://blog.youkuaiyun.com/m0_37585523/article/details/71107867
cas机制:https://www.cnblogs.com/mmmmar/p/8624242.html
线程间共享数据
- synchronized和wait() notify() notifyAll()
- ReentrantLock和await() signal() signalAll()
- BlockingQueen;
- 典型的生产者消费者模式
- 通过平衡生产者消费者提高运行效率
- 使生产者和消费者间解耦
线程池
Executor ExecutorService接口
- ForkJoinPool
- ThreadPoolExecutor
- AbstractExecutorService
- ExecutorService: submit()返回一个future,包含返回结果、异常,既可以传入Runnable也可以使Callable。
- Executor: execute()只能传入Runnable,不会有异常和返回值。
线程池
- 线程池可以灵活控制并发数,减少创建销毁线程的开销
- 核心类:ThreadPoolExecutor。继承ExecutorService
- java提供了简化的创建线程池的工厂Executors
- newFixedThreadPool():线程数固定,队列为Integer最大值长度
- newSingleThreadPool():线程数只有一个,队列为Integer最大长度
- newCachedThreadPool():有新任务来将会使用空闲线程或者新建一个线程来处理,由于队列中不缓存任务,所以可能创建很多线程导致资源耗尽。每个空闲线程只能存活60秒
- newScheduledThreadPool():创建一个定时调度的线程池
- newSingleThreadScheduledExecutor():创建一个单线程的定时调度线程池
- 不推荐使用Executors来创建线程池:https://blog.youkuaiyun.com/admin1973/article/details/78685795
- LinkedBlockingQueue用来装计划执行的任务
- ThreadPoolExecutor
- execute():向线程池提交任务
- submit():向线程池提交任务,能返回执行结果(Callable)
- shutdown()是用来关闭线程池的
- shutdownNow()是用来关闭线程池的,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务
- corePoolSize就是线程池大小
- maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施
- largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系
- 任务放入线程池的过程:
- 如果线程数 < corePoolSize,直接创建一个线程执行
- 如果线程数 > corePoolSize,将任务放入到任务队列
- 如果线程数 > corePoolSize,任务队列也放满了,则创建一个新线程执行
- 如果线程数 > corePoolSize,任务队列也放满了,线程数 > maximumPoolSize,执行丢弃策略
- 如果用LinkedBlockingQueue:默认是无限大的任务队列
- 如果用ArrayBlockingQueue:则必须设置大小,超过后先创建线程到最大上限,如果还有则执行丢弃策略
- 丢弃策略:
- AbortPolicy:丢弃并抛异常
- CallerRunsPolicy:在提交线程中运行
- DiscardOldestPolicy:将最久的任务丢掉,随后将此任务插入到队列中
- DiscardPolicy:丢弃
ThreadPoolExecutor:https://www.cnblogs.com/dolphin0520/p/3932921.html
newCachedThreadPool:https://www.cnblogs.com/baizhanshi/p/5469948.html
丢弃策略:https://blog.youkuaiyun.com/jgteng/article/details/54411423
concurrent
java.util.concurrent.atomic
- 包下的AtomicInteger、AtomicReference等类,它们提供了基于CAS的读写操作和并发环境下的内存可见性。
- 使用atomic类同时解决可见性问题(内部存储使用了volatile),和操作原子性问题(通过cas非阻塞算法实现)
public class AtomicDemo {
// private static int num=0;
private static AtomicInteger atomicInteger=new AtomicInteger(0);
private static int threadNum=10000;
public static void main(String[] args) throws InterruptedException {
Thread[] threads=new Thread[threadNum];
for (int i=0;i<threadNum;i++){
threads[i]=new Thread(()->{
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// num++;
atomicInteger.incrementAndGet();
});
threads[i].start();
}
for(int i=0;i<threadNum;i++){
threads[i].join();
}
// System.out.println(num);
System.out.println(atomicInteger.get());
}
}
AtomicInteger: https://blog.youkuaiyun.com/fanrenxiang/article/details/80623884
concurrentHashMap
- 1.5中提供,相比hashtable性能更高,1.6中concurrentHashMap采用的是局部上锁解决多线程安全问题
- 1.8中concurrentHashMap采用了cas实现同步,同时在数组+链表的基础上在添加了红黑树
fork/join
- RecursiveTask
- RecursiveAction
- ForkJoinTask:
fork():拆分任务,放入线程池,准备执行
join():返回结果进行合并
- ForkJoinPool:invoke(ForkJoinTask<T> task) 提交任务
CyclicBarrier CountDownLatch
- CyclicBarrier:同步屏障,等待指定数量的线程await()后,将触发被await的线程重新运行。可以reset()反复执行。还有其他的方法,比如获取被阻塞的线程数等
- CountDownLatch :作用和CyclicBarrier一样,但是只能执行一次
- 主要的使用场景如下:
- 让一个线程1 await(),其他线程运行结束后countDown(),当所有线程结束后线程1将重新开始运行
- new CountDownLatch(1) ,所有线程被同一个CountDownLatch实例await();主线程一旦countDown(),所有线程同时开始运行
其他
threadLocal
- 仅仅局限在线程内部的变量,属于线程自身,不在多个线程间共享
- 一种用空间换时间的做法,将每个线程的数据隔离开,没有线程安全问题,自然不用同步相关操作
多线程开发好习惯
- 最小同步范围
- 优先使用volatile
- 考虑使用线程池
- 优先使用并发容器
重排序、原子性、可见性
重排序
- 代码书写的顺序和实际执行顺序不同,主要目的是为了编译器和处理器提高运行效率
int a=0;
int b=9;
//实际的执行顺序可能是:
int b=9;
int a=0;
as-if-serial
- 无论怎么重排序,程序的执行结果和代码顺序执行的结果一样
- java会保证在单线程的情况下遵循as-if-serial
int a=0;
int b=a+4;//这两行不能进行重排序
原子性
- 保证程序要么一起执行,要么都不执行,执行过程中不会被其他的线程影响
- 保证原子性可以使用:
- synchronized
- ReentrantLock
- AtomicInteger
可见性
- 共享变量在线程间的可见性:一个线程对共享变量的修改能及时被其他的线程看到
- 共享变量:如果一个变量在多个工作线程中使用、在工作内存中存在副本,那么这个变量就是这几个线程的共享变量
- java内存模型:
- 所有变量都存储在主内存中
- 每个线程都有一个独立的工作内存,里面保存着该线程使用到的变量副本
- 线程对共享变量的所有操作都必须在自己的内存中进行,不能直接读取主内存
- 每个线程的工作内存相互独立
- 可见性:线程一将工作内存1中的数据刷新到主内存,线程二将主内存中的值更新到工作内存2中
- 导致不可见的原因:
- 线程的交叉执行
- 重排序同时线程交叉执行
- 共享变量没及时在主内存和共享内存更新
- 可见性问题只有在高并发下才会出现(主要是内存来不及刷新)
synchronized实现可见性
- 在获取到锁后先从主内存获取最新的值
- 在解锁前先将工作内存中的值刷新到主内存中
- 保证原子性
volatile实现可见性
- 通过内存屏蔽和禁止重排序来实现可见性
- 写:在工作线程被更新后,会及时刷新到主线程。底层通过在后面插入一条store命令实现
- 读:先从主内存中获取值更新到工作线程中。底层通过在面前插入一条load命令实现
- volatile不能保证原子性(eg:a++)
- volatile一般不能保证原子性,所以想要在多线程安全使用,则需要:
- 对变量的写入不依赖当前值(a++)
- 该变量没有包含在具有其他变量的不变式中(a>b)
- 读取long、double64位数据的时候保证原子性
final实现可见性
- 因为final一旦赋值就不能修改,所以在线程间是可见的
volatile和synchronized
- volatile更轻量级
- volatile不能保证原子性
64位(long,double)数据操作不是原子性
- jvm分两次读取内存,不是原子性的
- 可以用volatile解决
对数组使用volatile
- 如果改变数组索引,将受到volatile的保护,如果改变数组中的元素,volatile起不到保护作用