1 线程、进程
进程:可视为程序的一个实例,资源分配的最小单位
线程:程序执行的最小单位
关系: 包含关系
2并行和并发
并发:同一时间应对多件事的能力
并行:同一时间动手做多件事的能力
3 线程创建的方式
- 继承Thread 接口
public class FirstThreadTest extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
if (i == 50) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
`
- 通过Runnable接口创建线程类
public class RunnableThreadTest implements Runnable{
private int i;
public void run() {
for(i = 0;i <100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args){
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20)
{
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt,"新线程1").start();
new Thread(rtt,"新线程2").start();
}
}
}
}
- 通过Callable和Future创建线程
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
if (i == 20) {
new Thread(ft, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
}
对比
- Runnable、Callable 是通过实现接口,还可以继承其他类;通过实现接口的方式可以共享资源,适合多线程处理同一资源的情况;缺点编程相对繁琐。
Runnable和Callable的区别
- Runnable执行方法是run(),Callable是call()
- 实现Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回执行结果(Callable接口支持返回执行结果,需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取结果;当不调用此方法时,主线程不会阻塞! )
- call方法可以抛出异常,run方法若有异常只能在内部消化(如果线程出现异常,Future.get()会抛出throws InterruptedException或者ExecutionException;如果线程已经取消,会抛出CancellationException)
4守护线程
服务器其他线程的线程(垃圾回收器)
5线程状态
操作系统层面
java API 层面
6 常用方法
run / start
- run 不会创建新的线程、而是与当前线程执行任务代码
- start 会启动新的线程执行任务代码
sleep / wait
- sleep 方法属于Thread 类,wait 方法属于 Object
- sleep 不需要强制和 synchronized 配合使用,但wait 需要和synchronized 一起用
- sleep 在睡眠的同时不会释放对象的锁,wait 在等待的时候会释放锁
- sleep 睡眠结束会自动进入可运行状态,wait 必须等待notify 唤醒
wait / notify
notifty 、notifyAll
notify 随机唤醒一个线程,notifyAll 唤醒所有线程
7死锁、活锁、饥饿
死锁
死锁指的是某个资源占用后,一直得不到释放,导致其他需要这个资源的线程进入阻塞状态
必要条件
- 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
- 占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
- 非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
- 循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。我们强调所有四个条件必须同时成立才会出现死锁。循环等待条件意味着占有并等待条件,这样四个条件并不完全独立。
活锁
任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。 处于活锁的实体是在不断的改变状态,活锁有可能自行解开。
饥饿
一个线程因为 CPU 时间全部被其他线程抢占而得不到 CPU 运行时间,导致线程无法执行。
产生原因:
- 优先级线程吞噬所有的低优先级线程的 CPU 时间
- 其他线程总是能在它之前持续地对该同步块进行访问,线程被永久堵塞在一个等待进入同步块
- 其他线程总是抢先被持续地获得唤醒,线程一直在等待被唤醒
8 synchronized
作用
- 确保线程互斥的访问同步代码块
- 保证共享变量的修改能够及时可见
- 有效解决重排序问题
用法
- 修饰普通方法(锁当前方法对象)
- 修饰静态方法(锁类对象)
- 修饰代码块(锁传入对象)
原理
Monitor(锁) 原理
虚拟机给每个对象和class 字节码都设置了一个监听器Monitor ,用于检测并发代码的重入。
Synchronized 原理
Synchronized 是有JVM 实现的一种互斥同步的一种方式,在上面字节码中可以看到Synchronized 同步的代码前后被编译器生成了monitorenter 和 monitorexit 两个字节码指令。
- 如果获取不到锁,进入阻塞,直到对象锁被另一个线程释放为止(其它线程释放锁时会通知阻塞线程竞争锁)
- Synchronized 是有JVM 实现的一种互斥同步的一种方式,当执行到同步代码块时会获取与该对象关联的Monitor ,查看Monitor 中的Owner 是否为Null 或者当前线程,如果满足其一,把锁计数器+1 ,如果获取失败了就会进入Monitor 中的EntryList 中BLOCKED ,直到持有锁的线程释放锁,唤醒所有EntryList 中BLOCKED 的所有线程来竞争锁。
可重入性
可重入性是锁的一个基本要求,为了解决自己把自己锁死的情况,synchronized 是可重入锁
优化
偏向锁
当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
轻量级锁
如果有另一线程试图锁定某个被偏斜过的对象,JVM 就撤销偏斜锁,切换到轻量级锁实现
重量级锁
轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
粗化
原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。
消除
指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
非公平性
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
悲观锁
不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
乐观锁
乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
CAS 具有原子性(JDK提供Unsafe 类执行这些操作)
缺陷
- 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
- 长时间自旋可能导致开销大。假如 CAS 长时间不成功而一直自旋,会给 CPU 带来很大的开销。
- ABA 问题。
不可中断
当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
synchronized和 Lock 的区别(重要)
- Lock 是接口,而synchronized 是JAVA 中的关键字,synchronized是内置语言实现的
- synchronized 发生异常时能够自动释放锁,而Lock 必须主动释放锁,所以容易造成死锁。
- lock 可以让等待锁的线程响应中断,而synchronized 不行。
- lock 可以获取锁的状态(tryLock()方法:如果获取锁成功,则返回true),synchronized 不行
- lock 可以提高多线程进行读写的效率
synchronized和ReentrantLock(重入锁) 的区别
- 都是可重入锁,sychnronized关键字隐式的支持重进入
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)
- ReentrantLock 比 synchronized 增加了一些高级功能,主要有3点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
volatile关键字
保证内存可见性
主内存中变量发生变动后,强迫从主内存中重新获取新值
禁止指令重排序
如果一个变量被声明volatile的话,那么这个变量不会被进行重排序,也就是说,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它慢执行。(内存屏障)
不能保证原子性
适用一个线程修改,多线程读的场景
synchronized 关键字和 volatile 关键字的区别
- volatile 关键字是线程同步的轻量级实现,所以volatile 性能比synchronized 关键字要好。但是volatile 关键字只能用于变量而synchronized 可以修饰代码块和方法
- 多线程volatile 不会反生阻塞,synchronized 会发生阻塞
- volatile 主要用于解决变量多线程中的可见性,synchronized 解决多线程之间访问资源的同步性
- volatile 不能保证原子性 synchronized 可以保证
线程池
概念
管理线程的池子
- 帮助管理线程,避免增加创建线程和销毁线程的资源损耗
- 提高响应速度
- 重复利用。线程用完在放回池子,可以重复利用,节省资源
常用线程池
newFixedThreadPool (固定数目线程的线程池)
- 核心线程数和最大线程数大小一样
- 没有所谓的非空闲时间,即keepAliveTime为0
- 阻塞队列为无界队列LinkedBlockingQueue(任务太多可能出现OOM)
- FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
newCachedThreadPool(可缓存线程的线程池)
- 核心线程数为0
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列是SynchronousQueue
- 非核心线程空闲存活时间为60秒
- 用于并发执行大量短期的小任务。
newSingleThreadExecutor(单线程的线程池)
- 核心线程数为1
- 最大线程数也为1
- 塞队列是LinkedBlockingQueue
- keepAliveTime为0
- 适用于串行执行任务的场景,一个任务一个任务地执行
newScheduledThreadPool(定时及周期执行的线程池)
- 周期性执行任务的场景,需要限制线程数量的场景
线程池的状态
//线程池状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
状态切换
RUNNING
- 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
- 调用线程池的shutdownNow()方法,可以切换到STOP状态;
SHUTDOWN
- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
- 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;
STOP
- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- 线程池中执行的任务为空,进入TIDYING状态;
TIDYING
- 该状态表明所有的任务已经运行终止,记录的任务数量为0。
- terminated()执行完毕,进入TERMINATED状态
TERMINATED
- 该状态表示线程池彻底终止