day14 多线程02
1. lock锁
java.util.concurrent(多线程的使用)
Lock锁对象 VS synchoronized 锁对象
区别
1. synchronized 锁对象,只提供了用来模拟锁状态的标志位(加锁和释放锁),
但是加锁和释放锁的行为,都是由jvm隐式完成(和synchronized 锁对象没关系),
所以synchronized 锁对象不是一把完整的锁
2.一个Lock对象,就代表一把锁,而且还是一把完整的锁,(模拟了对象和行为)
Lock对象,它如果要实现加锁和释放锁,不需要synchronized关键字配合,它自己就可以完成
Lock(接口): lock() 加锁
unlock 释放锁
3. 两种锁对象,实现完全不同的
联系: 都可以实现线程同步
1. synchronized(锁对象) {
需要同步的代码
}
2. lock.lock()
需要同步的代码
lock.unlock()
Lock锁对象的标准使用
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
学习了Lock锁对象之后,我们就有两种方式,构造同步代码块,从而实现线程通过(构造原子操作),
实际开发的时,使用哪种方式呢? 推荐 synchronized代码块
1. 两种方式,实现线程同步,效果相同,但是 使用synchronized代码块的方式要简单的多
2. 虽然说在jdk早期版本中,两种方式加锁和释放锁,确实有效率上的差别,Lock锁机制加锁释放锁效率高一些,但是,在今天的jdk中,两种方式加锁和释放锁的效率已经相差无几了
对电影院售票问题继续改进:
将synchronized代码块替换为lock即可:(修改的部分)
// Lock锁对象(放在SaclesTask对象中)
private Lock lock = new ReentrantLock();
//在run()方法中
// 加锁
lock.lock();
try {
if (tickets > 0) {
// double check
System.out.println(Thread.currentThread().getName() + ": 售卖除了第" + this.tickets-- + "张票");
}
} finally {
// 释放锁
lock.unlock();
}
2.死锁问题
死锁问题:
死锁是指两个以上的线程在执行过程中,因为争夺资源而产生的一种相互等待的现象
同步另一个弊端:如果出现了嵌套锁,可能产生死锁
什么时候会出现嵌套锁? 如果说,在某些情况下,一个线程需要同时持有多把锁,此时就会出现所谓的嵌套锁
比如: 某个线程要同时持有两把锁lockA 和 lockB两把锁,换个说法,
该线程,成功持有lockA锁的情况下,在持有lockB锁
synchronized (lockA) {
//当某线程的代码,执行到这里
synchronized (lockB) {
// 执行到这里,意味着当前线程在持有lockA锁的情况下,又持有了lockB这把锁,所以此时当前线程就同时持有两把锁
}
}
案例:
- 假设,在多线程运行环境下,假设我们的程序要访问一个共享变量
- 利用共享变量,计算得到一些结果,但是对于一些计算的中间结果,需要利用打印机打印出来
而打印机,一次只允许一个线程访问(多线程对打印机的访问,还要做一次线程同步)
所以很显然,一个线程要完成上述工作,得获取对共享变量的访问权(对被用来同步共享访问,的那把锁加锁),还要获取,对打印机的访问权(对被用来同步访问,打印机的锁成功加锁)
假设对象共享变量的同步,使用的锁lockA
假设对象访问打印机的同步,使用的锁lockB
实现代码:
public class Demo1 {
public static Object lockA = new Object();
public static Object lockB = new Object();
public static void main(String[] args) {
ABThread abThread = new ABThread();
BAThread baThread = new BAThread();
abThread.start();
baThread.start();
}
}
/*
先访问共享变量,在访问打印机
*/
class ABThread extends Thread {
@Override
public void run() {
synchronized (Demo1.lockA) {
// 1. 访问共享变量
System.out.println("ABThread lockA锁加锁成功");
synchronized (Demo1.lockB) {
// 2. 根据共享变量的值,完成计算,并将结果发送打印机
System.out.println("ABThread lockB锁加锁成功");
}
}
}
}
/*
向获取对打印机的互斥访问权,在访问共享变量,将结果发送打印机
*/
class BAThread extends Thread {
@Override
public void run() {
synchronized (Demo1.lockB) {
// 先实现对打印机的互斥访问
System.out.println("BAThread lockB加锁成功");
synchronized (Demo1.lockA) {
// 访问共享变量,计算结果,发送打印机
System.out.println("BAThread lockA加锁成功");
}
}
}
}
运行结果(有可能为):
出现了死锁问题
3.如何解决死锁问题
解决思路一:调整加锁顺序:将两个class的加锁顺序都改为先加锁A,再加锁B
解决思路二:如果加锁顺序不好调整:如果我们能让一个线程一次成功对它需要的锁都加锁成功,即要么对所有需要的锁加锁成功,要么一把锁都不加(将加2把锁变成一组原子操作)
实现代码中,定义一个静态对象并加锁即可:
public static Object allLock = new Object();
@Override
public void run() {
synchronized (Demo1.allLock) {
synchronized (Demo1.lockA) {
System.out.println("ABThread lockA锁加锁成功");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Demo1.lockB) {
System.out.println("ABThread lockB锁加锁成功");
}
}
}
4.生产者消费者问题
先介绍两种方法:wait()和notify()和notifyAll()方法
wait()方法
public final void wait()
阻止自己:
wait()(jdk中的解释)
1.在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待
2.当前线程必须拥有此对象监视器。
3.该线程发布对此监视器的所有权并等待,
4.直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来
1. 阻塞功能:
当在某线程中,对象上.wait(), 在哪个线程中调用wait(), 导致哪个线程处于阻塞状态
说法:
当某线程,因为调用执行某对象的wait(),而处于阻塞状态,我们说,该线程在该对象上阻塞。
2. 唤醒条件
当某线程,因为某对象A的wait(), 而处于阻塞状态时,如果要唤醒该线程,只能在其他线程中,
再同一个对象(即对象A)上调用其notify()或notifyAll()
即在其他线程中,调用notify或notifyAll方法,才能唤醒,在该对象上阻塞的线程
3. 运行条件
当前线程必须拥有此对象监视器。
监视器:指synchronized代码块中的锁对象
即我们只能在,当前线程所持有的synchronized代码块中的,锁对象上调用wait方法,才能正常执行
4. 执行特征
a.该线程发布(release)对此监视器的所有权
b.等待(阻塞)
第3,4点的理解(代码):
Object o = new Object();
//o.wait();//这样做不会阻塞当前的线程
//会报错:IllegalMonitorStateException
synchronized (o) {
// 会让当前线程放弃锁的持有,即释放这把锁
o.wait();
}
System.out.println("after wait");
notify(),notifyALL()方法
通知别人:
notify():
1. 唤醒在此对象监视器上等待的单个线程。
2. 如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。
3. 选择是任意性的,并在对实现做出决定时发生。
notifyAll():
唤醒在此对象监视器上等待的所有线程
5.实现生产者,消费者问题
版本一:
通过在生产者和消费者进程中做同步,避免死锁的问题:
创建4个类:
- 消费者任务类:ConsumerTask
- 生产者任务类:ProducerTask
- 蒸笼共享区(描述蒸笼中的包子):class Container+class Food
- 主线程:创建生产者,消费者类和蒸笼共享区,启动各个线程
/*
消费者(吃包子的人)任务类
*/
public class ConsumerTask implements Runnable {
private Container container;
public ConsumerTask(Container container) {
this.container = container;
}
@Override
public void run() {
while (true) {
synchronized (container) {
// 1. 判断蒸笼状态
if (container.isEmpty()) {
// 如果蒸笼为空,则阻止自己
try {
// 在锁对象上调用wait方法
// 当前线程会释放,自己持有的锁
container.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 如果蒸笼不空,拿包子吃,并通知生产者做包子
// 吃包子
container.eatFood();
// 通知别人
container.notify();
}
}
}
}
}
import java.util.Random;
public class ProducerTask implements Runnable {
private Container container;
// 模拟菜单
private Food[] foodBill = {new Food("豆沙包", 1), new Food("粉丝包", 2),
new Food("鲜肉包", 3), new Food("狗不理",50)};
public ProducerTask(Container container) {
this.container = container;
}
@Override
public void run() {
while (true) {
synchronized (container) {
// 1. 判断蒸笼状态
if (!container.isEmpty()) {
// 如果蒸笼不空,阻止自己
try {
container.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 3. 如果蒸笼为空,做包子放入蒸笼,并通知吃包子的赶紧来吃
// 随机选择一种包子,放入蒸笼
Random random = new Random();
// nextInt(int bound), bound限定随机整数的上界,[0, bound)
int foodIndex = random.nextInt(foodBill.length);
// 放入包子
container.setFood(foodBill[foodIndex]);
// 通知别人, 通知吃包子的人赶紧来吃
container.notify();
}
}
}
}
}
/*
描述蒸笼
*/
public class Container {
/*
蒸笼中的包子
*/
private Food food;
/*
让生产者放入包子
*/
public void setFood(Food food) {
this.food = food;
System.out.println(Thread.currentThread().getName() + ": 做了" + food);
}
/*
让消费者拿包子出来吃
*/
public void eatFood() {
System.out.println(Thread.currentThread().getName() + ": 吃了" + food);
// 将包子从蒸笼中拿出来吃掉的行为
food = null;
}
/*
返回蒸笼的状态,即蒸笼是否为空
*/
public boolean isEmpty() {
return food == null;
}
}
/*
描述蒸笼中的包子
*/
class Food {
private String name;
private double price;
public Food(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Container{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
public class Test {
public static void main(String[] args) {
// 创建一个蒸笼对象
Container container = new Container();
// 创建任务对象
ConsumerTask consumerTask = new ConsumerTask(container);
ProducerTask producerTask = new ProducerTask(container);
// 运行
Thread thread1 = new Thread(consumerTask, "吃包子的");
Thread thread2 = new Thread(producerTask, "做包子的");
thread1.start();
thread2.start();
}
}
版本二:(实现"智能蒸笼")
智能蒸笼:
1. 线程间同步,因为eat方法运行在消费者线程,setFood运行在生产者线程
所以对于生产者线程,消费者的线程同步,通过eatFood和setFood实现
将他们变成同步方法(使用同一个锁对象,即同一个蒸笼对象)
2. 线程间的协作,因为eat方法运行在消费者线程,setFood运行在生产者线程
所以,我们只需要在eatFood方法和setFood方法体,实现阻止自己,通知别人
实现线程间的协作了
3. 同时,要实现多个生产者(>=2)和多个消费者(>=2),1个蒸笼共享区不发生死锁的问题(使用notifyAll()方法)
版本2和版本1的代码的区别:
- 将蒸笼作为了锁对象
- 使用synchronize实现了锁对象中的同步方法
- 使用了notifyAll()方法
同样的4个类:
- 消费者任务类:ConsumerTask
- 生产者任务类:ProducerTask
- 蒸笼共享区(描述蒸笼中的包子):class Container+class Food
- 主线程:创建生产者,消费者类和蒸笼共享区,启动各个线程
public class ConsumerTask implements Runnable {
private Container container;
public ConsumerTask(Container container) {
this.container = container;
}
@Override
public void run() {
while (true) {
// 吃包子
container.eatFood();
}
}
}
生产者任务类:
import java.util.Random;
public class ProducerTask implements Runnable {
private Container container;
// 模拟菜单
private Food[] foodBill = {new Food("豆沙包", 1), new Food("粉丝包", 2),
new Food("鲜肉包", 3), new Food("狗不理",50)};
public ProducerTask(Container container) {
this.container = container;
}
@Override
public void run() {
while (true) {
// 做包子放入蒸笼
// 随机选择一种包子,放入蒸笼
Random random = new Random();
// nextInt(int bound), bound限定随机整数的上界,[0, bound)
int foodIndex = random.nextInt(foodBill.length);
// 放入包子
container.setFood(foodBill[foodIndex]);
}
}
}
蒸笼共享区(核心!):
public class Container {
private Food food;
/*
让生产者放入包子, 运行在生产者线程中
*/
public synchronized void setFood(Food food) {
// 1. 判断蒸笼状态
if (this.food != null) {
// 如果有包子,阻止生产者线程
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 如果没有包子,才允许将包子放入蒸笼
System.out.println(Thread.currentThread().getName() + ": 做了" + food);
this.food = food;
// 通知别人
notifyAll();
}
}
/*
让消费者拿包子出来吃,运行在消费者线程中
*/
public synchronized void eatFood() {
if (this.food == null) {
// 蒸笼为空
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + ": 吃了" + food);
// 将包子从蒸笼中拿出来吃掉的行为
this.food = null;
// 通知别人
notifyAll();//注意这里不是notify();
}
}
}
/*
描述蒸笼中的包子
*/
class Food {
private String name;
private double price;
public Food(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Container{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
Test中的主线程:
public class Test {
public static void main(String[] args) {
// 创建一个蒸笼对象
Container container = new Container();
// 创建任务对象
ConsumerTask consumerTask = new ConsumerTask(container);
ProducerTask producerTask = new ProducerTask(container);
// 运行
Thread thread11 = new Thread(consumerTask, "吃包子的1");
Thread thread12 = new Thread(consumerTask, "吃包子的2");
Thread thread21 = new Thread(producerTask, "做包子的1");
Thread thread22 = new Thread(producerTask, "做包子的2");
thread11.start();
thread12.start();
thread21.start();
thread22.start();
}
}
6.面试题:聊一聊sleep和wait方法的区别
Thread.sleep VS Object.wait()
1. 所属不同:
a. sleep定义在Thread类,静态方法
b. wait定义在 Object类中,非静态方法
2. 唤醒条件不同
a. sleep: 休眠时间到
b. wait: 在其他线程中,在同一个锁对象上,调用了notify或notifyAll方法
3. 使用条件不同:
a. sleep 没有任何前提条件
b. wait(), 必须当前线程,持有锁对象,锁对象上调用wait()
4. 休眠时,对锁对象的持有,不同:(最最核心的区别)
a. 线程因为sleep方法而处于阻塞状态的时候,在阻塞的时候不会放弃对锁的持有
b. 但是wait()方法,会在阻塞的时候,放弃锁对象持有
7.java语言中线程的几种状态
几种状态的阐述: