概念
进程和线程
- 进程:有独立内存空间,每个进程中的数据空间都是独立的。(系统运行程序的基本单位)
- 线程:多线程之间堆空间与方法区是共享的,但每个线程的栈空间、程序计数器是独立的,线程消耗的资源比进程小的多。(进程中的一个执行单元)
并发与并行
- 并发(Concurrent):同一时间段,多个任务都在执行 ,单位时间内不⼀定同时执行。
- 并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行。并行上限取决于CPU核数(CPU时间片内50ms)
线程上下文切换
一个CPU内核,同一时刻只能被一个线程使用。为了提升CPU利用率,CPU采用了时间片算法将CPU时间片轮流分配给多个线程,每个线程分配了一个时间片(几十毫秒/线程),线程在时间片内,使用CPU执行任务。当时间片用完后,线程会被挂起,然后把 CPU 让给其它线程。
CPU切换前会把当前任务状态保存下来,用于下次切换回任务时再次加载。任务状态的保存及再加载的过程就叫做上下文切换
。
程序计数器:用来存储CPU正在执行的指令的位置,和即将执行的下一条指令的位置。他们都是CPU在运行任何任务前,必须依赖的环境,被叫做CPU上下文。
上下文切换过程:
- 挂起当前任务任务,将这个任务在 CPU 中的状态(上下文)存储于内存中的某处。
- 恢复一个任务,在内存中检索下一个任务的上下文并将在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指定的位置(即跳转到任务被中断时的代码行)。
过多的线程并行执行会导致CPU资源的争抢,产生频繁的上下文切换,常常表现为高并发执行时,RT延长。因此,合理控制上下文切换次数,可以提高多线程应用的运行效率。(也就是说线程并不是越多越好,要合理的控制线程的数量。)
- 直接消耗:指的是CPU寄存器需要保存和加载,系统调度器的代码需要执行
- 间接消耗:指的是多核的cache之间得共享数据,间接消耗对于程序的影响要看线程工作区操作数据的大小
线程状态(6种)
- NEW(新建):线程刚被创建,但是并未启动。
- RUNNABLE(可运行):线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
- BLOCKED(锁阻塞): 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
- WAITING(无限等待): 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
- TIMED_WAITING(计时等待): 同waiting状态,有几个方法有超时参数,调用他们将进入TimedWaiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
- TERMINATED(被终止): 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
常用方法:yield()线程让步;sleep()线程休眠;join()等待线程执行终止的方法;interrupt()线程中断;wait、notify等待与通知
wait与sleep区别
- 主要区别:sleep()方法没有释放锁,而wait()方法释放了锁
- 两者都可以暂停线程的执行
- wait()通常用于线程间的交互/通信,sleep()通常用于暂停线程执行
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象的notify或notifyAll。
- sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long)超时后,线程也会自动苏醒
线程安全问题
如果程序每次运行结果和单线程运行的结果一样,且其他的变量的值也和预期一样,就是线程安全的,反之则是线程不安全的。
引发线程安全问题: 线程安全问题都是由全局变量及静态变量【共享】引起的
- 如果每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
- 如果有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全问题
解决问题:线程同步、volatile、CAS、AQS锁
线程同步synchronized
private final Object lock = new Object();//锁对象,可以是任意类型数据
//同步代码块
synchronized(lock){
需要同步操作的代码
}
//同步方法
public synchronized void method(){
可能会产生线程安全问题的代码
}
//Lock锁
Lock lock = new ReentrantLock();//可重入锁
lock.lock();
需要同步操作的代码
lock.unlock();
多线程并发的3个特性 O(∩_∩)O
并发编程中,三个非常重要的特性:原子性,有序性和可见性
- 原子性:即一个操作或多个操作,要么全部执行,要么就都不执。执行过程中,不能被打断
- 有序性:程序代码按照先后顺序执行(因为指令重排)
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值(因为Java内存模型【JMM】)
要想多线程程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
volatile保证了操作的可见性,所修饰的变量不具备线程缓存,所以多个线程同时操作,也只会操作内存中的那个i,一个线程修改了i的值 其他线程可以立刻见到新值,这就是可见性.
指令重排序
重排序是编译器和处理器为了提高程序运行效率,会对输入代码进行优化的一种手段。它不保证程序中,各个语句执行先后顺序的一致。
按顺序执行不好么,为什么要重排序去执行
- 不进行指令重排,就相当于没有编译优化,那么程序执行效率就有问题
- 当前线程获取CPU时间片(几十毫秒),如果按照先后顺序执行,并不能把CPU性能发挥完全,上一个指令执行完执行下一个,CPU会出现空挡期。
什么是as-if-serial语义?
- 不管编译器和处理器怎么优化字节码指令,怎样进行指令重排,单线程所执行的结果不能受影响.
- 处理器在进行重排序时,会考虑指令之间的数据依赖性,如果一个 指令2 必须用到 指令1 的结果,那么处理器会保证指令1会在指令2之前执行
- 虽然重排序不会影响单个线程内程序执行的结果,但是多线程会有影响。
java内存模式 JMM
JMM并不像JVM内存结构一样是真实存在。它只是一个抽象的概念。是和多线程相关的,描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时,对另一个线程是可见的。
JMM就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种
平台下对内存的访问都能保证效果一致的机制及规范。
- 主内存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)。
- 本地内存:主要存储当前方法的所有变量,每个线程只能访问自己的本地内存。线程中的本地变量对其它线程是不可见的。本地内存是抽象的,不真实存在,涵盖:缓存,写缓冲区,寄存器等
JMM线程操作内存的基本规则
- 第一条,关于线程与主内存:线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读写
- 第二条,关于线程间本地内存:不同线程之间无法直接访问其他线程本地内存中的变量,线程间变量值的传递需要经过主内存
内存可见性
可见性是一个线程对共享变量值的修改,能够及时的被其他线程看到。
- 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
- 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。
JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序提供内存可见性的保证。
怎么解决?
- 使用Synchronized同步代码块
- 彻底禁止JMM?禁止重排序和读取本地内存副本
- happens-before规则:按需使用重排序和本地内存副本,前提是需要满足happens-before规则
happens-before规则
happens-before规则是JMM中的一种,保障内存可见性的方案。
happens-before的实现:1.处理器重排序规则,2.编译器重排序规则。
synchronized
JMM关于synchronized的两条规定:
- 线程解锁前:必须把自己本地内存中共享变量的最新值刷新到主内存中
- 线程加锁时:将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
实现过程
- 获得互斥锁(同步获取锁)
- 清空本地内存
- 从主内存拷贝变量的最新副本到本地内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
同步原理
同步操作主要是monitorenter和monitorexit这两个jvm指令实现的。
public class Demo05Synchronized {
public synchronized void increase(){
System.out.println("synchronized 方法");
}
public void syncBlock(){
synchronized (this){
System.out.println("synchronized 块");
}
}
}
同步代码块和同步方法的字节码是不同的
- 对于synchronized同步块,对应的
monitorenter
和monitorexit
指令分别对应synchronized同步块的进入和退出。- 为什么会多一个monitorexit?编译器会为同步块添加一个隐式的try-finally,在finally中会调用monitorexit命令释放锁
- 对于synchronized方法,对应
ACC_SYNCHRONIZED
关键字,JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁,方法调用结束了释放锁。在JVM底层,对于这两种synchronized的实现大致相同。
monitorenter和monitorexit指令,主要是基于 标记字段MarkWord和Monitor(管程)。