参考资料:
Av86373641:黑马程序员 - 全面深入学习java并发编程
-
进程和线程
并行并发
并发:同一个时间段交叉运行多个线程
并行:同一个时间点运行多个线程线程的创建方式
- 覆盖run方法
- 编写一个Runnable接口对象然后给到Thread对象
- FutureTask和Callable接口(Callable比Runnable多一个返回值,并且可以抛出异常),可以返回值,FutureTask创建完也要给到Thread,然后调用Thread就可以,因为FutureTask也实现了Runnable接口。我们看下FutureTask对runnable的实现就可以知道,它是调用了Callable对象,处理了异常和返回值。
public void run() {
if (state != NEW ||
!RUNNER.compareAndSet(this, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
前两者推荐推荐用第二种
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
Thread和Runnable的关系
有了Runnable可以把线程和任务分开来,Runnable可以更容易与线程池等高级API配合,更加灵活。栈帧
栈,每个函数有一个栈帧
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- Thread方法
join:调用的线程等待被调用的线程运行结束,主要用于同步。
yield:让出当前线程,从running到runnable
interrupt:打断其他线程的运行
如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
- 守护线程
总结:依附其他现成的存在而存在,比如垃圾回收器
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
-
java的线程状态
waiting是join的时候的状态
blocked是等待锁的时候的状态 临界区
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区临界区出问题的解决方案
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
- synchronized的两种用法
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized用于互斥
要注意两个都要枷锁,并且要锁住同一个对象。成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全局部变量是否线程安全?(重点关注P66)
局部变量是线程安全的
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类线程安全类方法的组合
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
开闭原则
闭原则(final private)可以增加类的安全性,比如String设置为final就是。(P66)-
monitor锁,管程/监视器
锁优化(这些措施都是java虚拟机自动操作的,虽然可能可以配置)
上面的monitor是重型锁,还有轻量锁和偏向锁。
偏向锁:这个锁的前提是锁真的主要是其中一个线程在用。
synchronized最开始加的是轻量级锁,后面有人来了,进行锁膨胀才加为重量级锁,锁膨胀是为了后面的线程有等待队列。
锁膨胀:轻量级锁变为重量级锁,锁膨胀是为了后面的线程有等待队列。
自旋优化:空转检查,避免因为进入阻塞队列带来的上下文切换,多核CPU才有用。自旋失败的时候才进行阻塞。单核的时候其实是利用任务队列当做队列,单核其实就很不好,因为自旋优化就是为了减少任务切换。
偏向锁:轻量级锁在没有竞争的时候,每次重入仍需要执行CAS操作。在对象一开始的时候就是使用的偏向锁,如果后面有任意一次竞争或者偏向的改变,即使解锁了,重新加锁,都不是偏向锁了。或者调用wait,notify的时候也会被撤销,终身禁用。因为wait,notify只有重量级锁才有。
批量重偏向:如果发现在t2线程内因为偏向不同而从偏向锁转向轻量级锁太多之后(即撤销偏向锁),后面把这些对象和锁都偏向于t2线程。
批量撤销:如果撤销偏向锁的次数太多,那么后面对于同一个类的对象再也不用偏向锁了。
锁消除:JIT即时编译器会优化,如果非必要会消除锁。-
wait¬ify
进入synchronized代码片段之后(记住wait,notify调用的这个必要条件),调用wait的线程会进入waitset进行等待,并且放弃锁,这时处于waiting状态,等待notify/notifyAll后进入EntryList等待调度,这时处于blocked状态。这意味着A在wait之后,B进入synchronized代码,B调用notify,A还是不会马上执行,至少得等待B退出synchronized代码(或者调用wait放弃锁),因为A在EntryList等待锁,而B没有退出synchronized代码就没有释放锁。
wait,notify都必须在sychronized里面才可以。
两者的使用:wait表示要满足一定的条件,notify表示条件已经满足 wait¬ify使用搭配
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
- join的实现
join是用wait实现的,wait等待线程死掉。
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
park&unpark
与 Object 的 wait & notify 相比
wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll
是唤醒所有等待线程,就不那么【精确】
park & unpark 可以先 unpark,而 wait & notify 不能先 notify-
ReentrantLock可重入锁
lock:得不到锁死等。
lockInterruptly可打断:在获取锁的时候可以打断,退出阻塞,得不到锁而返回。
tryLock:得不到锁立即返回,或者可以设置超时时间。
synchronized只能是死等,相当于只是lock。
但是synchronized会自动释放锁,包括发生异常的时候。 Synchronized和ReentrantLock等价代码
Synchronized{临界代码段}
ReentrantLock lock = new ReentrantLock();
lock.lock ();
try {
临界代码段
}finally{
lock.unlock();
}Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响volatile
保证可见性和有序性,并不能保证原子性。synchronized三者都可以。volatile原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
可见性:
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
有序性:
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前happens-before
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。
-
无锁并发(乐观锁)
CAS:compare and save
-
CAS
变量必须用volatile修饰,不然不能保证获得最新的
为什么无锁效率比较高
上下文切换的损耗比较高。
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻,线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。CAS特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响不可变类
不可变类可以解决资源共享的问题-
内存模型
-
可见性
-
有序性
指令优化的时候会进行重排,但是有些重排在多线程的情况下会出错。
-
如何保证可见性
-
double-check locking
-
happens-before
享元模式flyway
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects-
线程池继承体系
Scheduled修饰的线程池表示这个线程池有定时执行等功能。
ThreadPoolExecutor参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize 核心线程数目 (最多保留的线程数)
maximumPoolSize 最大线程数目
keepAliveTime 生存时间 - 针对救急线程
unit 时间单位 - 针对救急线程
workQueue 阻塞队列
threadFactory 线程工厂 - 可以为线程创建时起个好名字
handler 拒绝策略
下面几个是基于ThreadPoolExecutor的各种线程池
- newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数 == 最大线程数(没有救急线程被创建),阻塞队列是无界的,可以放任意数量的任务,适用于任务量已知,相对耗时的任务。
- newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心线程数是0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着1. 全部都是救急线程(60s 后可以回收)2. 救急线程可以无限创建。
队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)。但是在不超过最大线程数的情况下,每次都会新建新的线程。
这种对比newFixedThreadPool就是另外一种极端,没有固定线程,每次需要多少就创建多少,不需要有一定容量的队列来存储任务。
- newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景:希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:
自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法。Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。
- invoke和execute的区别
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
创建线程数量多少
简单来说,CPU密集型要创建cpu 核数 + 1
个线程差不多,IO密集型要创建多一点,因为IO密集型的线程经常在IO,CPU占有率并不高,多一点CPU占有率才高。线程池任务异常
- 主动捕获
- Future
- SynchronousQueue VS AbstractQueuedSynchronizer(AQS)
SynchronousQueue 同步队列,放进去的时候如果没有人来取会进行阻塞,如果放进去已经有人来取了,那就不会阻塞。
AbstractQueuedSynchronizer 简单说就是同步工具的队列
-
任务放弃策略
-
读写锁的示例
-
synchronize和aqs的区别
-
CountDownLatch
- join跟CountdownLatch的区别
- join比较底层,CountdownLatch比较高层
- join必须等到线程结束的时候才可以,而CountdownLatch只需调用。
CopyOnWriteArrayList
CopyOnWriteArrayList 可以实现读写并发,但是具有弱一致性,其他的并发容器一般只做到读读并发。并发高和一致性是矛盾的。-
线程安全类合集