Java多线程
进程和线程
进程是操作系统分配资源的最小单位,是一个独立的程序实例,拥有独立的地址空间和系统资源
线程是进程内的一个执行单元,是CPU调度的基本单位,不单独拥有系统资源,而是共享进程的资源
进程拥有独立的地址空间、堆、全局变量等,进程不共享资源。因此,创建和销毁进程的开销大,切换进程因为需要保留和恢复进程的全部状态,开销大
线程共享进程的资源,只拥有独立的私有栈和程序计数器。创建和销毁进程只需要对私有栈和程序计数器进行控制,切换线程也只需要对私有栈和程序计数器进行控制
进程通信由于地址空间独立,需要专门机制(如管道、消息队列、信号量、socket网络通信等等)
线程通信因共享进程资源,可以通过共享内存等,但需要同步机制(锁、信号量、屏障等等)
1.Java线程的6个状态

NEW:创建,未调用start()方法,
- 反复调用同一个线程的 start 方法是否可行?
不行,在调用 start 之后,threadStatus 的值会改变(threadStatus !=0),再次调用 start 方法会抛出 IllegalThreadStateException 异常。 - 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start 方法是否可行?
threadStatus 为 2 代表当前线程状态为 TERMINATED
Runnable:包括了操作系统的就绪态和运行态
BLOCKED:等待锁的释放进入同步区
Waiting:等待唤醒进入Runnable
TIMED_WATING:超时等待,等待具体的时间,到时间被唤醒
TERMINATED:终止状态
2.死锁
互斥:该资源任意一个时刻只由一个线程占用
占有并等待:一个线程因请求资源而阻塞时,对已获得的资源保持不放
不可剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺
循环等待:若干线程之间形成一种头尾相接的循环等待资源关系
3.volatile
保证可见性,禁止指令重排序
当写一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去
执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见
4.Synchronized
被它修饰的方法或者代码块在任意时刻只能有一个线程执行
原理:在JVM层面synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置
当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权
问题
1.不论读还是写,都只允许一个线程操作
2.无法知道线程是否获取到锁
3.如果出现阻塞,当前线程没有释放锁,所有线程都会等待
锁优化
jdk1.6之前,所有的锁都是”重量级“锁,涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低
JDK 1.6 引入了“偏向锁”和“轻量级锁” 的概念,对 synchronized 做了一次重大的升级
无锁->偏向锁->轻量级锁->重量锁
几种锁会随着竞争情况逐渐升级,锁的升级很容易发生;锁降级发生的条件就比较苛刻了,锁降级发生在 STW,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。
偏向锁在资源无竞争情况下消除了同步语句,一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID
轻量级锁是通过 CAS 操作和自旋来实现的,如果自旋失败,则会升级为重量级锁。
重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU
5.CAS
硬件级别的原子操作,新值,预期值,旧值
将预期值与旧值比较,如果相同就把新值覆盖旧值;否则一直等待
Unsafe 对 CAS 的实现是通过 C++ 实现的,还有一些atomic原子类,之后说
三大问题:
ABA:
一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。追加上版本号或者时间戳
长时间自旋:
如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。让 JVM 支持处理器提供的pause 指令
多个共享变量的原子操作:
一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性。
使用AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作
加锁
6.AQS
简单说,AQS是一个抽象类,为同步器定义了通用的执行框架,定义了资源的获取和释放的通用流程,具体的资源获取释放逻辑通过具体同步器通过重写模板方法来实现。
由Volatile的state变量和先进先出的双向队列组成

获取资源:CAS尝试修改state,失败就进入队列,可能会自旋
释放资源:通过CAS修改state,无需自旋,通过unpark()唤醒后续线程
7.锁和JUC
由于synchronized的问题,我们可以用JUC包下的锁弥补
分类:
1.悲观锁、乐观锁
2.自旋锁、适应性自旋锁(不阻塞),阻塞
3.无锁,偏向锁、轻量级锁、重量级锁

4.公平锁、非公平锁 (是否先来后到)
5.可重入锁(ReentrantLock,Synchronized)、非可重入锁
6.共享锁、排他锁(synchronized,reentrantlock)
JUC的locks包下的3个接口:Condition,Lock,ReadWriteLock
锁王:StampedLock,有读写锁的功能,读锁分为乐观读锁和悲观读锁,在读的时候如果发生了写,应该通过重试来获取新的值,而不是阻塞,读多写少非常适合,避免写饥饿
写锁在释放的时候,不是减WBIT,而是加WBIT,解决ABA问题
8.ReentrantLock
支持重入,能够对共享资源重复加锁,当前线程获取该锁之后再次获取不会阻塞
支持公平锁和非公平锁,公平锁意思是锁的获取顺序按照请求上的绝对时间顺序,FIFO
公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁
内部类内部类Sync继承AQS
ReentrantLock 可以实现多路选择通知(可以绑定多个 Condition),而 synchronized 只能通过 wait 和 notify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知)
9.线程池
核心线程数、最大线程数、空闲线程存活时间、时间单位、工作队列、线程工厂、拒绝策略
1.当前线程数<核心线程数,通过ThreadFactory创建一个线程执行任务,执行完去阻塞队列获取任务
2.线程数>核心线程数,将任务放入阻塞队列
3.阻塞队列满了,当前线程数<最大线程数,创建非核心线程执行提交的任务
4.当前线程数达到最大线程数,执行拒绝策略:AbortPolicy(丢弃任务,抛出异常),CallerRunsPolicy(由提交任务的线程执行任务,不是线程池内部的线程,DiscardPolicy(丢弃任务,不抛出异常),DiscardPolicy(剔除最先进入队列的任务,再次提交任务)
复用:线程池的线程是被封装成了work对象,work对象继承了AQS,调用 runWorker 方法来让线程执行任务,runWorker会一直while循环,不断getTask()执行任务
shutdown:执行队列中的任务
shutdownnow:尝试停止所有正在执行的任务,停止处理等待的任务,并返回等待处理的任务列表
ThreadPoolExecutor executor=new ThreadPoolExecutor(
10,
10,
200,
TimeUnit.MICROSECONDS,
new ArrayBlockingQueue<>(1000)
);
Runnable task=()->{
//System.out.println("wasd");
};
AtomicInteger atomicInteger=new AtomicInteger(0);
for(int i=0;i<1000;i++){
executor.execute(task);
Thread.sleep(1);
atomicInteger.incrementAndGet();
}
System.out.println("次数"+atomicInteger.get());
List<Runnable> runnables = executor.shutdownNow();
System.out.println(runnables.size());
不推荐使用 Executors 来创建线程池,newFixedThreadPool 线程池由于使用了 LinkedBlockingQueue,队列的容量默认无限大,实际使用中出现任务过多时会导致内存溢出;newCachedThreadPool 线程池由于核心线程数无限大,当任务过多的时候会导致创建大量的线程,可能机器负载过高导致服务宕机。
10.并发工具类
AtomicInteger提供线程安全的原子操作,无需加锁,核心基于CAS实现的,适用于需要频繁对数值进行无锁操作
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet();
atomicInt.decrementAndGet();
atomicInt.compareAndSet(1,2);
Semaphore控制访问资源的线程数量
Semaphore semaphore=new Semaphore(50);
try{
semaphore.acquire();
}catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
CountDownLatch适用于主线程需要等待多个子线程完成任务的场景
AtomicInteger a=new AtomicInteger(3);
CountDownLatch countDownLatch=new CountDownLatch(3);
Runnable Task=()->{
try{
a.decrementAndGet();
}finally {
countDownLatch.countDown();
}
};
new Thread(Task).start();
new Thread(Task).start();
new Thread(Task).start();
countDownLatch.await();
System.out.println(a.get());
11.创建线程的几种方法
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
}
});
thread1.start();
FutureTask<Integer> futureTask=new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
return null;
}
});
Thread thread2=new Thread(futureTask);
thread2.start();
//3.继承Thread类,重写run方法,创建对象调用start方法
//4.使用线程池
//5.CompletableFuture
12.sleep&wait
sleep释放CPU,不释放锁,超时自动恢复
wait释放锁,需要notify唤醒
13. 多线程之间怎么通信?
共享变量(volatile/synchornized/Lock)
等待通知(wait/notify/notifyall)
并发工具类(CyclicBarrier/CountDownLatch/Semaphore)
阻塞队列(BlockingQueue)
原子类(AtomicInteager)
14. 多进程直接怎么通信?
分为同一台主机和不同主机
不同主机:socket通信,方式有3种 TCP/UDP/同一进程内部通信
同一主机:
1.管道:匿名/命名管道 本质是内核缓冲区
2.消息队列:实际上是保存在内核的消息链表
3.共享内存:虽然不同进程无法直接共享内存,通过多进程访问同一共享地址空间,再通过信号量保证共享资源互斥访问
4.信号:异步通信机制,用于应用进程和内核直接交互
问题:
为什么多进程不能共享内存?
操作系统通过虚拟内存和页表机制隔离进程地址空间,需要通过内核显示映射内存才能共享
更多推荐



所有评论(0)