- 1 JUC概述
- JUC概念
- 进程、线程概念
- 1.1 进程 & 线程
- 1.2 线程状态
- 1.3 sleep & wait
- 1.4 并发 & 并行
- 1.5 管程
- 1.6 用户线程 & 守护线程
- 2 Lock接口
- 2.1 synchronized、Lock 对比、介绍
- 2.2 eg.卖票
- 3 线程间通信
- 4 线程间定制化通信
- 5 集合的线程安全
- 5.1 ArrayList线程不安全
- 5.2 HashSet线程不安全
- 5.3 HashMap线程不安全
- 6 多线程锁
- 6.0 锁的范围
- 6.1 非公平锁 & 公平锁
- 6.2 可重入锁
- 6.3 死锁
- 7 Callable接口
处理线程的工具包 java.util .concurrent 的简称
Java8API地址
| 进程 | 线程 |
|---|---|
| 系统中正在运行的一个应用程序 / 运行的程序 | 系统分配处理器时间资源的基本单元 / 进程内独立执行的一个单元执行流 |
| 资源分配和调度的最小单位 | 程序执行的最小单位 |
| 一个进程可并发多个线程 | 每条线程并行执行不同的任务 |
Thread.State 线程状态枚举类
线程状态转换图
参考
| 异 | sleep | wait |
|---|---|---|
| 所属类 | Thread静态方法 | Object方法 任何实例对象都可调用 |
| 时间 | 指定时间 | 可指定(限时等待)可不指定(无限等待) |
| 释放锁 | 释放CPU执行权 不释放同步锁 | 释放CPU执行权、同步锁 |
| 使用的地方 | 任何地方都能使用 | 只能在同步代码方法/块中使用(调用前提:当前线程占有锁->代码要在 synchronized 中) |
| 捕获异常 | 必须捕获异常 | 捕获/抛出异常 |
同:可被 interrupted 方法中断;在哪里睡,就在哪里醒
1.4 并发 & 并行串行:所有任务按先后顺序执行
并行:同一时刻多个线程在访问同一个资源
并发(concurrent):多项工作一起执行,之后再汇总
管程-Monitor监视器(OS) 锁(Java):是一种同步机制,保证同一时间,只有一个线程访问被保护数据/代码
JVM同步基于进入(加锁)和退出(解锁),使用管程对象实现(对临界区加锁和解锁)
用户线程:自定义线程
守护线程:如垃圾回收(后台执行)
| 异 | synchronized | Lock |
|---|---|---|
| 存在层次 | Java关键字 内置特性 托管给ivm执行 | 接口 非Java内置 是Java写的控制锁的代码 |
| 释放锁 | 异常->自动unlock(JVM会让线程释放锁) | 必须在finally中手动unlock,否则死锁 |
| 获取锁 | 不可响应中断,会一直等待锁释放 | 等锁线程可响应中断(interrupt) |
| 锁状态 | 无法判断 | 可用trylock()判断是否成功获取锁 |
| 锁类型 | 可重入 非公平 | 可重入 非公平(默认)/公平(传参true) |
| 性能 | 竞争资源不激烈,二者差不多(适合少量同步) | 竞争资源激烈,性能优(提高多线程读效率) |
| 优点 | 使用简单(JDK1.6后优化:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁) | 灵活 |
| 底层 | CPU悲观锁机制-线程获得的是独占锁 | 乐观锁 -CAS(Compare and Swap)实现 |
| 调度机制 | Object wait() notifyAll() notify()-this.notify(),JVM随机唤醒某个等待的线程 | Condition await() signalAll() signal()-ci.signal(),选择性通知ci |
synchronized隐式(内置)锁 & Lock显式锁
- synchronized好用,简单,性能不差
- 没有使用到Lock显式锁的特性就不要使用Lock锁了
Lock.java
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock同步形式
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock();
// block until condition holds
try {
// ... method body
} finally {
lock.unlock(); //Lock发生异常时,若不主动释放锁,会死锁->finally保证不管有无异常都会释放锁
}
}
}
2.2 eg.卖票
⭐多线程编程步骤(高内聚 低耦合)
- 创建资源类 定义属性、方法
- 创建多个线程 调用资源类方法
synchronized实现卖票
//1.创建资源类 定义属性、方法
class Ticket {
//票数
private int number=30;
//卖票
public synchronized void sale() { //synchronized自动上锁 去掉关键字 输出全是A卖出的
if(number>0) {
System.out.println(Thread.currentThread().getName()+"卖出"+number--+"号票");
}
}
}
public class Test1_SaleTicket {
//2.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Ticket ticket=new Ticket();
//创建3个线程
new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
//调用资源类方法
for(int i=0;i<40;i++) {
ticket.sale();
}
}
}, "A").start();
//同理创建线程B、C
}
}
Lock实现卖票
import java.util.concurrent.locks.ReentrantLock;
//1.创建资源类 定义属性、方法
class Ticket {
//票数
private int number=30;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
//卖票
public void sale() { //用ReentrantLock手动上锁
//加锁
lock.lock();
try{
if(number>0) {
System.out.println(Thread.currentThread().getName()+"卖出"+number--+"号票");
}
} finally {
//解锁
lock.unlock();
}
}
}
public class Test1_LSaleTicket {
//2.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Ticket ticket = new Ticket();
//创建3个线程
new Thread(() -> { //Lambda表达式
//调用资源类方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "A").start(); //线程调用start后可能马上创建(OS空闲),也可能等会儿创建(OS忙) 取决于OS
//同理创建线程B、C
}
}
线程执行顺序不固定的原因
3 线程间通信
线程间通信模型:共享内存、消息传递
⭐多线程编程步骤
- 创建资源类 定义属性、方法
- 在资源类操作方法(判断 干活 通知)
- 创建多个线程 调用资源类方法
- 防止虚假唤醒问题(判断条件要加到while中)
实现线程间交替+1-1操作
- 用synchronized关键字实现
this.wait()/notifyAll();
//1.创建资源类 定义属性、方法
class Share {
private int i=0;
//2.在资源类操作方法(在方法中 判断 干活 通知)
public synchronized void incr() throws InterruptedException {
//判断
while(i!=0) {
this.wait(); //不满足干活条件->在被通知前<等待> 释放锁
}
//干活
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
//通知其它线程
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
while(i!=1) {
this.wait(); //if的wait:在哪里睡,在哪里醒 被调用时唤醒->虚假唤醒 解决:条件放到while中
}
//干活
i--;
System.out.println(Thread.currentThread().getName()+":"+i);
//通知
this.notifyAll();
}
}
public class Test2_ThreadSignal {
//3.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Share share=new Share();
//创建2个线程 交替实现+1-1
//若是多个线程,等待条件放在if中:+的锁释放后又被+的线程抢到了,会+到>1 -的操作也会-到<0
new Thread(()->{
for(int i=0;i<10;i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//同理创建线程B、C、D
}
}
- 用Lock接口实现
new ReentrantLock().newCondition().await()/signalAll();
//1.创建资源类 定义属性、方法
class Share {
private int i=0;
//创建lock
private Lock lock=new ReentrantLock();
private Condition condition=lock.newCondition();
//2.在资源类操作方法
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(i!=0) {
condition.await();
}
//干活
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
//通知
condition.signalAll();
} finally {
//解锁
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
while(i!=1) {
condition.await();
}
i--;
System.out.println(Thread.currentThread().getName()+":"+i);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
public class Test2_LThreadSignal {
//3.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Share share=new Share();
//创建多个线程
new Thread(()->{
for(int i=0;i<10;i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//同理创建线程B-decr、C-incr、D-decr
}
}
线程虚假唤醒
- 问题
- 原因
- 解决
- 分析
理解虚假唤醒问题:坐飞机(调用线程)要进行安检(判断条件),下飞机(线程睡眠)再上飞机(唤醒线程)还要进行安检(while醒来条件不符合继续睡),否则只安检一次(if)的话可能会有可疑物品带上飞机(程序会往下执行)
4 线程间定制化通信
- 用 notify()通知时,JVM会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知
- 在调用 Condition 的 await()/signal()方法前,也需要线程持有相关锁,调用 await()后线程会释放这个锁,在 singal() 调用后会从当前 Condition 对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,一旦成功获得锁就继续执行
A 线程打印5次A,B线程打印10次B,C线程打印15次C,按照
此顺序循环10轮->用Lock Condition的 signal() 实现
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//用 Lock接口 实现 线程间定制化通信
//1.创建资源类 定义属性、方法
class ShareResource {
//定义标志位 A-1 B-2 C-3
private int flag=1;
//创建lock
private Lock lock=new ReentrantLock();
private Condition c1=lock.newCondition();
private Condition c2=lock.newCondition();
private Condition c3=lock.newCondition();
//2.在资源类操作方法
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag!=1) {
c1.await();
}
//干活
for(int i=1;i<=5;i++) {
System.out.println("第"+loop+"轮 "+Thread.currentThread().getName()+":"+i);
}
flag=2;
//通知B
c2.signal();
} finally {
//解锁
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag!=2) {
c2.await();
}
//干活
for(int i=1;i<=10;i++) {
System.out.println("第"+loop+"轮 "+Thread.currentThread().getName()+":"+i);
}
flag=3;
//通知C
c3.signal();
} finally {
//解锁
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag!=3) {
c3.await();
}
//干活
for(int i=1;i<=15;i++) {
System.out.println("第"+loop+"轮 "+Thread.currentThread().getName()+":"+i);
}
flag=1;
//通知A
c1.signal();
} finally {
//解锁
lock.unlock();
}
}
}
public class Test2_CustomizedThreadSignal {
//3.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
ShareResource resource=new ShareResource();
//创建多个线程
new Thread(()->{
for(int i=1;i<=10;i++) {
try {
resource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=1;i<=10;i++) {
try {
resource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for(int i=1;i<=10;i++) {
try {
resource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
}
}
5 集合的线程安全
演示
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
//List HashSet HashMap 线程不安全
public class Test3_CollectionUnsafe {
public static void main(String[] args) {
//ArrayList线程不安全
// List list=new ArrayList<>();
//Vector解决
// List list=new Vector<>();
//Collections解决
// List list= Collections.synchronizedList(new ArrayList<>());
//CopyOnWriteArrayList解决
List list=new CopyOnWriteArrayList<>();
//HashSet线程不安全
// Set set=new HashSet<>();
//CopyOnWriteArraySet解决
Set set=new CopyOnWriteArraySet<>();
//HashMap线程不安全
// Map map=new HashMap<>();
//Hashtable解决
// Map map=new Hashtable<>();
//ConcurrentHashMap解决
Map map=new ConcurrentHashMap<>();
for(int i=0;i<30;i++) {
String key=String.valueOf(i);
new Thread(()->{
//向集合添加内容
// list.add(UUID.randomUUID().toString().substring(0,8));
// set.add(UUID.randomUUID().toString().substring(0,8));
map.put(key,UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
// System.out.println(list);
// System.out.println(set);
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
5.1 ArrayList线程不安全
List<…> list=new ArrayList<>();
解决:
- Vector
List<…> list=new Vector<>();
- Collections集合工具类
List<…> list= Collections.synchronizedList(new ArrayList<>()); - CopyOnWriteArrayList
List<…> list=new CopyOnWriteArrayList<>();
读时共享,写时复刻
思想:拷贝一份
- 独占锁效率低:采用读写分离思想解决
- 写线程获取到锁,其他写线程阻塞
- 复制思想
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据
5.2 HashSet线程不安全CopyOnWriteArrayList原理分析
- 动态数组 机制
- 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyonWriteArrayList 的原因
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyonWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高
- 线程安全 机制
- 通过 volatile 和 互斥锁 来实现
- 通过 volatile数组 来保存数据. 一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证
- 通过 互斥锁 来保护数据. 在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的
Set<…> set=new HashSet<>();
异常报错是HashMap,因为HashSet底层基于HashMap实现:
Set元素唯一、无序的原因:
解决:CopyOnWriteArraySet
Set<…> set=new CopyOnWriteArraySet<>();
->
Map
HashMap可存储null的key和value,null作为键只能有一个,null作为值可有多个
解决:
- HashTable
Mapmap=new Hashtable<>();
HashTable使用synchronized来保证线程安全,在线程竞争激烈的情况下效率非常低下,所以HashTable基本被淘汰,不要在代码中使用它,要保证线程安全的话就使用ConcurrentHashMap
- ConcurrentHashMap
Mapmap=new ConcurrentHashMap<>();
ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步
key和Value都不能为null,否则抛出 NullPointerException 异常(图片1011行)
ConcurrentHashMap原理 jdk7和8版本的区别
ConcurrentHashMap基于JDK1.8源码剖析
ConcurrentHashMap如何保证线程安全
锁的8种情况 (锁的范围-是否是同一把锁)
import java.util.concurrent.TimeUnit;
class Phone {
public static synchronized void sendSMS() throws Exception {
// public synchronized void sendSMS() throws Exception {
//停留 4 秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
// public static synchronized void sendEmail() throws Exception {
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
public class Test4_Lock8 {
public static void main(String[] args) throws Exception {
Phone phone=new Phone();
Phone phone1=new Phone();
new Thread(()->{
try{
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100); //线程什么时候创建不确定,所以在两线程中间睡眠一下,使效果更明显
new Thread(()->{
try{
// phone.getHello();
// phone.sendEmail();
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
结论
- 一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,即某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法. 因为锁的是当前对象 this(同一把锁),被锁定后,其它的线程都不能进入到当前对象的其它的synchronized 方法
- 加个普通方法后发现和同步锁无关
- 换成两个对象后,不是同一把锁了,情况立刻变化
- 所有静态同步方法用的是同一把锁——类对象本身,和任何实例对象的普通同步方法用的锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的
⭐synchronized作用范围(3种同步方法)
| 作用范围 | 锁的对象 |
|---|---|
| 普通方法 | 当前实例对象(对象锁) |
| 静态方法 | 当前类的 Class 对象(类锁) |
| 代码块 | synchonized 括号里配置的对象(对象锁) |
synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁
- 修饰普通方法
public class X {
//锁的是 this (同步方法使用的同步对象为该方法所属类本身的实例对象)
public synchronized void test() {
//code
}
}
- 修饰静态方法
public class X {
//锁的是 X.clss (类的字节码文件)
public static synchronized void test() {
//code
}
}
- 修饰代码块
public class X {
public void test() {
//锁的是 obj (可以是任意对象,但必须为同一对象)
synchronized (obj){
//同步代码块
}
}
}
6.1 非公平锁 & 公平锁
非公平锁
效率高 会导致线程饿死
卖票例子
ReentraintLock()无参构造默认用非公平锁
只有线程A占着锁,其它线程饿死
公平锁
效率相对低 资源对线程-雨露均沾 公平锁是为了让CPU发挥多线程的性能
卖票例子,ReentraintLock()有参构造传参
每个线程都能获得锁
6.2 可重入锁
synchronized隐式(内置)锁、Lock显式锁 都是可重入锁(递归锁)
可重入锁在破解第一把锁之后可一直进入到内层结构
演示可重入锁
public class Test5_ReentraintLock {
//2.同步方法演示可重入锁
public synchronized void add() {
add();//因为是可重入锁,所以会递归调用add();若是不可重入锁. 就会等待this对象释放锁,这段代码会死锁
}
public static void main(String[] args) {
new Test5_ReentraintLock().add(); //循环递归调用->最后栈溢出
//1.同步代码块演示可重入锁
Object o=new Object();
new Thread(()->{
synchronized (o) {
System.out.println(Thread.currentThread().getName()+":外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+":中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+":内层");
}
}
}
},"A").start();
//3.Lock演示可重入锁
Lock lock=new ReentrantLock();
new Thread(()->{
try {
lock.lock(); //上锁
System.out.println(Thread.currentThread().getName()+":外层");
try {
lock.lock(); //上锁
System.out.println(Thread.currentThread().getName()+":内层");
} finally {
lock.unlock(); //解锁
}
} finally {
lock.unlock(); //解锁
}
},"B").start();
new Thread(()->{
lock.lock(); //若是另一个线程不解锁 这个线程就得不到锁 一直等待不能执行
System.out.println(Thread.currentThread().getName());
lock.unlock();
},"C").start();
}
}
- 注释掉线程B的一个unlock:
6.3 死锁
deadlock 多个进程互不相让,都得不到足够的资源(永久性阻塞)
演示死锁(记住 面试手撕代码)
import java.util.concurrent.TimeUnit;
public class Test6_DeadLock {
static Object a=new Object();
static Object b=new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"持有锁a,试图获取锁b");
//睡眠1s让线程创建确定,使死锁效果更明显
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"获取锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"获取锁a");
}
}
},"B").start();
}
}
死锁验证方式
- jps [= Lunix: ps -ef 查询当前正在运行的进程;jdk提供的查看当前java进程的小工具,可看做 JavaVirtual Machine Process Status Tool 的缩写]
- jstack [JVM自带堆栈跟踪工具]
PATH配置了JAVA_HOME就可以在IDEA终端中使用命令



