Java并发编程基础
Contents
1 多线程相关概念
1.1 线程和进程
1.1.1 什么是线程?什么是进程?
-
进程:是指内存中运行的一个应用程序,每个进程都有自己独立的内存空间;进程也是程序的一次执行过程,是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建、运行到消亡的过程。
-
线程:是进程中的一个执行单元,负责当前进程中任务的执行。一个进程在其执行过程中,会产生很多个线程。
-
从操作系统角度来看,
- 从用户态来看,创建进程其实就是启动一个项目。但是这个项目需要人去执行。有多个人并行执行不同的部分,这就叫
多线程(Multithreading)。如果只有一个人,那它就是这个项目的主线程。 - 但是从内核态来看,无论是进程,还是线程,我们都可以统称为任务(Task),都使用相同的数据结构
task_struct,平放在同一个链表中。该数据结构中包含 pid, tgid 两个字段,通过对比 pid 和 tgid 可判断是进程还是线程。
- 从用户态来看,创建进程其实就是启动一个项目。但是这个项目需要人去执行。有多个人并行执行不同的部分,这就叫
1.1.2 进程与线程的区别?
- 进程:有独立内存空间,每个进程中的数据空间都是独立的。
- 线程:多线程之间堆空间与方法区是共享的,但每个线程的栈空间、程序计数器是独立的,线程消耗的资源比进程小的多。
1.1.3 什么是多线程?
- 多线程(multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。
1.1.4 并发与并行的区别?
- 并发(Concurrent):同一时间段,多个任务都在执行 ,单位时间内不⼀定同时执行
- 并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行
- 并发是一种能力,并行是一种手段
1.1.5 什么是线程上下文切换?
-
一个CPU内核,同一时刻只能被一个线程使用。为了提升CPU利用率,CPU采用了时间片算法将CPU时
间片轮流分配给多个线程,每个线程分配了一个时间片(几十毫秒/线程),线程在时间片内,使用CPU
执行任务。当时间片用完后,线程会被挂起,然后把 CPU 让给其它线程。
- CPU切换前会把当前任务状态保存下来,用于下次切换回任务时再次加载。
- 任务状态的保存及再加载的过程就叫做上下文切换。
- 保存在哪里?程序计数器
-
过多的线程并行执行会导致CPU资源的争抢,产生频繁的上下文切换,常常表现为高并发执行时,RT延长。
1.1.6 java线程的生命周期?
- 查看Thread源码,能够看到java的线程有六种状态:
public enum State {
NEW, //创建但并未启动
RUNNABLE, //线程在Java虚拟机中处于可以运行的状态,是否正在执行取决于操作系统处理器
BLOCKED, //试图获取到锁但没有得到后处于这个状态
WAITING, //无法自动唤醒,必须等待另一个线程调用notify或者notifyAll
TIMED_WAITING, //这一状态将一直保持到超时期满或者接收到唤醒通知
TERMINATED; //被终止。因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡
}

-
Java线程常用方法:
- 线程让步:yield()
- 让线程休眠的方法:sleep()
- 等待线程执行终止的方法: join()
- 线程中断interrupt()
- 等待与通知系列函数wait()、notify()、notifyAll()
-
wait()与sleep()区别
- 主要区别:sleep()方法没有释放锁,而wait()方法释放了锁
- 两者都可以暂停线程的执行
- wait()通常用于线程间的交互/通信,sleep()通常用于暂停线程执行
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象的notify或notifyAll。
- sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long)超时后,线程也会自动苏醒
1.2创建线程在JVM中的实现原理
- 线程类被JVM加载时,完成线程所有native方法和C++中的对应方法绑定。
- Java线程调用start方法:start方法==>native state0方法==>JVM_StartThread==>创建JavaThread::JavaThread线程
- 创建OS线程,并指定OS线程的运行入口:创建JavaThread::JavaThread线程==>创建OS线程os::create_thread==> 指定OS线程执行入口Java线程的run方法
- 启动OS线程:运行时会调用Java线程的run方法,至此实现了Java线程的运行。
- 创建线程的时候使用的是互斥锁MutexLocker操作系统(互斥量),所以说创建线程是一个性能很差的操作!
1.3线程安全问题
1.3.1什么是线程安全?
-
如果有多个线程在同时执行,而多个线程可能会同时运行一行代码。如果程序每次运行结果和单线程运行的结果一样,且其他的变量的值也和预期一样,就是线程安全的,反之则是线程不安全的。
-
线程安全问题都是由全局变量及静态变量【共享】引起的。
- 如果每个线程中对全局变量、静态变量只有读操作,而无写操作,那么这个全局变量是线程安全的;
- 如果有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全问题。
-
如何解决线程安全问题?
- 线程同步
- volatile
- JUC
- 原子类(CAS)
- 锁(AQS)
1.3.2线程同步
同步代码块
synchronized(同步锁){
//需要同步操作的代码
}
同步方法
//同步方法
public synchronized void method(){
//可能会产生线程安全问题的代码
}
Lock锁
Lock lock=new ReentrantLock();//可重入锁
lock.lock();
//需要同步操作的代码
lock.unlock();
1.4 多线程并发的三个特性
1.4.1 并发编程中哪三个重要特性
-
原子性:即一个操作或多个操作,要么全部执行,要么就都不执。
-
有序性:程序代码按照先后顺序执行
- 为什么会出现无序问题呢?因为指令重排
- 指令重排是编译器和处理器为了提高程序运行效率,会对输入代码进行优化的一种手段。它不保证程序中,各个语句执行先后顺序的一致。
- 为什么会出现无序问题呢?因为指令重排
-
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 为什么出现不可见性问题呢?可以说是因为Java内存模型【JMM】
1.4.2 Java内存模型
-
诞生背景是因为CPU的缓存一致性、指令重排优化。
- Java为了保证并发编程中可以满足原子性、可见性及有序性,诞生出了一个重要的概念,那就是内存模型, 内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性,它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题。
-
内存模型如何解决并发问题?
- 1.限制处理器优化、2.使用内存屏障
-
JMM中定义一个共享变量何时写入,何时对另一个线程可见

- Java中关键字Synchronized、Volatile
-
JMM线程操作内存的基本规则
- 线程与主内存:线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读写
- 线程间本地内存:不同线程之间无法直接访问其他线程本地内存中的变量,线程间变量值的传递需要经过主内存
1.4.3 happens-before规则
- 在JMM中,使用happens-before规则来约束编译器的优化行为,允许编译期优化,但需要遵守一定的Happens-Before规则。
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before的关系。
- 需关注的happens-before规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- 需关注的happens-before规则:
2 synchronized
- synchronized可以保证并发程序的原子性,可见性,有序性。
- synchronized可以修饰方法和代码块。
- 方法:可修饰静态方法和非静态方法
- 代码块:同步代码块的锁对象可以为当前实例对象、字节码对象(class)、其他实例对象
2.1 synchronized解决内存可见性的原理
- 线程解锁前:必须把自己本地内存中共享变量的最新值刷新到主内存中
- 线程加锁时:将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
2.2 同步原理
- 同步操作主要是monitorenter和monitorexit两个指令实现,背后原理是Monitor(管程)
- 同步代码块
- 对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出
- 为什么会多一个monitorexit?
- 因为编译器会为同步块添加一个隐式的try-finally,在finally中会调用monitorexit命令释放锁
- 同步方法
- JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁,方法调用结束了释放锁。
- 底层依然是monitorenter和monitorexit指令
2.3 Monitor(管程)
- 管程(Monitor):是管理共享变量及对共享变量操作的过程,将共享变量和对共享变量的操作统一封装起来,让这个过程可以并发执行。
- 为什么所有对象都可以作为锁?
- 因为每个对象都都有一个Monitor对象与之关联。然后线程对monitor执 行lock和unlock操作,相当于对对象执行上锁和解锁操作。
- Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个指令来隐式地使用这两个操作:moniterenter和moniterexit。moniterenter 对应lock操作,moniterexit对应unlock操作, 通过这两个指令锁定和解锁 monitor 对象来实现同步。
- 当一个monitor对象被线程持有后,它将处于锁定状态。对于一个 monitor 而言,同时只能有一个线程能锁定monitor,其它线程试图获得已被锁定的 monitor时,都将被阻塞。当monitor被释放后,阻塞中的线程会尝试获得该 monitor锁。一个线程可以对一个 monitor 反复执行 lock 操作,对应的释放锁时,需要执行相同次数的 unlock 操作。
2.4 锁优化
- 什么是锁优化?
- 同步锁一共有四个状态:无锁,偏向锁,轻量级锁,重量级锁,JVM会视情况来逐渐升级锁,而不是上来就加重量级锁
- 偏向锁
- 只有一个线程访问锁资源(无竞争)的话,偏向锁就会把整个同步措施都消除。
- 轻量级锁
- 只有两个线程交替运行时,如果线程竞争锁失败了,先不立即挂起,而是让它飞一会儿(自旋),在等待过程中,可能锁就被释放了,这时线程重新尝试获取锁。
- 同步锁锁定的资源是对象,存储在对象头中。
| 偏向锁标记 | 锁状态标识 | 锁状态 |
|---|---|---|
| 0 | 01 | 无锁 |
| 1 | 01 | 偏向锁 |
| 无 | 00 | 轻量锁 |
| 无 | 10 | 重量锁 |
| 无 | 11 | GC标记 |
- 锁升级
- 锁可以升级但不能降级
- 偏向锁:是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争 时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
- 轻量级锁:(自旋锁)是指当锁是偏向锁的时候,却被另外的线程所访问 ,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程同样不会阻塞。长时间的自旋操作是非常消耗资源的, 一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)
- 重量级锁:此忙等是有限度的。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起 (而不是忙等),等待将来被唤醒。有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改
3 volatile
- volatile可以保证多线程场景下共享变量的可见性、有序性。
- 可见性:保证对此共享变量的修改,所有线程的可见性
- 有序性:禁止指令重排序的优化,遵循JMM的happens-before规则
3.1 volatile实现内存可见性的过程
// 添加volatile关键词
private volatile boolean flag=true;
- 线程写volatile变量的过程:
- 改变线程本地内存中volatile变量副本的值;
- 将改变后的副本的值从本地内存刷新到主内存
- 线程读volatile变量的过程:
- 从主内存中读取volatile变量的最新值到线程的本地内存中
- 从本地内存中读取volatile变量的副本
3.2 volatile实现原理
- 内存屏障(Memory Barrier)是CPU的一种指令,用于控制特定条件下的重排序和内存可见性问题。
- Java编译器会根据内存屏障的规则禁止重排序。
- 写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中
- 读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值
3.3 volatile缺陷
-
原子性问题
-
static class VolatileDemo implements Runnable { public volatile int count; public void run() { addCount(); } public void addCount() { for (int i = 0; i < 10000; i++) { count++;//实际是三条汇编指令 } } }
-
-
解决方法
- 使用synchronized
public synchronized void addCount()
- 使用ReentrantLock(可重入锁)
-
private Lock lock = new ReentrantLock(); public void addCount() { for (int i = 0; i < 10000; i++) { lock.lock(); count++; lock.unlock(); } }
-
- 使用AtomicInteger(原子操作)
-
public static AtomicInteger count=new AtomicInteger(0); public void addCount(){ for(int i=0;i< 10000;i++){ count.incrementAndGet(); } }
-
- 使用synchronized
3.4 volatile使用场景
- 变量真正独立于其他变量和自己以前的值,在单独使用的时适合用volatile
- 对变量的写入操作不依赖其当前值:例如++和–运算符的场景则不行
- 该变量没有包含在具有其他变量的不变式中
3.5 synchronized和volatile比较
- volatile不需要加锁,比synchronized更轻便,不会阻塞线程
- synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性
- 与synchronized相比,volatile是一种非常简单的同步机制
