1、 HashMap是如何实现的?
1. 特性:线程不安全,key、value都可以为null,元素无序
2. 数据结构---数组链表(拉链法)
3. 再说put和get过程(其中跟equals和hashcode方法相关),举例说明只重写其中一个方法会导致什么问题?
put---key为null的存入散列桶0中,key不为null的,根据key的hashcode并对hashcode进行均匀散列,得到一个hash值,相当于数据的索引,然后开始顺序遍历这个链表,如果有等于key值的Entry,就更新Entry的值,并返回旧值;否则遍历完链表之后,进行新Entry的添加,添加之前需要判断容器的size是否已经大于等于threshold,如果是,进行扩容(一般为2*table.length,当table大小等于2^30时,就不进行扩容了),否则,new一个Entry并采用头插法添加到hash值对应的散列桶中。
get---key为null,从散列桶0中,查找Entry的key为null进行返回,不存在的话,返回null;key不为null,用同样的hash函数获得hash值,遍历hash值对应的散列桶,查找该链表中是否存在相应的key,如果存在就返回对应的value,否则返回null
equals和hashcode---两个对象equals之后返回true,应该要返回相同的hashcode;如果重写了equals,但是没有重写hashcode,会导致插入的两个对象实际上是相同的,但是还是能够插入成功。
4. 扩容机制,加上1.8的改动---红黑树
对于1.8,当散列桶中的元素超过TREEIFY_THRESHOLD,就采用红黑树的结构,将元素插入到树中
5. 转化和退化
https://juejin.im/entry/5839ad0661ff4b007ec7cc7a
2、 HashTable与HashMap的区别?
1. HashTable是一个比较古老的类,继承于Dictionary类,HashMap继承于AbstractMap类;
2. HashTable是线程安全的,HashMap是线程非安全的
3. HashTable不允许key和value为null,HashMap允许
4. 需要同步的话可以选择HashTable
3、 ThreadLocal
http://www.cnblogs.com/dolphin0520/p/3920407.html
深入分析 ThreadLocal 内存泄漏问题
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
4、 HashMap和ConcurrentHashMap的区别,ConcurrentHashMap如何实现的?
http://blog.csdn.net/yan_wenliang/article/details/51029372
ConcurrentHashMap并不是可以完全替换Hashtable的,因为ConcurrentHashMap的get、clear函数是弱一致的(后面会说到),而Hashtable是强一致的。
成员变量:
final int segmentMask;//作为查找segments的掩码,前几个bit用来选择segment
final int segmentShift;
final Segment
基础方法分为这么几种:
1、段内加锁的:put,putIfAbsent,remove,replace等
2、不加锁的:get,containsKey等
3、整个数据结构加锁的:size,containsValue等
PUT方法:
该方法也是在持有段锁的情况下执行的,首先判断是否需要rehash,需要就先rehash,扩容都是针对单个段的,也就是单个段的数据数量大于设定的量的时候会触发扩容。接着找是否存在同样一个key的结点,如果存在就直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,这时一定要修改modCount和count的值,同样修改count的值一定要放在最后一步。put方法调用了rehash方法,reash方法实现得也很精巧,主要利用了table的大小为2^n,和HashMap的扩容基本一样,这里就不介绍了。
还有一个叫putIfAbsent(K key, V value)的函数,这个函数的实现和put几乎一模一样,作用是,如果map中不存在这个key,那么插入这个数据,如果存在这个key,那么不覆盖原来的value,也就是不插入了。
GET方法:
先根据segmentShift,segmentMask寻找segment的下标,然后寻找table的下标,遍历单链表查找key是否存在,如果对应的value不为空,返回,否则返回加锁读的结果。
为什么value会为null?
当put的key不存在,那么将在链表表头插入一个数据,那么将new HashEntry赋值给tab[index]是否能立刻对执行get的线程可见呢,我们知道每次put完之后都要更新一个count变量(写),而每次get数据的时候,在最一开始都要读一个count变量(读),而且发现这个count是volatile的,而对同一个volatile变量,有volatile写 happens-before volatile读,所以如果写发生在读之前,那么newHashEntry赋值给tab[index]是对get线程可见的,但是如果写没有发生在读之前,就无法保证new HashEntry赋值给tab[index]要先于get函数的getFirst(hash),也就是说,如果某个Segment实例中的put将一个Entry加入到了table中,在未执行count赋值操作之前有另一个线程执行了同一个Segment实例中的get,来获取这个刚加入的Entry中的value,那么是有可能取不到的,这也就是get的弱一致性。但是什么时候会找到key但是读到的value是null呢,仔细看下put操作的语句:tab[index]= new HashEntry(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,举个例子就是这条语句有可能先执行对key赋值,再执行对tab[index]的赋值,最后对value赋值,如果在对tab和key都赋值但是对value还没赋值的情况下的get就是一个空值。
clear方法:
因为没有全局的锁,在清除完一个segments之后,正在清理下一个segments的时候,已经清理过的segments可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此,clear方法是弱一致的。
clear方法:
如果我们要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean方法全部锁住,但是这种做法显然非常低效,因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clear方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。size()的实现还有一点需要注意,必须要先segments[i].count,才能segments[i].modCount,这是因为segment[i].count是对volatile变量的访问,接下来segments[i].modCount才能得到几乎最新的值,这里和get方法的方式是一样的,也是一个volatile写 happens-before volatile读的问题。
上面18行代码,抛出了一个问题,就是为什么会再算一遍,上面说只需要比较modCount不变不就可以了么?但是仔细分析,就会发现,在13行14行代码那里,计算完count之后,计算modCount之前有可能count的值又变了,那么18行的代码主要是解决这个问题。
5、 Iterator迭代器与Iterable的关系
6、 重排序和中断
http://www.jianshu.com/p/26fe9e362806
7、 CAS
8、 静态变量与非静态变量的区别
9、 Java的异常种类,有什么区别,如何自定义异常,异常与error的区别?
异常---异常是发生在程序执行过程中阻碍程序正常执行的错误事件。比如:用户输入错误数据、硬件故障、网络阻塞等都会导致出现异常。只要在Java语句执行中产生了异常,一个异常对象就会被创建,JRE就会试图寻找异常处理程序来处理异常。如果有合适的异常处理程序,异常对象就会被异常处理程序接管,否则,将引发运行环境异常,JRE终止程序执行。
异常的种类---所有异常,都继承自java.lang.Throwable类。Throwable有两个直接子类,Error类和Exception类。Exception异常可分为运行时异常(RuntimeException)和检查异常(CheckedExceptions)两种。
checked exceptions:其必须被try{}catch语句块所捕获,或者在方法签名里通过throws子句声明.受检查的异常必须在编译时被捕捉处理, Java编译器要进行检查.
Runtime Exception:需要程序员自己分析代码决定是否捕获和处理,比如空指针,被0除...
Error表示编译时和系统错误,通常不能预期和恢复,比如硬件故障、JVM崩溃、内存不足等。
区别:被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的FileNotFoundException。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的NullPointerException。
自定义异常:
importjava.io.IOException;
publicclassMyException extendsIOException {
privatestaticfinallongserialVersionUID = 4664456874499611218L;
privateString errorCode="Unknown_Exception";
publicMyException(String message, String errorCode){ super(message); this.errorCode=errorCode; }
publicString getErrorCode(){ returnthis.errorCode; }
} |
10、 Java NIO 和Java IO的区别?为什么要用JavaNIO?NIO的原理?
11、 Select poll epoll
select, poll, epoll是linux支持的几种IO方案,都能提供IOmultiplexing的解决方案,本文介绍他们的工作原理和优缺点。
l select和poll的原理基本相同:
注册待侦听的fd(这里的fd创建时最好使用非阻塞)
每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回
返回结果中包括已就绪和未就绪的fd
l select:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看,32位机默认是1024个,64位机默认是2048。
2、 对fd进行扫描时是线性扫描,即采用轮询(polling)的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
l poll:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
相比select,poll解决了单个进程能够打开的fd数量有限制这个问题:select受限于FD_SIZE的限制(一般为1024),如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。
select和poll共同的一个缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd数量增多而线性增大。
l epoll:
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
l epoll解决了select、poll的缺点:
基于事件驱动的方式,避免了每次都要把所有fd都扫描一遍。
epoll_wait只返回就绪的fd。
epoll使用mmap内存映射技术避免了内存复制的开销。
epoll的fd数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024
l epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销
12、 高并发的网络模型(reactor、proactor)?
13、 线程同步的方式,为什么要用同步?
同步方式(5种):1. 同步方法(synchronized) 2. 同步代码块 3. 重入锁(ReentrantLock)
多线程的执行顺序是不确定的,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确和不一致性,因此加入同步锁以避免在多个线程并发修改数据。
14、 Synchronized关键字的作用以及使用方法?
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码
2. 修饰一个实例方法,被修饰的方法称为同步方法,其作用的范围是整个方法,默认锁住的对象是this(调用方法的对象);其他线程可以调用该类的非synchronized方法
3. 修改一个静态的方法,其作用的范围是整个静态方法,锁住的对象是这个类的class对象(调用方法的对象),其他线程不能访问该类其他的synchronized静态方法;
15、 线程池
线程池的优势:1. 降低资源消耗(重用) 2.提高响应速度(不用等待创建) 3. 提高线程的可管理性(不能无限制创建线程,太多的线程会导致负载过高,cpu忙于线程间切换)
线程池的弊端:1. 如果连接大多是长连接,会导致线程池中的线程一直被占用;当有新的用户请求连接时,由于没有空闲线程来处理,导致客户端连接失败,影响用户体验。线程池适合有大量短连接请求的应用
线程池的生命周期:ExecutorService的生命周期有3种状态:运行、关闭、终止
16、 Sleep和yield的区别
l sleep()使当前线程进入阻塞状态,yield()只是使当前线程重新回到可执行状态。
l Sleep会给低优先级的线程机会,yield只会给优先级大于等于自己的线程机会。
l Sleep和yield都不会释放锁
17、 抽象类和接口的区别
抽象类---是用来描述抽象行为,子类的通用特性,比如Animal,我们不知道Animal具体有会有什么样的行为,只有具体的动物类,如Dog,Cat才有具体的行为,才能够被实例化。抽象类是实现多态的一种机制,它可以包含具体方法(有具体实现的方法),也可以包含抽象方法,而继承它的子类必须实现这些方法。
1. 抽象类不能被实例化,但可以有构造函数
2. 抽象方法必须由子类进行重写
3. 只要包含一个抽象方法的类,就必须定义为抽象类,不管是否还包含其他方法
4. 抽象类中可以包含具体的方法,也可以不包含抽象方法
5. 抽象类可以包含普通成员变量、静态成员变量,其访问类型可以任意
6. 子类中的抽象方法不能与父类的抽象方法同名
7. abstract不能与private、static、final或native并列修饰同一个方法
接口---在Java当中是通过关键字interface来实现,不能被实例化,接口是用来建立类与类之间的协议,它的提供的只是一种形式,而没有具体的实现。实现类必须实现接口的全部方法,Java不允许多重继承(即不能有多个父类,只能有一个),但可以实现多个接口。
1. 接口中不能有构造方法
2. 接口的所有方法自动被声明为public abstract,如果使用protected、private,会导致编译错误。
3. 接口可以定义"成员变量",而且会自动转为public final static,即常量,而且必须被显式初始化。
4. 接口中的所有方法都是抽象方法,不能包含实现的方法,也不能包含静态方法
5. 实现接口的非抽象类必须实现接口的所有方法,而抽象类不需要
6. 不能使用new来实现化接口,但可以声明一个接口变量,它必须引用一个实现该接口的类的对象,可以使用instanceOf来判断一个类是否实现了某个接口,如if (object instanceOf ClassName){doSth()};
什么时候使用抽象类和接口?
1. 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类。
2. 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
3. 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。
18、 进程与线程的区别
1. 进程是具有一定独立功能的程序、它是系统进行资源分配和调度的一个独立单位。
2. 线程是进程的一个实体,是CPU调度和分派的基本单位,线程自己基本上不拥有系统资源。在运行时,只是暂用一些计数器、寄存器和栈。
3. 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
4. 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
5. 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
19、 线程间通信方式
l 共享变量
l wait/notify机制
l Lock/Condition机制
l 管道(1.创建管道输出流PipedOutputStreampos和管道输入流PipedInputStream pis 2.将pos和pis匹配,pos.connect(pis); 3.将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了)
20、 进程间通信方式
管道(Pipe) :管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
命名管道(named pipe) :命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系 进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
信号(Signal) :信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号语义函数signal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
共享内存(mappedmemory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
信号量(semaphore) :主要作为进程间以及同一进程不同线程之间的同步手段。
套接字(Socket) :更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:linux和System V的变种都支持套接字。
21、 三级调度的联系
作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程并送入就绪队列;进程调度 从就绪队列中选择一个进程将其改为运行状态,把CPU调度给它。系统将暂时不能运行的进程挂起,当内存空间宽松时,由内存(中级)调度选择具备运行条件的进程,将其唤醒。
l 作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起
l 作业调度次数少,中级调度次数略多,进程调度频率最高
l 进程调度是最基本的,不可或缺
22、 进程调度方式和调度算法
l 非剥夺调度方式
又称非抢占方式,是指当一个进程在处理器上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞状态时,才把处理器分配给更为重要或紧迫的进程。
实现简单、系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。
l 剥夺调度方式
又称抢占方式,是指当一个进程正在处理器上执行时,若有某个更为重要或紧迫的进程需要使用处理器,则立即暂停正在执行的进程,将处理器分配给这个更为重要或紧迫的进程。
遵循原则:优先权原则、短进程原则和时间片原则等。
5.1 先来先服务(FCFS)调度算法
FCFS调度算法的特点是算法简单,但效率低;有利于 长作业 但对短作业不利;有利于 CPU繁忙型作业而不利于I/O繁忙型作业。
5.2 短作业优先(SJF)调度算法
SJF缺点:
对长作业不利。由于调度程序总是优先调度那些(即使是后进来的)短作业,将导致长作业长期不被调度。
SJF调度算法的平均等待时间、平均周转时间最少。
5.3 优先级调度算法
又称优先权调度算法。既可以用于 作业调度 ,也可用于 进程调度 。
该算法中的优先级用于描述作业运行的紧迫程度 。
根据新的更高优先级进程能否抢占正在执行的进程,又可将该调度算法分为非剥夺式优先级调度算法和 剥夺式优先级调度算法
5.4 高响应比优先调度算法
响应比Rp = (等待时间+要求服务时间)/要求服务时间
根据公式可知:
如果作业的等待时间相同,则要求服务的时间越短,其响应比越高,有利于短作业
当要求服务时间相同时,作业的响应比由其等待时间决定,等待时间越长,其响应比越高,因而它实现的是先来先服务。
对于长作业,作业的响应比可以随等待时间的增加而提高,当其等待时间足够长时,其响应比便可升到很高,从而也可获得处理器。
5.5 时间片轮转调度算法
主要适用于分时系统 。
系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,并仅能运行一个时间片,如100ms。
5.6 多级反馈队列调度算法
在优先权越高的队列中,每个进程的运行时间片就越小。
当一个新进程进入内存后,首先将它放入第1级队列的末尾,按FCFS原则排队等待调度。
当轮到该进程执行时,如它能在该时间片内 完成 ,便可 准备撤离系统 ;如 未完成 ,调度程序便将该进程转入第2级队列的末尾,再同样地按FCFS原则调度执行…如此下去,当一个长进程从第1级队列一次降到第n级队列后,在第n级队列中便采用时间片轮转 的方式运行。
仅当第1级队列为空时,调度程序才调度第2级队列中的进程运行,如此往下…。如果处理器正在执行第i级队列中的某进程时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此新进程将抢占 正在运行进程的处理器,即由调度程序把正在运行的进程放回到第i级队列的末尾,把处理器分配给新到带更高优先级的进程。
23、 三线程打印ABC
public class MyThreadPrinter2 implements Runnable { private String name; private Object prev; private Object self; private MyThreadPrinter2(String name, Object prev, Object self) { this.name = name; this.prev = prev; this.self = self; } @Override public void run() { int count = 10; while (count > 0) { synchronized (prev) { synchronized (self) { System.out.print(name); count--; self.notify(); } try { prev.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws Exception { Object a = new Object(); Object b = new Object(); Object c = new Object(); MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a); MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b); MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c); new Thread(pa).start(); Thread.sleep(100); //确保按顺序A、B、C执行 new Thread(pb).start(); Thread.sleep(100); new Thread(pc).start(); Thread.sleep(100); } } |
24、 AQS的理解
http://www.cnblogs.com/waterystone/p/4920797.html
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,并唤醒被阻塞的线程,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,并唤醒被阻塞的线程,失败则返回false。