JUC概述
1.1 JUC简介
- java.util.concurrent包名的简写,是关于并发编程的API
- 与JUC相关的有三个包:java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks
1.2 进程和线程
- 进程:系统中一个正在运行的应用程序;一个程序就是一个进程;进程操作系统资源分配的最小单位
- 线程:系统分配处理器时间资源的基本单位;进程之内独立执行的一个单元执行流。线程是程序执行的最小单位
1.3 线程的状态
1.3.1 线程状态枚举
1.3.2 wait、sleep区别
- sleep是Thread的静态方法。wait是Object的方法,任何对象实例都能调用。
- sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
- 都可以呗interrupted方法中断。
1.4 并发和并行
1.4.1 串行模式
- 所有任务都按先后顺序执行。一次只能取得一个任务,执行完成这个任务才能执行下一个任务。
1.4.2 并行模式
- 多个任务同时进行。可以同时取得多个任务,并同时去执行这些任务。
1.4.3 并发
- 并发:同一时刻多个线程在访问同一个资源,多个线程对一个点。例如:春运抢票 电商秒杀
- 并行:多项工作一起执行,之后再汇总。例如:泡方便面,电水壶烧水,一边撕调料倒入桶中
1.4.5 小结(重点)
1.5 管程
- 管理共享变量以及对其操作过程,让它们支持并发访问。也就是管理类的成员变量和成员方法,让这个类是线程安全的。
1.6 用户线程和守护线程
- 用户线程(自定义线程):主线程结束了,用户线程还在执行,JVM存活,可以通过线程的setDaemon(true)方法设置线程为守护线程
- 守护线程(垃圾回收):没有用户线程了,都是守护线程,JVM结束
2. Lock接口
2.1 Synchronized关键字
2.1.1 synchronized概述
synchronized{
...
}
是java中的关键字,是一种同步锁
- 修饰代码块,被修饰的代码块为同步代码块,作用范围为{}内,作用对象是调用这个代码块的对象
- 修饰方法,被修饰的方法为同步方法,作用范围为整个方法,作用对象是调用这个方法的对象
- 虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,故synchronized关键字不能被继承。如果在父类的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,那么子类中的这个方法默认情况下是不同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。
- 当然,还可以在子类中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类同步方法,因此,子类到方法也相当于同步了
- 修饰静态方法,作用范围是整个静态方法,作用对象是这个类的所有对象
- 修饰类,作用范围是synchronized后面括号起来的部分,作用的对象是这个类的所有对象
2.1.2 模拟卖票
class Ticket {
// 票数
private int number = 100;
// 卖票
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:" + (number--) + " 剩下:" + number);
}
}
}
public class TicketMain {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i<100; i++){
ticket.sale();
}
}
}, "AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i<100; i++){
ticket.sale();
}
}
}, "BB").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i<100; i++){
ticket.sale();
}
}
}, "CC").start();
}
}
2.1.3 多线程编程步骤(上)
- 创建资源类,创建属性和操作方法
- 创建多线程调用资源类的方法
2.2 Lock接口
- 属于java.util.concurrent.locks包下,为锁和等待条件提供一个框架的接口和类,它不同与内置同步和监视器
- Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作
2.2.1 synchronized和Lock的区别
- synchronized是Java的关键字。在JVM层面上,而Lock是一个类
- synchronized发生异常会自动释放锁,因此不会导致死锁的现象发生。Lock发生异常需要主动释放锁,否则会发生死锁。
- synchronized不能够响应中断,等待的线程会一直等待。Lock可以让等待锁的线程响应中断。
- synchronized不能获取锁的状态。Lock可以知道是否成功获取锁。
- Lock可以提高多个线程进行读操作的效率。
竞争资源不激烈两者性能相当,而竞争资源激烈时(大量线程同时竞争),Lock性能远远优于synchronized。
Lock锁模拟卖票
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Ticket {
// 票数
private int number = 100;
Lock lock = new ReentrantLock();
// 卖票
public synchronized void sale() {
try{
// 获取锁
lock.lock();
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:" + (number--) + " 剩下:" + number);
}
}finally {
// 释放锁
lock.unlock();
}
}
}
public class LTicketMain {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for(int i = 0; i<100; i++){
ticket.sale();
}
}, "AA").start();
new Thread(()->{
for(int i = 0; i<100; i++){
ticket.sale();
}
}, "BB").start();
new Thread(()->{
for(int i = 0; i<100; i++){
ticket.sale();
}
}, "CC").start();
}
}
细节
- 线程调用start()方法不一定会马上创建,具体什么时候创建由操作系统决定。操作系统空闲时会立马创建线程,不空闲会等一会创建线程。
2.3 创建线程的四种方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable接口
- 是一个线程池
3. 线程间通信
初始化变量i=0,i=0时线程A对i+1,i !=0,线程B对i-1
synchronized
public class Share {
private int i = 0;
// 加一
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 == 0){
this.wait();
}
i--;
System.out.println(Thread.currentThread().getName() + "::"+ i);
this.notifyAll();
}
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();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
// 增加多一个用来测试虚假唤醒
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
Lock
public class Share {
private int i = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 加一
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 == 0) {
condition.await();
}
i--;
System.out.println(Thread.currentThread().getName() + "::" + i);
condition.signalAll();
}finally {
lock.unlock();
}
}
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();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
// 增加案例测试虚假唤醒
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
注意: 当while条件换成if条件会造成虚假唤醒的情况,i会出现负数
虚假唤醒导致if在多线程环境下出错,因为它不再判断条件是否满足,继续从wait()方法之后执行。而while还会再次判断条件是否满足,如果不满足就不会执行。
4. 线程间定制化通信
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
让ABC线程按照指定顺序执行
public class ShareResource {
private int flag = 1;
private final Lock lock = new ReentrantLock();
private final Condition c1 = lock.newCondition();
private final Condition c2 = lock.newCondition();
private final Condition c3 = lock.newCondition();
// 打印5次
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(Thread.currentThread().getName() + "::" + i + " 轮数:" + loop);
}
flag = 2;
// 通知B线程
c2.signal();
} finally {
lock.unlock();
}
}
// 打印10次
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(Thread.currentThread().getName() + "::" + i + " 轮数:" + loop);
}
flag = 3;
// 通知C线程
c3.signal();
} finally {
lock.unlock();
}
}
// 打印15次
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(Thread.currentThread().getName() + "::" + i + " 轮数:" + loop);
}
flag = 1;
// 通知A线程
c1.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
// 创建三个线程分别调用每个方法
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
5. 集合的线程安全
5.1 集合线程不安全演示
多个线程同时向list插入元素会出现线程不安全问题,因为ArrayList<>()是线程不安全的
public class ListDemo {
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 ArrayList解决方案
Vector
List<String> list= new Vector<>();
- 在方法上加上了synchronized关键字实现线程安全,但是效率低下
Collections
List<String> list= Collections.synchronizedList(new ArrayList<>());
Collections.synchronizedList(List list) 返回的是一个 SynchronizedList 的对象,SynchronizedList 的实现里,get, set, add 等操作都加了 mutex(互斥) 对象锁,再将操作委托给最初传入的 list。这个对象以组合的方式将对 List 的接口方法操作,委托给传入的 list 对象,并且对所有的接口方法对象加锁,得到并发安全性。
这就是以组合的方式,将非线程安全的对象,封装成线程安全对象,而实际的操作都是在原非线程安全对象上进行,只是在操作前给加了同步锁。
CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
底层原理
- 并发读,独立写:读的时候是读原数组,写的时候会copy新的数组,在新的数组上进行操作,然后和原数组合并,再次读会读到合并后的数组。
5.3 HashSet线程不安全
HashMap底层就是HashMap
- 使用CopyOnWriteArraySet
Set<String> set = new CopyOnWriteArraySet<>();
5.4 HashMap线程不安全问题
- ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
6. 多线程锁
6.1 锁的八种情况
synchronized实现同步的基础:Java中每一个对象都可以作为锁。
具体表现为三种形式:
- 对于普通同步方法,锁谁当前实例对象(this)
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是synchronized括号里配置的对象
6.2 公平锁和非公平锁
公平和非公平相对于线程资源竞争来说,非公平则一个资源独占锁直到任务结束,其他线程会处于饿死的状态。
非公平锁:其他线程会饿死,但效率高
公平锁:线程平等,效率相对低
private Lock lock1 = new ReentrantLock(false); // 非公平锁 (默认为false)
private Lock lock2 = new ReentrantLock(true); // 公平锁
6.3 可重入锁
某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
synchronized是隐式 Lock是显式的可重入锁
注意:Lock执行了多少次上锁,就必须解锁多少次,否则导致其他线程无法获取锁。
6.4 死锁
两个或两个以上的进程在执行过程中,因为争夺资源造成一种互相等待的现象。如果没有外力干涉,则程序无法执行下去。
死锁产生的原因
- 系统功能资源不足
- 进程运行推进顺序不合适
- 资源分配不当
验证是否是死锁
- jps 类似linux ps -ef
- jstack jvm自带堆栈跟踪工具
7. Callable接口
Callable与Runnable的区别:
- Callable通过call方法获取具体的返回值,这个返回值具体是通过实现Future接口对象的get方法获取的,该方法会造成现场阻塞。Runnable的run方法没有返回值
- Callable的call方法可以抛出异常,可以通过捕获异常处理。Runnable的run方法不可以抛出异常,异常要在run方法内部必须得到处理,不能向外界抛出。
Callable使用
- FutureTask类实现了RunnableFuture接口,而RunnableFuture接口实现了Runnable和Future接口
方法1: 创建一个FutureTask对象,传入Callable接口的实现类,并实现Callable接口的call方法,通过FutureTask的get方法可以获取到任务的返回值
public class CallableTest {
public static void main(String[] args) {
// 方式1
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask, "A").start();
System.out.println(futureTask.get()); // 1
// 方式2 匿名函数
FutureTask<Integer> futureTask2 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName());
return 200;
});
new Thread(futureTask2, "B").start();
System.out.println(futureTask2.get()); // 200
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 1;
}
}
FutureTask支持Callable的匿名函数源码
8. JUC强大的辅助类
8.1 减少计数CountDownLatch
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
- 其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
模拟全部同学离开教室后,最后班长才可以锁门
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
// 模拟同学陆续离开教室
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "同学离开教室");
// 计数器减一
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("main班长锁门");
}
8.2 循环栅栏CyclicBarrier
CyclicBarrier是循环阻塞的意思,在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数就加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加一的操作。
集齐七龙珠后才可以召唤神龙
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("七龙珠集齐了,开始召唤神龙");
});
// 模拟同学陆续离开教室
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "星龙珠被收集到了");
// 等待
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
8.3 信号灯Semaphore
一个计数信号量。可以通过Semaphore构造方法设置许可数量,调用acquire()方法可以获得许可,release()方法释放许可。当没有获取许可时,其他线程处于阻塞或等待状态,只有当获取许可后才能继续执行操作。
6辆汽车,停到3个车位
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
// 模拟同学陆续离开教室
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
// 抢占
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "车停到了车位!");
// 随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(3));
System.out.println(Thread.currentThread().getName() + "车离开了车位-------");
} catch (Exception e) {
e.printStackTrace();
}finally {
// 释放
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
9. ReentrantReadWriteLock读写锁
读锁:共享锁,会发生死锁
写锁:独占锁,会发生死锁