1.并发与并行
- 并发:指两个或多个事件在同一时间段内发生的。(交替执行)
- 并行:指两个或多个事件在同一时刻发生。(同时执行)
2.线程与进程
- 进程:指正在运行中的应用程序,进程是程序的一次执行过程,是系统运行程序的基本单位。
- 线程:线程是进程中的一个执行单元,负责当前进程中任务的执行,一个进程中至少有一个线程。
3.线程状态
新建(New)
可运行(Runnable)
阻塞(Blocked)
等待(Waiting)
超时等待(Timed Waiting)
死亡(Terminated)
4.synchronized和Lock的区别?
1.锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2.性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3.释放锁资源
synchronized在执行完同步后,自动释放锁,Lock需要手动启动同步,结束同步也需要手动实现。发生异常时Lock可能发生死锁,因此 lock.unlock()放在finally 中。
4.响应中断
Lock可以让等待锁的线程响应中断,而synchronized 不能响应中断。
5.一个 ReentrantLock 可以同时绑定多个 Condition 对象。、
5.sleep() 和wait() 的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
2.不同点:
1) 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait方法。
2)调用要求不同:sleep()可以在任意需要的场景下调用,wailt()必须使用在同步代码块或同步方法。
3) 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait会释放锁。
6. Callable接口的方式创建多线程比实现Runnable接口强大?
call()可以有返回值,
call()可以抛出异常,被外面的操作捕获,获取异常的信息
Callable支持泛型
7.创建多线程有几种方式?
4种方式,继承Thread类 实现Runnable接口 实现Callable接口
线程池(响应速度快,提高资源重用率,便于管理)
8. synchronized
底层是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
9.CountDownLatch(闭锁)
CountDownLatch 主要有两个方法,当一个或多个线程调用await方法,这些线程就会阻塞。
其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
当计数器的值变为0时,await放法阻塞的线程会被唤醒,继续执行。
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t离开教室");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t班长关门");
10. CyclicBarrier(栅栏)
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待, 直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{System.out.println("召唤龙珠");});
for (int i = 1; i <=7 ; i++) {
int number = 1;
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t收集到第:"+number+"颗龙珠");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
11. Semaphore(信号量)
在信号量上我们定义了两种操作:
acquire(获取)当一个线程调用acquire操作时,要么通过,成功获取信号量(信号量减1),要么一直等待下去,直到有线程释放信号量,或超时。
release(释放)实际上会将信号量加1,然后唤醒等待的线程。
信号量主要有两个目的,一个用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
//假设三个车位
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//占用
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "\t占到了车位");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放车位
semaphore.release();
}
}, String.valueOf(i)).start();
}
12.FutureTask
Callable返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
13.阻塞队列
FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度) 优先级队列 :PriorityBlockingQueue
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列;这两个附加的操作支持阻塞的插入和移除方法:
- 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满;
- 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空;
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器;
抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出 IllegalStateException Queue full 异常;当队列为空时,从队列里获取元素时会抛出 NoSuchElementException 异常 ;
返回特殊值:插入方法会返回是否成功,成功则返回 true;移除方法,则是从队列里拿出一个元素,如果没有则返回 null ;
一直阻塞:当阻塞队列满时,如果生产者线程往队列里 put 元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里 take 元素,队列也会阻塞消费者线程,直到队列可用;
超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出;
使用 BlockingQueue 实现生产者消费者问题
14.线程池
线程池主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大线程数量(maxPoolSize),超出数量的线程就会排队等待,等待其它线程执行完毕,再从队列中取出任务来执行;
主要特点是:线程复用,控制量大的并发数,管理线程(降低系统资源消耗,提高系统响应速度);
使用 Executors 线程池的工具类可以提供工厂方法用来创建不同类型的线程池;newFixedThreadPool:创建固定线程数的线程池;
newSingleThreadExecutor:创建一个只有一个线程的线程池;newCachedThreadPool:可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程(一池 N 个工作线程,类似扩容);
上述三个创建的线程池,底层都是由 ThreadPoolExecutor 来进行创建的;关闭线程池调用 shutdown 方法;
ThreadPoolExecutor参数
- corePoolSize:线程池中保持的线程数量(常驻核心线程数),包括空闲线程在内,也就是线程池释放的最小线程数量界限;
- maximumPoolSize:线程池中能够容纳最大线程数量(必须大于等于 1);
- keepAliveTime:当前线程数量大于核心线程数量,并且空闲线程保持在线程池中的时间大于存活时间 keepAliveTime 时,就释放空闲的线程(maximumPoolSize - corePoolSize);
- TimeUnit(枚举类) unit:是 keepAliveTime 参数时间的单位,可以是分钟,秒,毫秒等等;
- BlockingQueue< Runnable > workQueue:任务队列,当线程任务提交到线程池以后,首先放入队列中,然后线程池按照该任务队列依次执行相应的任务;可以使用的 workQueue 有很多,比如:LinkedBlockingQueue 等等;
- ThreadFactory threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般使用默认即可;
- RejectedExecutionHandler handler:如果队列已满,并且工作线程大于了最大线程数(maximumPoolSize),就会按照指定的拒绝策略拒绝执行任务;(注意都是静态内部类)
拒绝策略:
AbortPolicy:丢弃任务并抛出异常(默认策略)
DiscardPolicy:也是丢弃任务,但是不抛出异常;
DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务;
CallerRunsPolicy:不会抛弃任务,也不抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;
线程池工作原理:
15. 锁优化
JVM对synchronized的优化
自旋锁:
自旋锁的思想是让一个线程在请求一个共享数据的锁时自旋一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的 锁定状态很短的场景。
锁消除:
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
轻量级锁:
使用CAS避免重量级锁使用互斥量的开销。。对于绝大部分的锁, 在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如 果 CAS 失败了再改用互斥量进行同步。
偏向锁:
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
16.死锁
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
synchronized (right) {
synchronized (left) {
doSomethingElse();
}
}
}
}
死锁的4个必要条件
互斥条件(互斥使用资源)、不可剥夺条件(只能进程主动释放)、请求和保持条件(申请新资源时继续占用原有资源)、循环等待条件(等待对方释放需要的资源)
显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限,tryLock()不会一直等待,而是返回错误信息。使用tryLock()能够有效避免死锁问题
17.CAS
比较并交换(compare and swap, CAS),是原子操作的一种。
CAS有3个操作数:
- 内存值V
- 旧的预期值A
- 要修改的新值B
多个线程尝试使用CAS更新同一个变量时,只有一个线程能更新变量的值。
当 A==V,将V修改为B,如果不相等,什么都不做或者自旋。
重试(自旋):当线程A和线程B同时读取 i=0,A得到CPU执行权,进行了+1操作,i更新为1,B线程发现此时内存值为1,不是预期值0,将预期值更新为内存值,然后执行操作。
什么都不做:直接结束线程。
18.原子变量类
- AtomicBoolean:布尔型
- AtomicInteger:整型
- AtomicLong:长整型
18.ABA问题
使用AtomicStampedReference解决
为对象提供一个版本,版本被修改则自动更新。
19.ThreadLocal
hreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!
原理总结:(需要手动删除key,否则会内存泄漏)
- 每个Thread维护着一个ThreadLocalMap的引用
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。