文章目录
- 单例模式
- 什么是单例模式
- 饿汉模式
- 懒汉模式
- 多线程- 懒汉模式
- 分析多线程问题
- 第一种添加sychronized的方式
- 第二种添加sychronized的方式
- 改进第二种添加sychronized的方式(DCL检查锁)
- 阻塞队列
- 什么是阻塞队列
- 什么是消费生产者模型
- 标准库中的阻塞队列
- 消息队列应用的场景
- 自己模拟实现阻塞队列
- 定时器
- 标准库中的定时器
- 实现定时器
- 工厂模式
- 线程池
- 线程池的一些问题
- 实现一个线程池
- 创建系统自带的线程池
- wait和sleep的区别
单例模式
什么是单例模式
- 单例模式能保证某个类在程序中只存在唯⼀⼀份实例, ⽽不会创建出多个实例
- 单例模式实现方式很多,最常用饿汉模式和懒汉模式实现
饿汉模式
- 创建过程:
– 1. 定义一个static修饰的变量,就可以包子这个变量全局唯一
– 2.构造方法私有化,防止变量被修改
– 3.提供一个获取变量的get静态方法,通过类名的方式去调用
public class Singleton {
//懒汉模式
//创建一个私有静态属性,并且把对象new出来
private static Singleton instance =new Singleton();
//私有化构造器
private Singleton() {
}
//提供一个公共的静态方法,返回单例对象
public static Singleton getInstance() {
return instance;
}
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true
}
}
- 把这种类加载时候就完成对象的初始化的创建方式,就叫”饿汉模式“
- 这种模式存在的问题是,可能对象创建了但是没有使用,从而导致资源浪费。
懒汉模式
public class SingLetonLazy {
//创建一个对象不去new对象
private static SingLetonLazy instance;
//私有化构造器
private SingLetonLazy() {
}
//提供一个公共的静态方法,返回单例对象
public static SingLetonLazy getInstance() {
if(instance==null) {
instance=new SingLetonLazy();
}
return instance;
}
public static void main(String[] args) {
SingLetonLazy s1 = SingLetonLazy.getInstance();
SingLetonLazy s2 = SingLetonLazy.getInstance();
System.out.println(s1 == s2); // true
}
}
- 懒汉模式创建对象,在要获得单例对象的时候,创建,避免了资源的浪费但是存在多线程安全问题。
多线程- 懒汉模式
public class SingLetonLazy {
private static SingLetonLazy instance;
//私有化构造器
private SingLetonLazy() {
}
//提供一个公共的静态方法,返回单例对象
public static SingLetonLazy getInstance() {
if(instance==null) {
instance=new SingLetonLazy();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread t1 =new Thread(()->{
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
SingLetonLazy s1 = SingLetonLazy.getInstance();
System.out.println(s1);
});
t1.start();
}
}
}
出现了多线程问题。
分析多线程问题
第一种添加sychronized的方式
public static SingLetonLazy getInstance() {
if(instance==null) {
synchronized (SingLetonLazy.class){
instance=new SingLetonLazy();
}
}
return instance;
}
- 这种写法不能保证多线程安全
第二种添加sychronized的方式
public static SingLetonLazy getInstance() {
synchronized (SingLetonLazy.class){
if(instance==null) {
instance=new SingLetonLazy();
}
}
return instance;
}
- 这种写法似乎可以保证多线程安全,但是还是存在一个问题,当一个线程进行这个方法,如果没有初始化,则获取锁进行初始化操作,此时单例对象被第一个线程创建完成,后面的线程以后永远不会在执行new对象的操作,synchronized就没必要添加了,第二次线程开始这个加锁解锁都是无效的操作,lock和unlock对应的锁指令是互斥锁,比较消耗系统资源。
- 添加锁本质就会消耗很多资源
改进第二种添加sychronized的方式(DCL检查锁)
private volatile static SingLetonLazy instance;
//给共享变量加上volatile
public static SingLetonLazy getInstance() {
//第一次判断是否加锁
if(instance==null) {
synchronized (SingLetonLazy.class) {
判断是否创建了一个对象
if (instance == null) {
instance = new SingLetonLazy();
}
}
}
return instance;
}
阻塞队列
什么是阻塞队列
- 阻塞队列本质还是队列,遵循”先进先出“的原则
- 阻塞队列是一种线程安全的数据结构,有以下特征
– 当队列满的时候,继续入队就会发生阻塞等待,直到队列中有其他线程取出元素后,队列有空位才会再次入队
– 当队列空的时候,继续出队就会放生阻塞等待,知道队列中有其他线程插入元素时候,队列有元素才会再次出队 - 阻塞队列适用于一种典型场景‘消费生产者模型’
什么是消费生产者模型
- 生产者消费者模式就是通过一个容器解决消费者和生产者的强耦合问题。
- 生产者和消费者不会直接影响,生产者生产的资源直接放入容器(阻塞队列)中,消费者消费的资源,直接从容器(阻塞队列)中拿。从而保证生产者不会生产资源等待消费者消费,消费者也不会等待生产者生产资源。
- 阻塞队列相当于一个缓冲区,平衡生产者和消费者的处理能力
– 比如双11时候,会涌入大量的支付订单,这时候如果服务器直接处理这些订单,可能就会把服务器挤爆,这时候中间设置一个阻塞队列,把产生的大小支付订单扔进阻塞队列里面,然后服务器根据自己的处理能力,从队列里面取出要处理的订单,从而达到削峰的效果,防止服务器被挤爆。 - 阻塞队列也能使生产者和消费者之间 解耦
– 过年期间大家都会包饺子,擀饺子皮相当于生产者,包饺子相当于消费者,中间放个案板,所有的饺子皮都放在案板上,包饺子皮的人直接从案板上取,擀饺子皮的可能是妈妈可能是爸爸可能是我,无论是谁擀饺子皮消费者都不关心,因为都是从案板上取的饺子皮。
标准库中的阻塞队列
- 在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
– BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
– put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
– BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性. - 创建一个BlockingQueue
– 其中capacity是这个队列的大小。
– 这里设置一个三个大小的阻塞队列,当第四个元素入队时候就会发生阻塞等待
– 这里取出三个元素后,队列为空,队列阻塞等待
– put和take都会抛出一个InterrupteException异常
- 其他补充常问的方法
– add()
– offer()
– remove()
– poll
消息队列应用的场景
-
解耦
– 高内聚,低耦合:业务强相关的代码组织在一起,不相关的单独定义便于以后的维护,以为要把重复的代码尽量抽象出来,封装成一个公共方法,在需要的地方直接调用这个方法即可
– 生产消息的应用程序把消息写进消息队列(生产者),使用消息的应用程序从消息队列里面取出消息(消费者)
在这个模型中,服务器A要时刻感应到服务器B,在调用的过程中双方都要知道对方需要调用的参数和调用方式
,在ABC整个调用的链路中秒如果其中一个出现了问题,就会影响整个业务执行
-
削峰填谷(流量)
– 针对流量暴增的时候使用消息队列来进行缓冲
– 实例:
-
异步操作
周末:我和我女朋友取买包子
- 同步操作:她一直等我买包子回来,开始,中间这个过程啥也不干,同步发出请求后,必须要等待响应才能- 进行下一步操作
- 异步操作:她让我去之后,在家做点别的事情,比如,做点小菜,熬点稀饭,异步操作,发出请求之后,不需要等待响应,而做其他的事情,等待响应主动通知自己
自己模拟实现阻塞队列
public class MyBlockingDeque {
int [] arr;
volatile int head=0;
volatile int tail=0;
volatile int size=0;
MyBlockingDeque(int capacity){
if(capacity<=0) {
throw new RuntimeException("capacity must be positive");
}
arr = new int[capacity];
}
public void put(int val) throws InterruptedException {
while(size>=arr.length) {
synchronized (this){
this.wait();
}
}
arr[tail]=val;
tail++;
if(tail>=arr.length) {
tail=0;
}
size++;
synchronized (this){
this.notifyAll();
}
}
public synchronized int take() throws InterruptedException {
while(size==0) {
this.wait();
}
int val =arr[head];
head++;
if(head>=arr.length) {
head=0;
}
size--;
this.notifyAll();
return val;
}
}
class Main{
public static void main(String[] args) throws InterruptedException {
MyBlockingDeque myBlockingDeque = new MyBlockingDeque(10);
int i=0;
new Thread(()->{
while (true){
try {
sleep(1000);
int val = myBlockingDeque.take();
System.out.println(Thread.currentThread().getName()+"取出成功"+val);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
while (true){
myBlockingDeque.put(i);
System.out.println(Thread.currentThread().getName()+"添加成功"+i);
i++;
}
}
}
- put时候
- take时候
- 我们上锁可以锁代码块也可以方法
- if改为while的原因是防止大量现场
定时器
标准库中的定时器
- 标准库中定义一个TImer类。Timer类的核心方法为schedule
- schedule包含两个参数,第一个参数指定要执行的代码任务,第二个参数指定多场实际之后执行。
import java.util.Timer;
import java.util.TimerTask;
public class Demo_801 {
public static void main(String[] args) {
// 使用jdk中提供的类,创建一个定器
Timer timer = new Timer();
//向定时器添加任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello World!");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务1");
}
},1500);timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务3");
}
},2500);
}
}
ctrl+p查看方法的参数列表
定义自己的任务
延迟多久执行的任务
任务具体执行的时间
实现定时器
- 设计思路
- 用一个类描述任务和执行任务的时间
– 具体任务逻辑用Runable表示,执行时间可以用一个long型delay表示 - 组织任务和时间对应的对象
– 可以考虑用一个阻塞队列,我们选择用PriorityBlockingQueue(),保证扫描任务时候,延时最少的任务先执行
-
提供一个方法,提交任务
-
要有一个线程执行任务
– 在哪里定义扫描线程?
–在构造方法里面直接定义线程
– 1.取出队首元素,2.判断一下任务到执行的时间没有,3,如果到了就执行,4.没有就放回队列
- 代码实现
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
public class MyTimer {
//用一个阻塞队列来组织任务
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object lock = new Object();
public MyTimer() {
//创建线程
Thread thread =new Thread(()->{
while(true){
try {
//从队列中取出任务
MyTask task=this.queue.take();
//判断有没有到执行的时间
long currentTime=System.currentTimeMillis();
if(currentTime>=task.getTime()){
//时间到了执行
task.getRunnable().run();
} else{
//时间没到,将任务放回队列中
this.queue.put(task);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
/**
* 添加定时任务
* @param runnable 任务
* @param delay 延时
* @throws InterruptedException
*/
public void schedule(Runnable runnable,long delay) throws InterruptedException {
//根据传的参数构造一个MyTask对象
MyTask task=new MyTask(runnable,delay);
//将这个MyTask对象阻塞放入队列中
queue.put(task);
}
}
//MyTask类,用于封装任务和执行时间
class MyTask implements Comparable<MyTask>{
//任务
private Runnable runnable;
//执行时间
private long time;
public MyTask(Runnable runnable, long delay) {
//增强代码健壮性
if(runnable==null){
throw new IllegalArgumentException("任务不能为空");
}
if(delay<0) {
throw new IllegalArgumentException("延迟时间不能为负数");
}
this.runnable = runnable;
//计算出任务的执行时间
this.time = delay+System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
if(this.getTime()<o.getTime()){
return -1;
} else if(this.getTime()==o.getTime()){
return 0;
}else {
return 1;
}
//万一时间超过了long的范围溢出,怎么办?用上面的比较比较好
//return (int)(this.getTime()-o.getTime());
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable(){
@Override
public void run() {
System.out.println("任务1");
}
},1000);
timer.schedule(new Runnable(){
@Override
public void run() {
System.out.println("任务2");
}
},500);
timer.schedule(new Runnable(){
@Override
public void run() {
System.out.println("任务3");
}
},2000);
//timer.schedule(null,-100);
//任务加强健壮性
}
}
- 注意事项:
– 注意我们要实现Conparable接口指定排序规则
– 我们要添加校验,防止非法的输入
– 解决数据可能会溢出的问题,比如设置的时间
- 再次深度优化我们代码
- 以上代码我们存在“忙等”的情况
- 优化后的代码
– 这里注意一下这个lambda表达式中的this引用的是他所在对象的实例。 - 新的问题:当任务1在等待时候,这时候如果又put进来一个新的任务,这个等待的时间就有问题。再次优化
每添加新的任务都进行一次唤醒,保证执行的永远是最少延时的任务。 - 从CPU调度的过程中可以会产生的执行顺序的问题,或当一个线程执行到一半的时间被掉调度走的现象。
这个线程造成的原因就是没有保证原子性。 - 优化代码
- 再次观察一种极端情况
– 我们发现当我们把三个任务的延时时间设置为0的时候,结果只执行了任务1,我们进行调试
– 调试之后我们又发现是正常情况,但是运行时候不符合我们的预期结果,这时候我们不要慌,我们用jconsole工具去查看下扫描情况
- 我们发现在MyTimer。java22行出现了问题
1.创建一个定时器
2.向定时器添加任务1
3.第一个任务被添加到阻塞队列中
4.扫描线程启动,处理第一个任务
5.扫描线程1循环,获得第二个任务时候,队列为空,开始等待,同时扫描线程获得锁
6.主线程向阻塞队列添加任务时候,等待扫描对象的对象,由于扫描线程无法释放锁对象,主线程也就获取不到锁对象,造成相互等待,造成死锁
- 我们再次优化代码创造一个后台扫描线程,只做定时唤醒操作,定时1秒或者10ms,唤醒一次
-最终的代码
public class MyTimer {
//用一个阻塞队列来组织任务
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object lock = new Object();
public MyTimer() {
//创建线程
Thread thread =new Thread(()->{
while(true) {
try {
synchronized (this) {
//从队列中取出任务
MyTask task = this.queue.take();
//判断有没有到执行的时间
long currentTime = System.currentTimeMillis();
if (currentTime >= task.getTime()) {
//时间到了执行
task.getRunnable().run();
} else {
//时间没到,将任务放回队列中
long waitTime = task.getTime() - currentTime;
this.queue.put(task);
this.wait(waitTime);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
//创建守护线程,定时唤醒一次
Thread deamonThread=new Thread(()->{
synchronized (this) {
//唤醒一次
this.notifyAll();
//每隔100ms唤醒一次
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//设置为守护线程
deamonThread.setDaemon(true);
deamonThread.start();
}
/**
* 添加定时任务
* @param runnable 任务
* @param delay 延时
* @throws InterruptedException
*/
public void schedule(Runnable runnable,long delay) throws InterruptedException {
//根据传的参数构造一个MyTask对象
MyTask task=new MyTask(runnable,delay);
//将这个MyTask对象阻塞放入队列中
queue.put(task);
System.out.println("任务添加成功");
}
}
//MyTask类,用于封装任务和执行时间
class MyTask implements Comparable<MyTask>{
//任务
private Runnable runnable;
//执行时间
private long time;
public MyTask(Runnable runnable, long delay) {
//增强代码健壮性
if(runnable==null){
throw new IllegalArgumentException("任务不能为空");
}
if(delay<0) {
throw new IllegalArgumentException("延迟时间不能为负数");
}
this.runnable = runnable;
//计算出任务的执行时间
this.time = delay+System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
if(this.getTime()<o.getTime()){
return -1;
} else if(this.getTime()==o.getTime()){
return 0;
}else {
return 1;
}
//万一时间超过了long的范围溢出,怎么办?用上面的比较比较好
//return (int)(this.getTime()-o.getTime());
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable(){
@Override
public void run() {
System.out.println("任务1");
}
},0);
timer.schedule(new Runnable(){
@Override
public void run() {
System.out.println("任务2");
}
},0);
timer.schedule(new Runnable(){
@Override
public void run() {
System.out.println("任务3");
}
},0);
//timer.schedule(null,-100);
//任务加强健壮性
}
}
工厂模式
- 先看出现的问题
我们这里造成了重载参数的相同,但是我们就是要这样的构造方法我们怎么解决呢?
工厂方法模式。根据不同的业务需求定义不同的方法来获取对象。
线程池
线程池的一些问题
- 什么是线程池
1.线程池就是一次创建多个线程,把这些线程放进一个池中,用的时候从池中取出,用完就还回去 - 为什么要用线程池
我们首先要明白,线程的创建和销毁都会消耗大量的资源,线程池中的线程当有任务的时候,就会执行任务,没有任务的时候就阻塞等待,并不销毁线程,线程池最⼤的好处就是减少每次启动、销毁线程的损耗。 - 为什么使用线程池可以提升效率
少量创建,少量销毁,创建一个线程要分为内核态和用户态,用户态相当于jvm层面,内核太相当于操作系统层面,当我们在jvm层面创建一个线程,就要在操作系统层面创建对应指令,就会消耗大量资源,消耗线程也如此,所以线程池减少了频繁的销毁和创建,用的时候就直接在线程池里面用已经创建多的,从而提升效率。 - 怎么用?
– jdk给我们提供了一组针对不同场景的线程池实例
public static void main(String[] args) {
//1.用来处理大量短时间的任务的线程池,如果池没有可用的线程将创建线程,如果线程空闲60秒将收回并移除缓存
ExecutorService cachedThreadpool= Executors.newCachedThreadPool();
//2.创建一个操作无界队列,线程池大小固定的线程池
ExecutorService fixedThreadpool= Executors.newFixedThreadPool(5);//可以指定线程数量
//3.创建一个操作无界队列,只有一个线程的线程池
ExecutorService singleThreadExecutor= Executors.newSingleThreadExecutor();
//4.创建一个单线程执行器,可以加时间给定时间后执行或者定期执行
ScheduledExecutorService singleThreadScheduledExecutor= Executors.newSingleThreadScheduledExecutor();
//5.创建一个指定大小的线程池,可以加时间给定时间后执行或者定期执行
ScheduledExecutorService scheduledThreadpool= Executors.newScheduledThreadPool(5);
//6.创建一个指定大小(不传参,为当前机器的cpu核数)的线程池,并行处理任务,不保证处理顺序
Executors.newWorkStealingPool();
}
Runtime.getRuntime().availableProcessors()
获取系统的cpu核数
实现一个线程池
- 先构思思路(先描述,再组织)
- 用Runable描述任务
- 组织管理任务可以使用一个队列,可以使用阻塞队列取实现
- 提供一个向队列的添加任务的方法
- 创建多个线程,扫描队列里面的任务,有任务时候执行,没有任务时候等待
public class MyExectorService {
//定义阻塞队列阻止任务
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue(100);
private static Object lock=new Object();
public MyExectorService(int threadNum){
for (int i = 0; i < threadNum; i++){
Thread thread=new Thread(()->{
//不停扫描队列
while (true) {
try {
synchronized (lock){
Runnable runable= queue.take();
runable.run();
}
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}//take()方法会阻塞,直到队列中有任务
});
//启动线程
thread.start();
}
}
/**
* 提交任务到线程池中
* @param runnable 具体的任务
* @throws InterruptedException
*/
public void sumbit(Runnable runnable) throws InterruptedException {
if(runnable==null){
throw new IllegalArgumentException("任务不能为空");
}
//把任务加入队列中
queue.put(runnable);
}
}
class Test1{
public static void main(String[] args) throws InterruptedException {
MyExectorService myExectorService=new MyExectorService(3);
AtomicInteger j= new AtomicInteger();
for (int i = 0; i < 10; i++) {
myExectorService.sumbit(() -> {
System.out.println(Thread.currentThread().getName() + " " + j);
j.getAndIncrement();
});
if(i%3==0){
TimeUnit.SECONDS.sleep(1);
}
}
}
}
创建系统自带的线程池
- 前面jdk提供的线程池比较固定,也就是说我们不能自己定制,但是我们看底层代码时发现,这些线程池都是对ThreadPoolExecutor的封装
- 那我们可以根据ThreadPoolEecutor创建一个自定义线程池
用现实的两个例子去模拟线程工作的原理
周末去吃饭
银行办理业务
- 线程池的拒绝策略详解
- 我们注意一下,3和4是不会抛出异常的,1和2是会抛出异常的,放弃的任务永远都找不回来,所以指定拒绝策略的时候,要关注任务是不是必须要执行,如果必须要执行,就指定“返回调用者”,否则选1,3,4一个即可
public static void main(String[] args) {
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(
2,5,10, TimeUnit.SECONDS
,new LinkedBlockingQueue<>(7)
,new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 100; i++) {
int takeI=i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 执行任务 "+takeI);
}
});
}
}
- 直接拒绝
实际只执行几个后面的都没执行
- 返回给调用者
有一部分代码返回给调用者main执行了
public static void main(String[] args) {
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(
2,5,10, TimeUnit.SECONDS
,new LinkedBlockingQueue<>(7)
,new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 100; i++) {
int takeI=i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 执行任务 "+takeI);
}
});
}
}
- 放弃最早的任务
public static void main(String[] args) {
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(
2,5,10, TimeUnit.SECONDS
,new LinkedBlockingQueue<>(7)
,new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 100; i++) {
int takeI=i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 执行任务 "+takeI);
}
});
}
}
- 放弃最新的任务
public static void main(String[] args) {
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(
2,5,10, TimeUnit.SECONDS
,new LinkedBlockingQueue<>(7)
,new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 100; i++) {
int takeI=i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 执行任务 "+takeI);
}
});
}
}
wait和sleep的区别
1.共同点,让线程休眠一会
2.从实现使用上来说是两种不同的方法
wait是Object类的方法,与锁相关,配合sychronized一起使用,调用wait之后会释放锁
sleep是Thread类的方法,与锁无关
wait可以通过notify和指定直线的方法唤醒,唤醒之后重新竞争锁资源
sleep只能通过超时时间唤醒
- 补充