Java并发编程
相关重要知识
进程和线程的内存空间
进程:通俗的来讲,进程就是一个运行中的程序,是操作系统资源分配的基本单位
虚拟地址空间:操作系统制造了一个假象,让所有进程以为他们独占了这整个内存。操作系统和CPU合作,将虚拟地址空间映射到硬件上去。
虚拟地址空间包括:
用户栈:在函数调用时使用,会有栈帧。
堆:是运行时动态分配内存时候使用的。
从可执行文件加载的程序数据 和 程序代码。
(其中还有程序计数器:指向正在执行的指令的地址,或者即将执行的指令)
另一个假象:操作系统通过调度,让多个进程轮流使用时间片,形成多个程序并行的假象。
每个线程有着他逻辑上的自己的寄存器(一个CPU中临时存储的空间,速度飞快)和堆栈。在寄存器中的值在不占有CPU的时候,这些值会暂存到内存中(保持现场)
JVM级别的线程会映射到操作系统级别的线程。
为什么要用进程而不是线程?
进程之间本质上是资源独立的,线程之间的数据共享要比进程之间的高效。
进程之间的通信可能需要管道、消息队列、信号量、共享内存、套接字等方式来进行通信,而线程之间本来就是处于同一地址空间之中,效率比较高。
线程的实现
三种方式:
1.完全在用户层面(用户空间)实现
好处:线程之间的调度不需要内核的介入
缺点:进程的某个线程阻塞了(比如是网络IO还是文件IO),操作系统会觉得你整个进程中的线程都被阻塞住了,从而导致整个进程都被挂起了
为什么不能够在用户层实现线程之间的调度?
因为在做文件IO的时候,只能够通过操作系统的内核为上层提供的访问接口即系统调用来执行,必须进入到内核态中才行
2.用户层和内核层(内核空间)做映射
将一个用户线程映射到一个内核线程,然后这个内核线程再将其分配到CPU中去,由操作系统来完成线程的创建和调度。
需要两个栈,一个是用户态的栈,一个是内核态的栈。用户栈处理的是普通函数调用,内核栈处理的是系统调用。
优点:充分的利用了硬件
缺点:每次创建一个用户线程,都需要在内核创建一个相对应的线程(也不用)
3.用户层的多个线程映射到内核层的一个线程当中去
这个是多对一或者一对一
这样的话内核线程不用开太多,来给他们复用,在效率和硬件来进行平衡
多线程编程
如何实现互斥:锁。
临界区:访问修改共享资源(变量或者文件)的代码。
锁:必须在底层有硬件指令的支持。如果是代码来控制的话,CPU无法做到。
两个硬件指令
硬件指令-TestAndSet
boolean TestAndSet(boolean *lock) {
boolean rv = *lock;
*lock = TRUE;
return rv;
}
//用一个while来拦截
while(TestAndSet(&lock)) {
//什么事情都不做,只是循环等待
}
(是个汇编代码,实现的是一个原子操作,在一个CPU上是可以实现的,但是多个CPU的时候就不行,因此就需要锁总线)
总线:用来控制内存的控制总线和用来读写数据的数据总线。
屏蔽中断和锁总线相比,屏蔽中断更加危险,如果忘记解除的话,屏蔽中断会使得CPU对外界的任何操作都无法响应,变成“植物CPU”
i++有三步:
将内存中的i读入寄存器;
将寄存器中的值++;
将寄存器中的i值写回内存;
硬件指令-Swap
lock = false;(全局变量)
key = true;(每个线程一个)
while(key == true) {
swap(&lock, &key);
}
lock = false;
设计锁的时候需要考虑的问题
当发现其他线程已经持有锁了怎么办?
1.继续尝试,无限循环
- 时间片用完,变为就绪状态,等待下次调度
- 自旋锁2.将线程放到阻塞队列中
自旋锁有个特性,就是不可重入。(自己会把自己搞成死锁)
解决办法:需要记录这个锁被谁持有,以及重入的次数。
变成可重入锁
线程之间的通信
线程之间的通信:通过共享变量
//共享变量
volatile boolean loaded = false;
//线程1
读取配置文件
解析配置文件
loaded = true;
//线程2
while(!loaded) {
//do nothing
}
线程之间的通信:wait/notify(底层用的是非常的多啊)
synchronized中用wait/notify
一个线程一旦调用了某个对象的wait方法之后,这个线程会进入一个非运行状态,并且会释放获取到的锁,进入阻塞队列,直到另一个线程调用notify,这个时候这个线程只是被唤醒了,被唤醒的线程只能够抢到了锁才能够进入就绪状态。
线程之间的通信:join
线程b调用threadA.join(); //b会等待A执行完
join本质上是调用了wait方法进行等待,当线程a终止的时候才会调用notify
thread.yield()让出CPU
等待锁的时候会进入block状态
进入wait状态的几种方式:
o.wait t.join LockSpport.park –> block
进入 time waiting:
sleep(sleeptime) wait(timeout) join(timeout) LockSupport.parkNanos/parkUntil
时间到了是running 之后是block wait是等待notify notify之后进入block状态请求获取锁
JDK中常用的锁
可重入互斥锁
Lock lock = new ReentrantLock();
//获取锁
lock.lock();
try {
//do something
} finally {
//释放锁
lock.unlock();
}
互斥锁:同一时间能只有一个线程进入临界区。(推荐使用,能够进行更加精细的)
获取锁的不公平和公平(先来后到)
信号量
Semaphore s = new Semaphore(3);
同一时刻,只有三个线程能够获得锁(不是互斥的)
Reader Writer(读写锁)
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readlock().lock() lock.wirtelock().lock()
CountDownLatch(倒计时)
CountDownLatch latch = new CountDownLatch(5); //5个前驱线程
希望当多个线程执行完之后在执行某个逻辑 [可用于初始化]
CyclicBarrier(栅栏)
互相等待其他线程是否走到了某一处
barrier.await() 同时1起跑干一个事情
CyclicBarrier barrier = new CyclicBarrier(); [业务场景,多线程下载]
死锁
双方互相持有锁,互相等待
死锁的预防:
1.规定次序 (将这个进行了串行化,效率超级低)
2.申请锁的时候加上timeout (低并发可以,高并发还是不太行)
按照次序申请锁的实现:
int fromHash = 获取from的hash code;
int toHash = 获取to的hash code;
if(fromHash > toHash) {
synchronized(from) {
synchronized(to) {
//转账
}
}
}else ...
(如果恰好相等,那么就引入第三方锁)
synchronize和ReetrantLock在使用上怎么选择,性能上有什么区别?
一个简单一个精细 性能上差别不大
(操作系统的书:现代操作系统 操作系统概念 深入理解计算机系统)
线程池
- 常见使用场景
- web/数据库/文件/邮件 服务器
- 这些服务器请求来自远程的客户端
- 请求数量很大,任务执行时间较短
一个请求一个线程
- 创建线程,销毁线程的开销比较大
- 太多的线程导致内存耗尽
关于线程池的思考:
1.当线程池刚刚创立,还没有Task到来的时候,线程池中的线程处于什么状态?
不能是就绪状态,否则cpu就可能调度他运行了,也不能睡觉,应该是处于阻塞状态,等待任务
2.当Task到来的时候线程池中的线程如何得到通知?
本质上是一个阻塞队列,当task来的时候,需要通过notify将其唤醒
3.当线程池中的线程完成工作的时候,如何回到池中?
其实只需要去阻塞队列中获取任务,如果没有任务就再次阻塞住就行了
4.Task是个什么东西?
本质上是自己定义的一个结构,只要这个task能够被识别就行,比如解析http request生成response
Task Queue 是一个BlockingQueue:一个先进先出的阻塞队列
在线程阻塞住停止的时候,调用interrupt()方法,会让他从wait状态出来,确保能够停止。有个标志位isStop会被置为true。
BlockingQueue不仅在出队列的时候,请求task的线程会被阻塞住,而且当Queue满了的时候,添加任务的线程也会被阻塞,会交给RejectExcutionHandle来处理。
JDK1.5之后有的线程池
如何使用线程池
//还有几种实现请回去看
ExcutorService excutorService = Excutors.newFixThreadPool(10); //线程数
//or
ScheduleExcutorService scheduleExcutorService = Excutors.newScheduledThreadPool(5); //定时调度任务的线程池,5s以后执行
//task需要实现runnble或者callable接口,缺点是无法获得执行结果
excutorService.excute(new Runnable() {
public void run() {
System.out.print("this is a task!");
}
});
excutorService.shutdown();
线程池的一些参数
int corePoolSize = 10;
int maxPoolSize = 20;
int keepAliveTime = 5000;
ExutorService thredPoolExcutor = new ThreadPoolExcutor(
corePoolSize,
maxPoolSize,
keepAliveTIme,
TImeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
ThreadFactory threadFactory,
RejectedExecutionHandler handler) ); //后两个参数是可选的
threadPoolExecutor.execute(task);
//这些参数的的含义,新来的线程怎么办,自己学习完成//TODO (四种拒绝策略)
LinkedBlockingQueue:保存任务的阻塞队列,与线程池的大小有关:
当运行的线程数少于corePoolSize时,在有新任务时直接创建新线程来执行任务而无需再进队列
当运行的线程数等于或多于corePoolSize,在有新任务添加时则选加入队列,不直接创建线程
当队列满时,在有新任务时就创建新线程(意思就是必须得等到缓冲队列满了才能够创建线程)
**四种拒绝策略:**AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。
AbortPolicy:线程池的默认执行策略,不会执行这个任务,并且会在抛出一个运行时异常,切记在执行 ThreadPoolExcutor.excute()方法的时候需要try catch,否则程序会直接退出。
discardPolicy:直接丢弃这个任务,空方法。
DiscardOldestPolicy:从队列里面抛弃head的一个任务,并且再次excute这个task。
CallRunsPolicy:在调用execute的线程里面执行此command,会阻塞入口(重试添加当前的任务,他会自动重复调用execute()方法)
获取到执行的结果
1.返回一个future对象
Future future = excutorService.submit(new Runnnable() {
public void run() {
System.out.print("This is a task!");
}
});
//或者
Future future = excutorService.submit(new Callable() {
public Object call() throws Exception {
System.out.print("this is a task!");
return "Object";
}
});
future.get(); //这是一个阻塞方法,如果返回null,表示这个任务已经完成,否则将一直阻塞住
CAS(compare and swap)//TODO 到底是怎么实现的?
也是硬件实现的。
就是在写入前,将这个之前的值和内存中的比较,看看是否有变化,没有的话就写入新的值,有的话就再次读取修改再进行比较。
当添加synchronized 的时候,是悲观锁,将这个操作串行化了
问题:当有很多的线程的时候,同一时间只能够有一个线程,其余的线程只能够挂起等待激活。
乐观锁:多个线程都可以读取同一变量,但是当他们使用CAS同时更新同一个变量的时候,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会挂起,而是被告知这次竞争中失败并可以再次尝试。
cas的应用:
private AtomicInteger count = new AtomicInteger(10);
while(true) {
int current = count.get();
int next = current + 1;
if(count.compareAndSet(current, next)) {
return next;
}
}
CAS的优点
- 非阻塞:线程不会阻塞,只是不停的检查下去
- CAS不会出现死锁,不会挂起造成互相等待的场面
- 在轻度和中度的竞争情况下,非阻塞的算法的性能会超越阻塞算法,如果争用很厉害的情况下就不一定了
- 只不过多了几个循环而已
CAS能够实现很多数据结构
非阻塞的栈:(//TODO) use atomicReferrence
ABA问题
解决办法:
- 版本戳version来对记录或者对象标记
- Atomic**Stamped**Reference
- 先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置成给定的更新值。
ThreadLocal
当在多层函数调用的时候,需要获得一个多层函数之前的值,解决方法:
1.通过参数传参(麻烦,可能无法实现)
2.设置成静态的(线程不安全)
3.用ThredLocal
为什么不会出现并发问题?
因为每一个线程都有一个属于自己的ThreadLocalMap
这个地方的key是ThreadLocal对象本身,所以想在当前的线程中保存多个值就要创建ThreadLocal的实例
不变类(String)
如果将对象设置成只读的呢?
对象在创建的时候就应该提供数据,并且在生命周期内不变
如何成为不变类
- 不能有改变对象的方法
- 不能被扩展(子类化),要使用final或者静态工厂
- 所有的字段都应该是私有的,final的
坏处:对于不同的值哪怕更改一点点,都应该创建一个新的对象,可能会造成大量的资源浪费
解决方式可以提供一个公有的可变配套类(StringBulider)