这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
进程与线程
- 进程:系统进行资源分配的最小单元,每个进程都有独立的代码和数据空间--进程的上下文
- 上下文切换:cpu从一个进程切换到另一个进程的动作
- 线程:cpu调度的对消单位,是进程的一部分,只能由进程创建(分为用户线程和守护线程)
- 每个线程共享进程的数据空间,它们分别有独立的栈和程序计数器
并发与并行
- 并行:同一时间在多台计算机上同时运行多个任务(类似:10个小朋友分两组,一组抢玩具车玩,另一组抢玩具熊玩,这两组小朋友就是
并行
玩耍) - 并发:同时执行多个任务(类似:10个小朋友抢一个文具玩,每人玩一会儿)
public class MyThread extends Thread{
@Override
public void run(){
// 当run()方法执行完毕并返回时,当前线程将终止
for(int i = 0; i < 1000; i++){
try{
Thread.sleep(10L);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
复制代码
tips:
- 调用start()方法后,run()方法中的代码并不一定立即开始执行,start()方法只是将县城变为可
就绪状态
,什么时候运行由操作系统决定的
wait和sleep的区别
- wait()方法必须在synchronized同步块或方法中使用
- wait()方法形成的阻塞,可以通过针对同一个对象锁的synchronized作用域调用
notify()/notifyAll()
来唤醒;而sleep()则无法被唤醒,其只能定时醒来或被interrupt()方法中断
sleep和yield的区别
- 调用
sleep()
方法后转入阻塞状态
,并在睡眠一段时间后自动醒来回到就绪状态
- 调用
yield()
方法后,当前线程转入就绪状态
- sleep()方法是,线程无论优先级高低都有机会运行,而yield()只会给那些相同优先级或更高优先级的线程运行的机会
线程池
面试常问:如何创建线程池
- 根据《阿里开发手册》线程池不允许使用
Executors
创建,而是通过ThreadPoolExecutor
的方式创建 - Executors返回的线程池对象的弊端
1.FixedThreadPool 和 SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2.CachedThreadPool 和 ScheduleThreadPool
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
复制代码
常问参数:
int corePoolSize
核心线程数
int maximumPoolSize
最大线程数
keepAliveTime
线程最大空闲时长
保证线程同步的手段(简单列举)
CAS操作
- 原理:在更新某个变量之前,检查变量的当前值是够符合期望值,如果相符就用新值代替旧值,否则循环重试(自旋)直到成功
- 用cas简单实现一个计数器(包括线程安全/不安全)
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100a; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println(System.currentTimeMillis() - start);
}
/** * 使用CAS实现线程安全计数器 */
private void safeCount() {
for (;;) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非线程安全计数器
*/
private void count() {
i++;
}
}
复制代码
cas三大问题
1.ABA问题。
- 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,
- 但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,
- 但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,
每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
。 - 从Java 1.5开始,JDK的Atomic包里提供了一个类
AtomicStampedReference
来解决ABA问题。 - 这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,
- 如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2. 循环时间长开销大。
- 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的
pause
指令, - 那么效率会有一定的提升。pause指令有两个作用:
第一,它可以
延迟流水线执行指令
(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;
第二,它可以
避免在退出循环的时候因内存顺序冲突
(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而
提高CPU的执行效率。
3. 只能保证一个共享变量
的原子操作。
- 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,
- 但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。
- 比如,有两个共享变量
i=2,j=a,合并一下ij=2a
,然后用CAS来操作ij。从Java 1.5开始, - JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
Lock自旋锁
java.util.concurrent.locks
包中提供了锁Lock
接口及其实现类ReentrantLock
- 在
ReentrantLock
中,调用lock()
方法获取锁;调用unlock()
方法释放锁。 ReentrantLock
的实现依赖于Java同步器框架AbstractQueuedSynchronizer
(本文简称之为AQS)。AQS使用一个整型的volatile
变量(命名为state)来维护同步状态
源码
CountDownLatch 计数器
- 多个线程通过调用它们所共享的计数器(
CountDownLatch
对象)的countDown()
方法来让计数器减1 - 可以通过
CountDownLatch
对象的await
方法来阻塞当前线程,直到计数器的值为0;
1.
- 当我们调用CountDownLatch的
countDown()
方法时,N就会减1,CountDownLatch的await()
方法会阻塞当前线程, - 直到N变成零。由于
countDown
方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。 - 用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
2.
- 如果有某个解析sheet的线程处理得比较慢,我们不可能让主线程一直等待,所以可以使用另外一个带指定时间的
await()
方法 await(long time,TimeUnit unit)
,这个方法等待特定时间后,就会不再阻塞当前线程。join
也有类似的方法。
CyclicBarrier 柵栏
- CyclicBarrier 是一种可重用的线程阻塞器,通过调用
await()
方法在代码中形成"柵栏",率先执行到"柵栏"(await()方法)阻塞,知道制定数量的线程也都达到"柵栏"处。
近期评论