须知少许凌云志,曾许人间第一流
看的是尚硅谷的视频做的学习总结,感恩老师,下面是视频的地址
传送门https://www.bilibili.com/video/BV1Kw411Z7dF
0.思维导图
1.JUC简介
1.1 什么是JUC
JUC, java.util.concurrent工具包的简称,一个处理线程的工具包。
1.2 进程和线程的概念
1.2.1 进程与线程
- 进程
指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位。
- 线程
系统分配处理器时间资源的基本单元;进程之内独立执行的一个单元执行流;线程是程序执行的最小单位。
一个进程包含多个线程。
1.2.2 线程的状态
查看jdk源码
1.2.3 wait 和 sleep
区别
- sleep是Thread的静态方法;wait是Object的方法,任何对象实例都能调用。
- sleep不会释放锁,它也不需要占用锁;wait会释放锁。(wait会释放锁去睡觉,sleep会抓住锁去睡觉,在哪里谁就会在哪里醒)
- 它们都可以被interrupt方法中断。
1.2.4 并发和并行
- 并发
同一时间间隔内多个线程交替执行,实际上是宏观上并行,微观上串行。(春运抢票,电商秒杀,抢同一个资源)
- 并行
同一时刻多个线程正在执行,多核并行。(一边看书,一边听音乐)
1.2.5 管程
叫 Monitor 监视器,就是锁。
是一种同步机制,保证同一个时间,只有一个线程访问被保护的数据或者代码。
1.2.6 用户线程和守护线程
- 用户线程
自定义的线程,不随主线程的结束而结束。主线程结束了,用户线程还会运行,jvm还是存活状态。
- 守护线程
随着主线程的结束而结束,如垃圾回收线程。主线程结束,jvm结束。
2.Lock接口
2.1 Synchronized
2.1.1 Synchronized作用范围
synchronized是Java的关键字,是一种同步锁。
synchronized
的作用范围可以根据使用方式的不同而有所区别,主要有以下几种情况:
- 同步方法(实例方法)
public class SynchronizedExample {
public synchronized void syncMethod() {
// 同步代码块
}
}
当synchronized
修饰一个实例方法时,它作用于整个方法体。当一个线程进入一个对象的同步方法时,其他线程在该对象上调用同步方法时会被阻塞,直到第一个线程退出该方法。这种同步方式是基于对象的,也就是说,不同的对象实例的同步方法是互不干扰的。
- 同步静态方法
public class SynchronizedExample {
public static synchronized void syncStaticMethod() {
// 同步代码块
}
}
当synchronized
修饰一个静态方法时,它作用于整个静态方法体。由于静态方法是属于类的,而不是类的实例,因此这种同步是基于类的。当一个线程进入一个类的同步静态方法时,其他线程在该类上调用同步静态方法时会被阻塞。
- 同步代码块
public class SynchronizedExample {
private final Object lock = new Object();
public void someMethod() {
synchronized (lock) {
// 同步代码块
}
}
}
synchronized
也可以用来修饰一个代码块,此时需要指定一个对象作为锁对象。当线程进入同步代码块时,它会获取指定对象的锁,如果其他线程已经持有该对象的锁,则进入阻塞状态。这种同步方式允许更细粒度的控制,只同步需要同步的代码部分。
2.1.2 多线程编程步骤
第一步:创建资源类,在资源类创建属性和操作方法。
第二步:创建多个线程,去调用资源类的操作方法。
2.1.3 Synchronized实现买票示例
需求:3个售票员卖一百张门票。
分析:资源是一百张门票,操作方法是买票,创建多个线程是3个售货员。
代码示例
// 第一步:定义资源类
class Ticket {
// 第二步:定义资源
public int number = 100;
// 第三步:定义操作方法
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - number + 1) + "张票,剩余" + --number + "张票");
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 使用匿名内部类创建线程
Runnable runnable = () -> {
for (int i = 0; i < 150; i++) {
ticket.sale();
}
};
// 创建多个线程进行卖票
new Thread(runnable,"售票员1").start();
new Thread(runnable,"售票员2").start();
new Thread(runnable,"售票员3").start();
}
}
输出结果
2.2 Lock
2.2.1 Lock接口的介绍
Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。
2.2.2 使用Lock实现卖票例子
// 第一步:定义资源类
class Ticket {
// 第二步:定义资源
public int number = 100;
// 创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
// 第三步:定义操作方法
public synchronized void sale() {
// 手动上锁
lock.lock();
try{
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - number + 1) + "张票,剩余" + --number + "张票");
}
}finally {
// 手动解锁
lock.unlock();
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 使用匿名内部类创建线程
Runnable runnable = () -> {
for (int i = 0; i < 150; i++) {
ticket.sale();
}
};
// 创建多个线程进行卖票
new Thread(runnable,"售票员1").start();
new Thread(runnable,"售票员2").start();
new Thread(runnable,"售票员3").start();
}
}
2.2.3 synchronized和Lock两者差异
- synchronized是java内置关键字。Lock不是内置,可以实现同步访问且比 synchronized中的方法更加丰富。
- synchronized自动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)。
- Lock 可以让等待锁的线程响应中断,而等待synchronized锁的线程不能响应中断,会一直等待。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候,Lock 性能远远好于synchronized)。
2.2.4 创建线程的四种方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable接口
- 使用线程池
3.线程间通信
3.1 多线程编程步骤
第一步:创建资源类,在资源类创建属性和操作方法。
第二步:在资源类操作方法,判断、干活、通知。
第三步:创建多个线程,去调用资源类的操作方法。
第四步:防止虚假唤醒。
3.2 synchronized 实现线程通信案例
关键字 synchronized
与 wait()/notify()
这两个方法一起使用可以实现等待/通知模式。
代码示例
// 第一步:创建资源类,定义属性和操作方法
class Share {
private int number = 0;
// +1的方法
public synchronized void incr() throws InterruptedException {
// 第二步:判断
if (number != 0) {
this.wait();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName() + ":" + number);
// 通知其他线程
this.notifyAll();
}
// -1的方法
public synchronized void decr() throws InterruptedException {
// 判断
if (number != 1) {
this.wait();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + ":" + number);
// 通知其他线程
this.notifyAll();
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 第三步:创建多个线程,调用资源类中的操作方法
Share share = new Share();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "生产").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "消费").start();
}
}
3.3 虚假唤醒问题(if改while)
当多个线程都处于等待集合中,一旦收到通知,可以直接操作而不再判断,这叫做虚假唤醒问题。 将this.wait()
放在while循环中可以解决该问题。
代码示例
class Share {
int number = 0;
public synchronized void incr() throws InterruptedException {
//判断
if (number != 0) {
this.wait();//这里会释放锁
}
//执行
number++;
System.out.print(Thread.currentThread().getName() + " : " + number + "-->");
// 通知
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
if (number != 1) {
this.wait();
}
//执行
number--;
System.out.println(Thread.currentThread().getName() + " : " + number);
//通知
this.notifyAll();
}
}
public class ThreadDemo {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "C").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "D").start();
}
}
输出结果
原因
由于 wait()
方法使线程在哪里睡就在哪里醒,B和D在wait后被唤醒,执行操作时不会再通过 if 判断,从而导致出现异常结果。
为了保证线程“醒了”之后再次判断,需要将wait()
方法放入while
循环中。
class Share {
int number = 0;
public synchronized void incr() throws InterruptedException {
//判断
while (number != 0) {
this.wait();
}
//执行
number++;
System.out.print(Thread.currentThread().getName() + " : " + number + "-->");
// 通知
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
while (number != 1) {
this.wait();
}
//执行
number--;
System.out.println(Thread.currentThread().getName() + " : " + number);
//通知
this.notifyAll();
}
}
3.4 Lock实现线程间通信案例
在 Lock 接口中,有一个 newCondition() 方法,返回一个新 Condition 绑定到该实例 Lock 实例。
Condition 类中有 await() 和 signalAll() 等方法,和 synchronized 实现案例中的 wait() 和 notifyAll() 方法相同。
代码示例
class Share {
private int number = 0;
//创建Lock
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.print(Thread.currentThread().getName() + " : " + number + "-->");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
//判断
while (number != 1) {
condition.await();
}
//执行
number--;
System.out.println(Thread.currentThread().getName() + " : " + number);
//通知
condition.signalAll();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "C").start();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "D").start();
}
}
4.线程间定制化通信
4.1 Condition 类选择性通知
案例: 启动三个线程,按照如下要求执行,AA打印5此,BB打印10次,CC打印15次,一共进行10轮
具体思路: 每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程。
代码示例
class ShareResource {
// 标志位 1:AA 2:BB 3:CC
private int flag = 1;
private ReentrantLock lock = new ReentrantLock();
// 创建三个Condition对象,实现定向唤醒
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
public void print5(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 1) {
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);
}
flag = 2;
c2.signal();
} finally {
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 2) {
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);
}
flag = 3;
c3.signal();
} finally {
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 3) {
c3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);
}
flag = 1;
c1.signal();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareResource.print5(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareResource.print10(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareResource.print15(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "CC").start();
}
}
上面的代码采用单标志法,设置一个公用整型变量flag,用于指示被允许进入临界区的进程编号。
若 flag =1,则允许 AA 进程进入临界区;
若 flag =2,则允许 BB 进程进入临界区;
若 flag =3,则允许 CC 进程进入临界区。
该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区。在线程的run()方法调用中设置不同的loop次数,在后期会有部分线程不能访问 Share 资源了,违背了"空闲让进"原则,让资源利用不充分。
4.2 进程/线程同步四个原则
- 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待。
- 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区。
- 让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待。
5.集合的线程安全
集合线程不安全,简单来说就是底层的方法没有使用同步安全锁。
5.1 ArrayList 不安全
jdk源码
没有使用synchronized
关键字,所以在多线程并发时,会出现线程安全问题。
代码示例
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
运行报错
5.2 解决方案 Vector
jdk源码
Vector类中的方法加了synchronized
关键字,因此可以保证线程安全。
代码改造
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = new Vector<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
运行结果
5.3 解决方案 synchronizedList(List list)
Collections 接口中的 synchronizedList(List list)
方法,可以将传入的 List列表对象转为同步(线程安全的)列表并返回。
语法
List<String> list = Collections.synchronizedList(new ArrayList<>());
jdk源码
代码示例
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
5.4 解决方案 CopyOnWriteArrayList
语法
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList 采用读写分离的思想,读操作不加锁,写操作加锁。(redis也使用)
- 读的时候并发读取旧数据(多个线程操作)
- 写的时候独立,先复制一份比旧数据长 1 的数据出来,在最后添加数据,旧新合并,完成写操作,之后就可以读所有数据(每次加新内容都写到新区域,合并之前旧区域,读取新区域添加的内容)