持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
JUC笔记
JUC简介
JUC主要包在jdk中的位置
主要有:Condition、Lock、ReadWriteLock几个接口
多线程回顾
业务:普通的线程代码 继承Thread类
Runnable接口 没有返回值,效率相比Callable较低,使用Callable比Runnable多
进程和线程
进程是指在系统中正在运行的应用程序,是执行程序的依次执行过程,是一个动态概念,是资源分配(内存、外设等)的最小单位
线程是进程的基本执行单元,是CPU资源分配的最小单位
区别
- 进程之间是独立的地址空间,同一个进程的多个线程共享地址空间
- 同一进程内的线程共享本进程的所有资源,进程之间则是独立的
Java默认有两个进程:主进程main和GC(垃圾回收)守护进程。Java是无法开启线程的,是通过start0()这个本地方法调用底层的C++才能开启
并发和并行
并发: 对于单处理机而言,多条线程快速交替执行,宏观上好像是在同时运行,微观上依然是串行的
并行: 对于多处理机而言,其他资源充足的情况下,多个线程可以分别跑在一个处理机上,互不干扰同时进行,无论宏观上还是微观上,都是同时进行的
public class Test {
public static void main(String[] args) {
// 获取CPU的核数
// CPU密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质:充分利用CPU的资源
线程状态
public enum State {
// 新生态
NEW,
// 运行时状态
RUNNABLE,
// 阻塞状态
BLOCKED,
// 等待,死等,直到其他线程执行指定动作,通知结束等待
WAITING,
// 一段时间的等待,等待另一个线程执行动作达到指定时间
TIMED_WAITING,
// 终止状态
TERMINATED;
}
wait和sleep的区别
1、来自不同的类,wait来自Object,sleep来自Thread类
2、wait方法会释放锁,sleep不会释放锁
3、适用范围不同,wait必须在同步代码块中使用,sleep可以在任何地方使用
4、wait不需要捕获超时异常,sleep需要捕获超时异常
线程方法
线程优先级
Lock锁
Lock是一个接口,里面有ReentrantLock、ReadLock、WriteLock几个实现类。
在需要加锁的代码块中进行显示加锁和释放锁
可重入锁
参考:segmentfault.com/a/119000002…
ReentrantLock就是一个可重入锁Re-Entrant-Lock:即表示可重新反复进入的锁,但仅限于当前线程;锁的粒度更小,更加灵活
公平锁
先到先得,不可抢占
非公平锁
非严格先到先得,可以抢占。Java中默认为非公平锁
使用步骤
1、Lock lock = new ReentrantLock();定义一个可重入锁 2、lock.lock();添加锁 3、finally => lock.unlock();释放锁
public class TicketLock {
private int ticketNum;
// 定义一个可重入锁
Lock lock = new ReentrantLock();
public TicketLock(int ticketNum) {
this.ticketNum = ticketNum;
}
public int getTicketNum() {
return ticketNum;
}
public void setTicketNum(int ticketNum) {
this.ticketNum = ticketNum;
}
// 这里改用显式加锁方法
public void sale() {
// 显式添加锁
lock.lock();
// 尝试获取锁
// lock.tryLock();
try {
// 业务代码
if (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticketNum-- + "票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
}
synchronized和Lock锁区别
1、synchronized是内置关键字,而Lock是一个类
2、synchronized无法判读获取锁的状态,Lock可以判断是否获取到了锁
3、synchronized会自动释放锁,用完就释放;Lock需要手动释放锁,不释放可能会导致死锁
4、synchronized得到锁之后,如果阻塞,那么另外的线程会一直等待;Lock锁就不会一直等待,可以尝试使用tryLock()尝试获取锁
5、synchronized是可重入锁,不可中断,非公平的;Lock锁是可重入锁,可以设置是否是公平/非公平锁,通过设置ReentrantLock构造函数的参数
6、synchronized适合锁少量的同步代码问题;Lock适合锁大量的同步代码
生产者和消费者问题
使用synchronized锁实现的生产者和消费者问题
步骤:判断等待(while代码块中使用wait()方法)、业务处理、通知(notify/notifyAll()方法)
多线程笔记中已经实现,这里注意虚假唤醒问题,等待(wait()方法)需要放到while代码块中
代码如下:
/**
* 生产者、消费者问题,信号量法
* 这里假设演员表演节目,观众观看节目不能同时进行
*/
public class ProducerAndConsumer2 {
public static void main(String[] args) {
// 节目
TV tv = new TV();
new Actor(tv).start();
new Audience(tv).start();
}
}
// 生产者 -- 演员
class Actor extends Thread {
TV tv;
public Actor(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
tv.show("央视新闻");
} else {
tv.show("广告");
}
}
}
}
// 消费者 -- 观众
class Audience extends Thread {
TV tv;
public Audience(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
// 产品 -- 节目
class TV {
// 演员表演,观众等待
// 观众观看,演员等待
// 表演的节目
private String showName;
// 表演/观看,默认是表演,为true
private boolean flag = true;
public synchronized void show(String showName) {
// 这里需要使用while循环,避免发生虚假唤醒。如果存在多个消费者进程,如果同时有多个消费者等待之后被唤醒,但执行有先后顺序,前一个消费者执行之后条件可能不再满足其他消费者执行。
// 如果使用的是if,在等待之前已经判断过了,唤醒之后不会再进行判断,则会发生错误,所以需要使用while
// 虚假唤醒问题:https://www.cnblogs.com/jichi/p/12694260.html
while (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + showName);
// 通知观众观看
this.notifyAll();
this.showName = showName;
this.flag = !this.flag;
}
public synchronized void watch() {
while (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:" + showName);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
使用Lock锁实现的生产者和消费者问题
需要使用到Condition代替传统的wait、notify等方法,关于Condition:www.liaoxuefeng.com/wiki/125259…
代码如下:
public class Product {
// 产品数量
private int number = 0;
// Lock锁
private Lock lock = new ReentrantLock();
// 得到Condition
Condition condition = lock.newCondition();
// 改用Lock锁配合Condition实现生产者、消费者问题
// 生产
public void produce() {
// 添加锁
lock.lock();
try { // 这里需要使用while循环,避免发生虚假唤醒。如果存在多个消费者进程,如果同时有多个消费者等待之后被唤醒,但执行有先后顺序,前一个消费者执行之后条件可能不再满足其他消费者执行。
// 如果使用的是if,在等待之前已经判断过了,唤醒之后不会再进行判断,则会发生错误,所以需要使用while
// 虚假唤醒问题:https://www.cnblogs.com/jichi/p/12694260.html
while (number != 0) {
// 等待
condition.await();
}
this.number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 唤醒全部
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
// 消费
public void consunme() {
// 添加锁
lock.lock();
try { // 这里需要使用while循环,避免发生虚假唤醒。如果存在多个消费者进程,如果同时有多个消费者等待之后被唤醒,但执行有先后顺序,前一个消费者执行之后条件可能不再满足其他消费者执行。
// 如果使用的是if,在等待之前已经判断过了,唤醒之后不会再进行判断,则会发生错误,所以需要使用while
// 虚假唤醒问题:https://www.cnblogs.com/jichi/p/12694260.html
while (number == 0) {
// 等待
condition.await();
}
this.number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 唤醒全部
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
}
精准通知和唤醒线程
控制执行顺序,在Lock锁实现消费者、生产者问题的基础上实现精准通知,假设需要执行顺序为A->B->C->D
需要Condition配对,不同的Condition监视不同的等待和唤醒
代码如下:
public class DataCondition {
// 定义Lock锁
private Lock lock = new ReentrantLock();
// 获取Condition
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
// 资源类数量 为1时A执行,2时B,3时C
private int number = 1;
// 输出A
public void printA() {
// 加锁
lock.lock();
// 判断等待、业务处理、唤醒
try {
while (number != 1) {
conditionA.await();
}
System.out.println(Thread.currentThread().getName() + "=>" + "AAA");
// 设置2,去唤醒B
number = 2;
// 唤醒指定的Condition,这里设置number=2并唤醒conditionB
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 输出B
public void printB() {
// 加锁
lock.lock();
// 判断等待、业务处理、唤醒
try {
while (number != 2) {
conditionB.await();
}
System.out.println(Thread.currentThread().getName() + "=>" + "BBB");
// 设置3,去唤醒C
number = 3;
// 唤醒指定的Condition,这里设置number=2并唤醒conditionC
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 输出C
public void printC() {
// 加锁
lock.lock();
// 判断等待、业务处理、唤醒
try {
while (number != 3) {
conditionC.await();
}
System.out.println(Thread.currentThread().getName() + "=>" + "CCC");
// 设置1,去唤醒A
number = 1;
// 唤醒指定的Condition,这里设置number=2并唤醒conditionA
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
什么是锁?
锁的一般是对象(类的实例)和类(Class)
8锁现象(关于锁的8个问题)理解锁是什么?
1、一般情况下,顺序调用synchronized锁实现的两个同步方法,哪个会先执行?
先获得锁的方法会先执行,通常调用的方法会先获得锁,这里是sendMsg方法先执行
代码示例:
资源类
public class Phone01 {
public synchronized void sendMsg() {
System.out.println("发短信");
}
// synchronized锁的是方法的调用者,是当前类的一个实例
public synchronized void call() {
System.out.println("打电话");
}
}
测试类
public class PhoneTest01 {
public static void main(String[] args) throws InterruptedException {
Phone01 phone = new Phone01();
// 这里就算sendMsg()方法休眠3秒,也是sendMsg()先执行,因为先调用sendMsg(),所以该方法先获得了phone这个对象的锁
// 谁先获得锁谁先执行
// 这里的phone::sendMsg是在lamda表达式里面使用方法引用(方法引用由::双冒号操作符标示)的方式,参考:https://www.bbsmax.com/A/x9J2Pj1nd6/#stream--parallelstream
// 方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
new Thread(phone::sendMsg, "A").start();
// JUC包下的sleep,这里是睡眠1秒
TimeUnit.SECONDS.sleep(1);
new Thread(phone::call, "B").start();
}
}
2、在 1 的基础上,sendMsg方法中添加延迟,延迟3秒,哪个方法先执行?
依然是先获得锁的方法先执行,就算该方法中添加了延迟,这里还是sendMsg方法先执行
修改 1 中的资源类
public class Phone01 {
public synchronized void sendMsg() {
try {
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// synchronized锁的是方法的调用者,是当前类的一个实例
public synchronized void call() {
System.out.println("打电话");
}
}
3、在 2 的基础上添加一个普通方法hello,哪个方法先执行?
hello方法先执行,然后是sendMsg,普通方法不需要获得资源类对象的锁,在有CPU资源是就可以直接执行
代码如下:
资源类:
public class Phone01 {
public synchronized void sendMsg() {
try {
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// synchronized锁的是方法的调用者,是当前类的一个实例
public synchronized void call() {
System.out.println("打电话");
}
// 非同步方法不受锁的影响
public void hello() {
System.out.println("非同步方法");
}
}
测试类:
public class PhoneTest01 {
public static void main(String[] args) throws InterruptedException {
Phone01 phone = new Phone01();
// 谁先获得锁谁先执行
new Thread(phone::sendMsg, "A").start();
// JUC包下的sleep,这里是睡眠1秒
TimeUnit.SECONDS.sleep(1);
new Thread(phone::call, "B").start();
// hello()方法不需要获取锁,所以不需要等待A或B执行完,可以自主执行
new Thread(phone::hello, "C").start();
}
}
4、在 2 的基础上,再添加一个资源类,两个线程分别调用两个资源类中的不同方法,哪个方法先执行?
call先执行,因为sendMsg中有一个延迟。这时有两个对象,则有两把锁,phone和phone1的锁互不干扰,即使sendMsg先获得了phone的锁,但不影响phone1, 但因为sendMsg中有一个3秒的延迟,所以call 会获得CPU 资源,call先执行
代码示例:
资源类:
public class Phone01 {
public synchronized void sendMsg() {
try {
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// synchronized锁的是方法的调用者,是当前类的一个实例
public synchronized void call() {
System.out.println("打电话");
}
}
测试类:
public class PhoneTest01 {
public static void main(String[] args) throws InterruptedException {
Phone01 phone = new Phone01();
// 再添加一个资源类,这时就有了两个对象,两把锁
Phone01 phone1 = new Phone01();
// 谁先获得锁谁先执行
new Thread(phone::sendMsg, "A").start();
// JUC包下的sleep,这里是睡眠1秒
TimeUnit.SECONDS.sleep(1);
new Thread(phone1::call, "B").start();
}
}
5、在 2 的基础上,将普通同步方法改为静态同步方法之后,哪个方法先执行?
sendMsg先执行,这时由于是静态方法,则在类一加载时就被加载进内存了,这时synchronized锁的是Class,所以先调用sendMsg,则sendMsg方法先获得锁,先执行
代码示例:
资源类:
public class Phone02 {
// 修改为静态同步方法,类一加载时方法就加载到内存中了,这时synchronized锁的是Class,不再是对象
public static synchronized void sendMsg() {
try {
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// synchronized锁的是方法的调用者,是当前类的一个实例
public static synchronized void call() {
System.out.println("打电话");
}
}
测试类:
public class PhoneTest02 {
public static void main(String[] args) throws InterruptedException {
// 这里就算sendMsg()方法休眠3秒,也是sendMsg()先执行,因为先调用sendMsg(),所以该方法先获得了Phone02这个Class的锁
// 谁先获得锁谁先执行
new Thread(Phone02::sendMsg, "A").start();
// JUC包下的sleep,这里是睡眠1秒
TimeUnit.SECONDS.sleep(1);
new Thread(Phone02::call, "B").start();
}
}
6、在 5 的基础上,先构建两个资源类对象,两个线程再通过这两个资源类分别调用不同的方法,哪个方法先执行?
sendMsg先执行,这时由于是静态方法,则在类一加载时就被加载进内存了,这时synchronized锁的是Class,所以先调用sendMsg,则sendMsg方法先获得锁,先执行。这时无论几个对象,都是sendMsg 方法先执行,因为这时synchronized锁定的是Class(唯一的),而不是具体的对象,所以和具体对象的个数无关。甚至不需要构建对象,可以直接通过类名调用对应的方法。
7、在 5 的基础上修改,call改为非静态方法,调用顺序不变,哪个方法先执行?
call方法先执行,sendMsg中有延迟。这是因为sendMsg是静态方法,锁的的是Class;call是非静态方法,锁的是phone这个对象。二者不是同一个锁,而且互不干扰,但sendMsg存在延迟,所以call先执行
代码示例:
资源类:
public class Phone02 {
// 修改为静态同步方法,类一加载时方法就加载到内存中了,这时synchronized锁的是Class,不再是对象
public static synchronized void sendMsg() {
try {
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// synchronized锁的是方法的调用者,是当前类的一个实例
public synchronized void call() {
System.out.println("打电话");
}
}
测试类:
public class PhoneTest02 {
public static void main(String[] args) throws InterruptedException {
Phone02 phone = new Phone02();
// 谁先获得锁谁先执行
new Thread(Phone02::sendMsg, "A").start();
// JUC包下的sleep,这里是睡眠1秒
TimeUnit.SECONDS.sleep(1);
new Thread(phone::call, "B").start();
}
}
8、在 7 的基础上,再添加一个资源类,一个调用静态方法sendMsg,一个调用非静态方法call,哪一个先执行?
call先执行,原理同 7 类似,增加资源类调用静态方法,同样获得是Class(唯一)的锁,多个对象调用静态方法和使用类名调用静态方法,效果是相同的。而非静态方法获得的phone对象的锁仍然不受Class锁的影响,sendMsg 存在延迟,所以call先执行。