一、什么情况下会产生线程安全问题?
同时满足以下两个条件时:
1,多个线程在操作共享的数据。
2,操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
public class TicketDemo implements Runnable {
private int tickets = 100;//成员变量
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "....sale:...." + tickets--);
}
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
Thread t1 = new Thread(ticketDemo);
Thread t2 = new Thread(ticketDemo);
Thread t3 = new Thread(ticketDemo);
Thread t4 = new Thread(ticketDemo);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
二、简述一下面对线程安全问题的解决思路
将多个操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
》》在java中,用同步代码块就可以解决这个问题。
同步代码块的格式:
synchronized(对象){
需要被同步的代码 ;
}
这个对象一般称为同步锁。
同步的前提:同步中必须有多 个线程并使用同一个锁。
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
public class TicketDemo implements Runnable {
private int tickets = 100;
Object obj = new Object();
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "....sale...." + tickets--);
}
}
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
Thread t1 = new Thread(ticketDemo);
Thread t2 = new Thread(ticketDemo);
Thread t3 = new Thread(ticketDemo);
Thread t4 = new Thread(ticketDemo);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
三、Java 实现线程安全的三种方式汇总
1、方式一:同步代码块
public class ThreadSynchronizedSecurity {
static int tickets = 50;
class SellTickets implements Runnable {
@Override
public void run() {
// 同步代码块
while (tickets > 0) {
synchronized (this.getClass()) {
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "--->售出第: " + tickets + " 票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName() + "--->售票结束!");
}
}
}
}
public static void main(String[] args) {
SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();
Thread thread1 = new Thread(sell, "1号窗口");
Thread thread2 = new Thread(sell, "2号窗口");
Thread thread3 = new Thread(sell, "3号窗口");
Thread thread4 = new Thread(sell, "4号窗口");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
2、方式二:同步函数
public class ThreadSynchroniazedMethodSecurity {
static int tickets = 50;
class SellTickets implements Runnable {
@Override
public void run() {
//同步方法
while (tickets > 0) {
synMethod();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName() + "--->售票结束");
}
}
}
synchronized void synMethod() {
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "---->售出第 " + tickets + " 票 ");
tickets--;
}
}
public static void main(String[] args) {
SellTickets sell = new ThreadSynchroniazedMethodSecurity().new SellTickets();
Thread thread1 = new Thread(sell, "1号窗口");
Thread thread2 = new Thread(sell, "2号窗口");
Thread thread3 = new Thread(sell, "3号窗口");
Thread thread4 = new Thread(sell, "4号窗口");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
3、方式三:Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块
- 总结:
- 由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;
- 而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。
- 另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,
- 此时ReentrantLock是个不错的方案。
public class ThreadLockSecurity {
static int tickets = 10;
class SellTickets implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() {
// Lock锁机制
while(tickets > 0) {
try {
lock.lock();
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
} catch (Exception e1) {
e1.printStackTrace();
}finally {
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName()+"--->售票结束!");
}
}
}
public static void main(String[] args) {
SellTickets sell = new ThreadLockSecurity().new SellTickets();
Thread thread1 = new Thread(sell, "1号窗口");
Thread thread2 = new Thread(sell, "2号窗口");
Thread thread3 = new Thread(sell, "3号窗口");
Thread thread4 = new Thread(sell, "4号窗口");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
四、同步函数和同步代码块的区别是什么?
(1)同步代码块可以绑定任意对象,而同步函数只能绑定该类对象this,static同步函数只能绑定字节码类名.class
(2)如果多个线程使用同一个锁的话,那么两者均可以使用,如果存在多个锁的话,只能使用同步代码块
》》建议使用同步代码块。
class Ticket implements Runnable {
private static int num = 100;
boolean flag = true;
public void run() {
if (flag)
while (true) {
synchronized (Ticket.class)//(this.getClass())//(this)同步代码块
{
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ".....obj...." + num--);
}
}
}
else
while (true)
this.show();
}
//同步函数
public static synchronized void show(){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ".....function...." + num--);
}
}
}
class StaticSynFunctionLockDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
t.flag = false;
t2.start();
}
}
【1】同步代码块的锁可以是任意对象
Object obj = new Object();
synchronized(obj){
if(num > 0){
try{Thread.sleep(10);} catch(InterruptedException e){};
System.out.println(Thread.currentThread().getName() + "......obj......" + num--);
}
}
【2】同步函数的锁是固定的的this(同步函数是同步代码块的简写形式)
public synchronized void show(){
if(num > 0){
try{Thread.sleep(10);} catch(InterruptedException e){};
System.out.println(Thread.currentThread().getName() + "......function......" + num--);
}
}
【3】静态同步函数使用的锁是该函数所属字节码文件对象,可以使用this.getClass()方法获取,也可以用当前 类名.class表示。
静态方法中没有this对象,静态方法没有所属对象。而任何类在加载的时候都会有字节码类对象,用getClass获取。
//静态方法中没有this对象,静态方法没有所属对象
//任何类在加载的时候都会有字节码类对象,用getClass获取。
public static synchronized void show(){
if(num > 0){
try{Thread.sleep(10);} catch(InterruptedException e){};
System.out.println(Thread.currentThread().getName() + "......function......" + num--);
}
}
五、死锁的常见情况?
同步嵌套时,两个线程你拿了我的锁,我拿了你的锁,都不释放,造成死锁。
》》编写一个简单的死锁demo
public class DeadLockTest implements Runnable{
private boolean flag;
DeadLockTest(boolean flag) {
this.flag = flag;
}
public void run() {
if (flag) {
while (true)
synchronized (MyLock.locka) {//同步代码块
System.out.println(Thread.currentThread().getName() + "..get locka....");
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName() + "..get lockb....");
}
}
} else {
while (true)
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName() + "..get lockb....");
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName() + "..get locka....");
}
}
}
}
static class MyLock {
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
public static void main(String[] args) {
DeadLockTest a = new DeadLockTest(true);
DeadLockTest b = new DeadLockTest(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}
六、单例设计模式中的线程安全问题(饿汉式与懒汉式)
【1】饿汉式:
在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
class Single{
private static final Single s = new Single();
private Single(){}
public static Single getInstance(){
return s;
}
}
【2】懒汉式:
*加入同步是为了解决多线程安全问题。
*加入双重判断不用每次都判断是否上锁,是为了解决效率问题。
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s==null){
synchronized(Single.class){
if(s==null){
s = new Single();
}
}
}
return s;
}
}
七、Java多线程中锁的理解与使用?
1、简介:锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等 ) 。
2、Java锁的种类
* 公平锁/非公平锁
* 可重入锁
* 独享锁/共享锁
* 互斥锁/读写锁
* 乐观锁/悲观锁
* 分段锁
* 偏向锁/轻量级锁/重量级锁
* 自旋锁
备注:这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计
【1】公平锁/非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
【2】可重入锁
可重入锁又名递归锁:是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。对于Java ReentrantLock而言, 其名字是Re entrant Lock即是重新进入锁。对于synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
//下面代码就是一个可重入锁的一个特点,
//如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
【3】独享锁/共享锁
独享锁:是指该锁一次只能被一个线程所持有;
共享锁:是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于synchronized而言,当然是独享锁。
【4】互斥锁/读写锁
上面说到的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock;读写锁在Java中的具体实现就是ReadWriteLock。
【5】乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。
备注:CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
JAVA对CAS的支持:
在JDK1.5中新增java.util.concurrent包就是建立在CAS之上的。相对于对于synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以java.util.concurrent在性能上有了很大的提升。
【6】分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
【7】偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
【8】自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
八、从源码角度彻底理解ReentrantLock(重入锁)-AQS
1、AQS (AbstractQueuedSynchronizer)抽象队列同步器原理:
》》public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
【CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。】
2、AQS原理图:
》》AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
》》状态信息通过procted类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3、AQS 对资源的共享方式
AQS定义两种资源共享方式
【1】 Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
* 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
* 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
【2】Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock。
4、AQS底层使用了模板方法模式
》》同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
5、应用
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
九、谈谈 synchronized和ReentrantLock 的区别?
【1】区别一:API层面
syschronized使用
synchronized即可修饰方法,也可以修饰代码块。
synchronized修饰方法时,如下所示:
//synchronized修饰一个方法时,这个方法叫同步方法
public synchronized void test(){
//方法体
}
synchronized修饰代码块时,包涵两部分:锁对象的引用和这个锁保护的代码块。如下所示:
synchronized (0bject){
//括号中表示需要锁的对象
//线程执行时会对object上锁
}
ReentrantLock使用
public class test(){
private Lock lock=new ReentrantLock();
public void testMethod(){
try{
lock.lock();
//code
}
finally{
lock.unlock();
}
}
}
【2】区别二:等待可中断
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可等待特性对处理执行时间非常长的同步块很有帮助。
具体说来,假如业务代码中有两个线程,Thread1 Thread2.假设Thread1获取了对象object的锁,Thread2将等待Thread1释放object的锁。
使用synchronized。如果Thread1不释放,Thread2将一直等待,不能被中断。synchronized也可以说是Java提供的原子性内置锁机制。内部锁扮演了互斥锁(mutual exclusion lock ,mutex)的角色,一个线程引用锁的时候,别的线程阻塞等待。
使用ReentrantLock。如果Thread1不释放,Thread2等待了很长时间以后,可以中断等待,转而去做别的事情。
【3】区别三:公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;而非公平锁则不能保证这一点。非公平锁在锁被释放时,任何一个等待锁的线程都有机会获得锁。 synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。
【4】区别四:绑定多个条件
ReentrantLock可以同时绑定多个Condition对象,只需多次调用newCondition方法即可。
synchronized中,锁对象的wait和notify() 或notifyAll()方法可以实现一个隐含的条件。但如果要和杜宇一个的条件关联的时候就不得不额外添加一个锁。
【5】区别五:性能
JDK 1.5中,synchronized还有很大的优化余地。JDK 1.6 中加入了很多针对锁的优化措施,synchronized与ReentrantLock性能方面基本持平。虚拟机在未来的改进中更偏向于原生的synchronized。
【补充】
①Java中每个对象都有一个锁(lock)或者叫做监视器(monitor).
②ReentrantLock和synchronized持有的对象监视器不同。
③如果synchronized方法是static的,那么当线程方法改方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在对象所对应的class对象,因为java中不管一个类有多少对象,这些对象会应对唯一一个Class对象。因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,是顺序执行的,即一个线程先执行,另一个才开始。
④synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行synchronized方法,synchronized快则是一种细粒度的并发控制。只会将 块中代码同步,位于方法内,synchronized块之外的是可以被多个线程同时访问的。
⑤synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令,操作对象均为锁的计数器。
⑥相同点:都是可重入的。可重入值的是同一个线程多次试图获取它所占的锁,请求会成功。当释放的时候,直到冲入次数清零,锁才释放。
十、为什么wait(),notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?
》》Object.wait(),Object.notify(),Object.notifyAll()都是Object的方法,换句话说,就是每个类里面都有这些方法。
Object.wait():释放当前对象锁,并进入阻塞队列
Object.notify():唤醒当前对象阻塞队列里的任一线程(并不保证唤醒哪一个)
Object.notifyAll():唤醒当前对象阻塞队列里的所有线程
》》为什么这三个方法要与synchronized一起使用呢?解释这个问题之前,我们先要了解几个知识点
每一个对象都有一个与之对应的监视器
每一个监视器里面都有一个该对象的锁和一个等待队列和一个同步队列
wait()方法的语义有两个,一是释放当前对象锁,另一个是进入阻塞队列,可以看到,这些操作都是与监视器相关的,当然要指定一个监视器才能完成这个操作了
notify()方法也是一样的,用来唤醒一个线程,你要去唤醒,首先你得知道他在哪儿,所以必须先找到该对象,也就是获取该对象的锁,当获取到该对象的锁之后,才能去该对象的对应的等待队列去唤醒一个线程。值得注意的是,只有当执行唤醒工作的线程离开同步块,即释放锁之后,被唤醒线程才能去竞争锁。
notifyAll()方法和notify()一样,只不过是唤醒等待队列中的所有线程
因wait()而导致阻塞的线程是放在阻塞队列中的,因竞争失败导致的阻塞是放在同步队列中的,notify()/notifyAll()实质上是把阻塞队列中的线程放到同步队列中去
为了便于理解,你可以把线程想象成一个个列车,对象想象成车站,每一个车站每一次能跑一班车,这样理解起来就比较容易了。
值得提的一点是,synchronized是一个非公平的锁,如果竞争激烈的话,可能导致某些线程一直得不到执行。