文章目录
环境准备
创建普通的maven项目,确保我们的项目一定是在Java8的环境下,检查settings中的以下几点
添加一个 lombok 依赖
1.JUC 简介
什么是 JUC ?
- JUC 就是 java.util.concurrent 下面的类包,专门用于多线程的开发
为什么使用 JUC ?
- 以往我们所学,普通的线程代码,都是用的thread或者runnable接口
- 但是相比于callable来说,thread没有返回值,且效率没有callable高
2.线程和进程
线程和进行
- 线程是进程中的一个实体,线程本身是不会独立存在的。
- 进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位。
- 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
- 操作系统在分配资源时是把资源分配给进程的, 但是CPU 资源比较特殊, 它是被分配到线程的, 因为真正要占用CPU 运行的是线程, 所以也说线程是CPU 分配的基本单位。
- java默认有几个线程? 两个 main线程 gc线程
- Java 中,使用 Thread、Runnable、Callable 开启线程。
- Java 没有权限开启线程 、Thread.start() 方法调用了一个 native 方法 start0(),它调用了底层 C++ 代码。
查看源码可以发现,start方法底层调用了本地方法,本地方法就是C语言提供的
//本地方法,调用底层c++, java无法操作硬件
private native void start0();
并发和并行
并发(多线程操作同一个资源,交替执行)
- CPU一核, 模拟出来多条线程,天下武功,唯快不破,快速交替
并行(多个人一起行走, 同时进行)
- CPU多核,多个线程同时进行 ; 使用线程池操作
代码检测当前CPU核数
public class TestCore {
public static void main(String[] args) {
// 获取cpu核数
// cpu密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质: 充分利用CPU的资源
线程的状态
查看Thread
源码,可以发现枚举类State
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死等
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
wait/sleep的区别
- 来自不同的类:wait来自object类, sleep来自线程类
- 关于锁的释放:wait会释放锁, sleep不会释放锁
- 使用范围不同:wait必须在同步代码块中,sleep可以在任何地方睡
- 是否需要捕获异常:wait不需要捕获异常,sleep需要捕获异常
3.Lock 锁(重点)
Synchronized 传统的锁
之前我们所学的使用线程的传统思路是:
- 单独创建一个线程类,继承Thread或者实现Runnable
- 在这个线程类中,重写run方法,同时添加相应的业务逻辑
- 在主线程所在方法中new上面的线程对象,调用start方法启动
比如,
//线程不安全:买票例子
//线程不安全,输出结果有买重票有负数票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"抢票的我").start();
new Thread(station,"买票的你们").start();
new Thread(station,"可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
//外部停止方式
boolean flag = true;
@Override
public void run() {
//买票
while (true){
if (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
private void buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时,放大问题
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"--> 拿到第 "+ ticketNums--+" 张票");
}
}
但是这样写代码有诸多问题,不太符合OOP思想,增加耦合性等问题,
实际工作的使用线程的思路是:
- 创建一个独立的类只作为资源类,存放属性、方法,所以在多线程中我们需要锁这个公共资源
- 线程类主要作为工具使用,用于开启多线程,把资源类实例丢到线程类的重写run方法中执行业务
- 在我们的业务类中,比如主线程中,创建若干个线程类实例,去操作资源类
比如,这里我们使用了实现Runnable的方法创建线程,还是用了lambda表达式来创建Runnable实例
public class TestCore {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}).start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}).start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
ticket.sale();
}
}).start();
}
}
// 这是一个资源类,存放属性、方法
class Ticket{
private int number = 300;
public synchronized void sale(){
if(number>0){
System.out.println(Thread.currentThread().getName()+"get "+number+"#");
number--;
}
}
}
这需要我们在工作加以注意
Lock锁
查看 api 文档
可以看到,Lock
是一个接口,有三个实现类,现在我们使用 ReentrantLock
就够用了
查看 ReentrantLock
源码,构造器
公平非公平:
- 公平锁::十分公平, 可以先来后到,一定要排队
- 非公平锁::十分不公平,可以插队(默认)
ReentrantLock 构造器
- ReentrantLock 默认的构造方法是非公平锁(可以插队)。
- 如果在构造方法中传入 true 则构造公平锁(不可以插队,先来后到)。
我们将上面的抢票代码改造为
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for(int i = 0; i < 40; i++) ticket.sale();}, "a").start();
new Thread(()->{
for(int i = 0; i < 40; i++) ticket.sale();}, "b").start();
new Thread(()->{
for(int i = 0; i < 40; i++) ticket.sale();}, "c").start();
}
}
class Ticket {
private int ticketNum = 30;
private Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (this.ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "购得第" + ticketNum-- + "张票, 剩余" + ticketNum + "张票");
}
//增加错误的发生几率
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行,发现,多线程都有几率抢到票,且没有出现线程安全问题
综述,Lock 锁实现步骤:
- 创建锁,new ReentrantLock()
- 加锁,lock.lock()
- 解锁,lock.unlock()
- 基本结构固定,中间的业务自己灵活修改
synchronized 和 lock 锁的区别
- synchronized 是内置的 Java 关键字,Lock 是一个 Java 类
- synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
- synchronized 会自动释放锁,Lock 必须要手动释放锁!如果不释放锁,会产生死锁
- synchronized 假设线程1(获得锁,然后发生阻塞),线程2(一直等待); Lock 锁就不一定会等待下去,可使用 tryLock 尝试获取锁
- synchronized 可重入锁,不可以中断的,非公平的;Lock锁,可重入的,可以判断锁,是否公平(可自己设置)
- synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
总体来说,synchronized 本来就是一个关键字,很多规则都是定死的,灵活性差;Lock 是一个类,灵活性高
思考问题:什么是锁?锁的是什么?
4.生产者和消费者问题
面试高频考点:
- 单例模式、八大排序、生产者消费者、死锁
Synchronized 版本
解决线程之间的通信问题,比如线程操作一个公共的资源类
基本流程可以总结为:
- 等待:判断是否需要等待
- 业务:执行相应的业务
- 通知:执行完业务通知其他线程
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
// 创建一个生产者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
// 创建一个消费者
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//这是一个缓冲类,生产和消费之间的仓库,公共资源类
class Data{
// 这是仓库的资源,生产者生产资源,消费者消费资源
private int num = 0;
// +1,利用关键字加锁
public synchronized void increment() throws InterruptedException {
// 首先查看仓库中的资源(num),如果资源不为0,就利用 wait 方法等待消费,释放锁
if(num!=0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"=>"+num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 首先查看仓库中的资源(num),如果资源为0,就利用 wait 方法等待生产,释放锁
if(num==0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
思考问题:如果存在ABCD4个线程是否安全?
- 不安全,会有虚假唤醒
查看 api 文档
解决办法:if 判断改为 while,防止虚假唤醒
- 因为 if 只会执行一次,执行完会接着向下执行 if() 外边的代码
- 而 while 不会,直到条件满足才会向下执行 while() 外边的代码
修改代码为:
// ...
// 使用 if 存在虚假唤醒
while (num!=0){
this.wait();
}
// ...
while(num==0){
this.wait();
}
JUC 版本
锁、等待、唤醒 都进行了更换
将代码改造为 JUC 版本的生产者和消费者模式,这里我们使用四个线程,ABCD,两个生产者,两个消费者,
改造之后,确实可以实现01切换,但是ABCD是无序的,不满足我们的要求,
package com.swy.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author SuperSong
* @version 1.0.0
* @ClassName juc-study.com.swy.pc.ConsumeAndProduct.java
* @Description TODO
* @createTime 2021年04月25日 07:47:00
*/
public class ConsumeAndProductLock {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data2{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition 的优势在于,精准的通知和唤醒线程!比如,指定通知下一个进行顺序。
重新举个例子,
三个线程 A执行完调用B,B执行完调用C,C执行完调用A,分别用不同的监视器,执行完业务后指定唤醒哪一个监视器,实现线程的顺序执行
锁是统一的,但监视器是分别指定的,分别唤醒,signal,之前使用的是 signalAll
// A执行完调用B,B执行完调用C,C执行完调用A
public class ConditionDemo {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printC();
}
},"C").start();
}
}
class Data3 {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
while (num != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName() + " Im A ");
num = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + " Im B ");
num = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + " Im C ");
num = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5.八个有关锁的问题
深入理解锁
关于锁的八个问题
问题1:两个同步方法,先执行发短信还是打电话?
标准情况下,两个线程,先发短信还是先打电话?
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendMsg();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}).start();
}
}
// 可视作资源类
class Phone{
public synchronized void sendMsg(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
经过测试,一直是先发短信
问题2:如果发短信延迟2秒,谁先执行
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone()