Java高并发程序设计(二)—— 多线程基础

一、什么是线程

线程是进程内的执行单元。

 通过外部工具可以查看每一个进程所开设的所有的线程。

使用线程间的切换,是因为进程上下文间的切换是一个非常非常重量级的操作,使用多进程去做并行,并发度不可能很高。

线程在进程内更细小的执行单元,所以线程可以被较为广泛的做并发程序的设计。

事实上,如果进程是多线程的,也可是【设置相关性】,把多线程的线程在Windows分配到指定CPU上进行。这样进程中的线程会在指定CPU内核上执行。

在JAVA中线程概念和操作系统线程概念非常类似,事实上,在java中会把在java里面的线程直接映射到操作系统中的线程去,如果在java中比如Windows平台上建立了一个线程,相当于在Windows平台上调用了createThread方法,把线程创建出来,这两者基本上是等价的。

二、线程的基本操作

1. 线程的状态

如果在java中创建了一个线程,线程的基本状态和可用的主要操作如下图。

一个线程刚刚把它新建出来,这个线程的状态是一个new的状态。一个处于new状态的线程,其实它并没有开始工作,它只是一个静态的实体,当你调用了这个实例上的start方法后,这个线程才真正的被启动。被启动后的状态线程开始进入Runnable状态,Runnable状态表示线程可以执行,一切准备就绪,但是Runnable状态并不表示线程一定在CPU上被执行,Runnable状态只是表示在java虚拟机层面,这个线程所有的工作,包括它可能需要某些锁的申请,包括它可能需要某些资源,我都已经调度合理了,都已经到位了,但是如果系统本身很繁忙,CPU还没有办法分出时间片来执行,这个时候也是没有办法执行的。但是Runnable状态对于我们上层来讲,就可以认为它已经在执行了,但它有没有真实在执行,取决于物理CPU的调度。如果一个线程它所有的工作都做完了,它就自然被终结掉,变成terminated状态,表示线程执行任务结束。有时候线程执行过程当中,不可避免的可能会去申请某些锁,比如说去申请某个对象的监视器,比如说我们调用了synchronized,这个时候线程有可能会被阻塞住(blocked),这个时候线程被挂起,挂起的原因是说这个线程进入临界区之前,必须申请到监视器或者说是对象的一把锁,这个监视器可能被其它线程占用,因此导致该线程被挂起。如果线程在执行过程中调用了wait方法,这个线程就会进入等待状态,进入等待状态的线程,它会等候另外一些线程对它进行一个通知(notify),它被通知后,线程可以由等待状态切换到Runnable状态。等待状态有两种,有限等待或无限期等待,有限等待是指等待固定时间,如果还没有线程来通知它,它也会把自己唤醒。

 2. 新建线程

(1)Thread t1 = new Thread();  t1.start();

新建线程直接用new Thread()来新建,得到一个线程的实例t1,调用线程实例t1的start方法,就可以把线程跑起来。

注意区分线程实例的start方法和run方法。

在线程当中有两个可以调用的方法:start()方法和run()方法,start方法开启一个线程,run方法实现与Runnable接口,每一个线程都是Runnable接口的一个实现。

start方法只是在一个新的操作系统的线程上调用run方法,换句话说,调用run方法而不调用start方法,实际上做的事情是一样的,因为start函数就是调用run函数,但是调用run方法并不开启新的线程,而是在调用run方法当前线程中去执行这个操作,而只有使用的start方法,才是在真的开启的新的线程中去做run方法做的这件事情。

run方法的实现:Thread表示一个线程,这个线程实现了Runnable接口,在Runnable接口中只有一个run方法。在Thread类中run方法的实现,它回去调用target的run方法,target就是一个Runnable的接口。

 

这就说明线程的说明有两种方式,第一种方式是直接覆盖run方法,也就是说父类的run方法是怎么实现的呢,就是调用target的run方法,那能不能无视target的呢,因为天生的是把线程新建起来的时候,其实传递给你的Runnable接口target是一个null,第二个参数是一个null,也就是说target本身是不存在的,如果target是不存在的,那我就可以简单的把run方法重载,把我要做的事情写进去,开启start方法,我的start方法就可以在新的线程里面去调用run方法,去做这个事情。

 

第二种方式,不重载run方法,传一个实例给target,这样会自动调用target里面的run方法。如上图,传一个Runnable接口的实例new CreateThread3(),构造一个线程时,我传递一个Runnable接口的实例,这个时候不需要重载run方法,只需要start方法即可,这个run方法会自动调用Runnable接口,执行它的一些行为。

3. 终止线程

(1)Thread.stop()不推荐使用。它会释放所有monitor。

不使用stop方法的原因之一,是太过暴力,例如线程正在做一件事情,然后很暴力的把它stop掉,这个时候无法得知线程执行线程执行到哪个语句上,然后就被stop掉,这个时候线程会释放它所有占用的锁(monitor),但引起的问题是有可能会导致一些多线程的数据的不一致性。

例如:有两条数据,记录1:ID=1,NAME=小明;记录2:ID=2,NAME=小红。

如果有一个对象实例u,表示记录对象user,这个用户有两个字段,一个ID,一个NAME,默认情况下,两个都是null,用0代表。我为了防止对象被多个线程修改,所以在写之前或者读之前,对对象进行加锁,读要进行锁等待,确保写完了再读,这样不会读到一个错误的数据。写也是一样,要进行加锁。现在在写之前,我加了一把锁,然后我对写进行写入,这时候我想把记录1的ID=1,NAME=小明写入,如果已经写入了ID=1,这时候正准备写入NAME=小明,但是这时候很暴力的把该线程stop掉,这样导致的后果是锁会释放掉,释放的后果是,这时候如果有一个线程正在等待读取这个对象的数据,读线程就能够读进来,能够读到这个对象,但是很不幸的是,name还没有被写入,因此会出现这个对象中的ID是记录1的,NAME不是记录1的,数据出现不一致,这时候读线程读到错误数据,并且这种错误很难发现。stop方法不推荐使用。

(2)中断线程

public void Thread.interrupt()  //中断线程

public boolean Thread.isInterrupted()  //判断是否中断

public static boolean Thread.interrupted()  //判断是否被中断,并清除当前中断状态

线程中断的理解:一个线程在跑的过程中,我们可以给它发送一条消息,给它打一个招呼,中断就相当于给线程打了一个招呼,你给线程打了一个招呼后,线程就会把自己的中断标志位给置上,线程就知道有人来寻求过我,需要线程做一些响应,这时候线程就知道你给我打过招呼了,如果线程知道了有人来打过招呼之后,线程就能够给自己做出一些额外的操作,比如说上面的stop案例,直接stop掉太过暴力,使得多线程的线程一致性也很难得到保证,所以可以尝试使用中断的方式停止一个线程。

举例:

有一个线程的run方法如上,里面是一个while(true)死循环,里面是调用了一个Thread.yield()方法,实际上,这里面可能是做一些很复杂的操作,因为很多的后台线程都是一个死循环,然后在里面不停的accept某些东西,然后去做某些操作,想这样的线程我们需要在某些场合需要把他们冷停止掉,如果一个线程根本不是一个死循环写在那边,可能是没有几步就能做完的,我们也不会有太大的需求去中断它,因为它本来就是调用后马上就停止了,我们也不需要特别的把它停止掉。通常我们需要停止的线程,都是以上图的模式存在的,内部有一个很大的循环体,里面做一些很复杂的事情。

如何去停止这个线程呢?假设这个线程是t1,我们对这个线程调用interrupt方法,中断这个线程,如果仅仅是上图的代码,中断时不会有任何响应的。因为t1.interrupt()仅仅是打了个招呼,但是代码中并没有写明如果有人跟你打了招呼你应该怎么办,因为我们没有写明打完招呼后线程该怎么办,这时候线程就会不理这个招呼,所以代码依然做循环,没有任何响应。t1.interrupt()不会对程序产生额外的影响。

但是如果我希望你非常优雅的去终止这个线程,可以对代码进行如下优化。

每次进入循环里后,首先判断当前线程是否已经被中断,如果是,直接break,终止线程。而对这个线程来讲,break外循环等同于这个线程被结束掉,因为整个run方法体就是这个while循环,跳出while循环,线程自然终结掉。这种方式可以让一个线程终止工作,而且是一种非常优雅的方式。如果Thread.yield()处在实际代码中是一些非常复杂的操作,它不会破坏和影响这些操作,因为它总是会等操作完成之后,并且在下一次操作执行之前让这个线程结束,使得数据的安全有一定的保证。

另外的一个比较通用的方法,就是sleep方法,如果我们希望线程不要走不快,希望它慢慢走,这个时候我们可能会调用一个sleep方法,来休眠几秒钟,来等待一个事件,或者等待外部的一些行为。包括sleep在内,大部分需要让线程等待的方法都有一个特点,就是它们会抛出一个interruptedException异常。原因是如果线程一直处于等待状态,比如说等2秒钟,或者更长事件,实际上我是希望我在等待过程中,有人也能停止掉我,或者说有别的线程对我打招呼,我也能做出响应,比如说在上段代码循环体中,先等待5秒钟,再进行别的复杂的操作。假如有一个需求就是,我在等5秒钟的过程中,如果有一个线程中断我,我也能做出响应。因为我在等的过程中,对业务上是没有任何意义的,既然有别的线程让我结束了,或者让我产生某些动作了,那么我等的过程中是应该要对别人打招呼的这个行为做出响应的。所以Sleep方法会抛出一个interrupted异常,sleep外这个try...catch...为什么必要呢,只有我可以响应interruptedException之后,如果我在sleep过程中有人跟我打了招呼,有人中断了我,那我能立即做出响应。如果我在sleep当中有人中断了我,我就能跳到catch当中去,这个时候我就能做出某些动作,比如说我可以把自己结束掉,或者做一些其它的事情。在本例中,跳到catch代码块后,如果希望上面能够检测到中断位被置上了,还需要对自己再调用一次interrupted,原因是如果一个线程抛出了中断异常之后,它的中断标志位被清空,如果再去检测中断标志位的时候,是检测不到的,会觉得该线程没有被中断。换句话,如果省略了对当前线程置上中断标志(Thread.currentThread().interrupt())这句话,在上面判断这个线程是不是被中断了,判断的结果将是该线程没有被中断,所以这句话比较重要。这句话是我再去设置我的中断标志,使得上面这句话能够检测到我已经被中断了。

4. 挂起(suspend)和继续执行(resume)线程

suspend不会释放锁

如果加锁发生在resume之前,死锁会发生。

 suspend指的是把线程挂起,让这个线程暂停一段时间,如果这个线程在运行过程中,我想让这个线程暂停一下,我就可以调用suspend方法,如果把它停止了之后,希望它能够继续往下走,那就可以对它调用resume方法,它就可以继续往下走。这个看似是一个比较方便的操作,但是实际上这两个操作是不推荐使用的。

被弃用的原因具体解释为:

 

现在有一个线程假如说是线程1,它加了一把锁,是对象u的一把锁,意思是说,如果我不释放这把锁,就没有人可以再拿到这把锁,就必须在这把锁外等待,接着线程1就有可能会被挂起。因为我们是多线程的操作,所以对线程1的suspend和对线程1的resume,这两个操作很有可能是发生在系统不同的地方,不同的线程当中,比如这里希望在线程2当中去resume线程1,希望线程1去继续执行下去,但是没有办法保证resume一定发生在suspend之后,事实上因为在不同的线程当中,所以resume有可能发生在suspend之前,如果resume发生在suspend之前,那就相当于你对一个正在执行的线程,去调用resume方法,这个时候对这个线程是没有什么影响的,然后把线程1去挂起了,这时候因为线程2已经调用了resume方法,所以呢,没有人再会来resume这个线程1的执行,所以呢,就会出现线程1的执行有可能永远的被冻结在那边,因为线程1永远被冻结,所以线程1占用的u这个锁也不会被释放,导致其它的线程比如等待对象u的锁的线程3也没有办法继续执行下去,因为会导致一种尴尬的状态。

代码案例:

package com.sishisan.ch2;

public class SimpleTest {
    public static Object st = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");
    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }
        @Override
        public void run() {
            synchronized(st) {
                System.out.println("in" + this.getName());
                Thread.currentThread().suspend();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        t1.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}

 结果:程序无法停止。

分析:

 

 可以看到t2线程卡在suspend0上面,不能继续执行。

5. 等待线程结束(join)和谦让(yield)

(1)yield是一个static静态方法,所以可以使用Thread.yield直接调用。yield是说当前线程希望给与其它线程有机会去争夺CPU,所以会把自己所占用的CPU释放掉,可以使得其它的线程有更多的机会继续的往下执行,而不是被当前线程占用。但是这里要注意的是,当前线程把CPU资源释放掉,并不意味着当前线程释放了自己执行的机会,释放掉的CPU资源,当前线程与其它线程依然会一起参与竞争,也是是说当前线程在下一次竞争的时候还是有可能拿到时间片,还是有可能竞争成功的,只是说当前线程给与其它线程机会来跟我抢而已,这是一个看起来非常罕见的操作,可能用于调试和测试。

 

 (2)join

有时候使用多线程可能存在一些问题,我不知道我开出去的线程它究竟什么时候执行完毕了,因为这里都是一些异步操作,也就是我发送了开启线程的请求,我把Runnable实例传给你,让你去做某些事情,给你start起来,但是你什么时候结束,并没有告诉我。但是往往有时候,我是非常迫切的想知道你什么时候结束的,因为我可能要等待你的某些数据,你的某些信息我才能进行下一步的动作,所以说我会希望等到你结束,我再来做事情,这就是join。

代码案例:

 1 public class JoinMain {
 2     public volatile static int i = 0;
 3     public static class AddThread extends Thread{
 4         @Override
 5         public void run() {
 6             for(i = 0; i < 10000; i++);
 7         }
 8     }
 9     public static void main(String[] args) throws InterruptedException {
10         AddThread add = new AddThread();
11         add.start();
12         add.join();
13         System.out.println(i);
14     }
15 }

 join内部主要调用两个方法,第一个是判断线程是否结束,第二个是wait,wait方法是让当前线程等待在我调用wait方法的对象上面,

 

join的实现,参数为0的情况,首先判断线程是否已经结束,如果线程已经结束,我就没有必要在等,我就出去了,如果我要等的这个线程还没有结束,那我就会在这个线程上面做一个等待,如果我在线程上做了等待,我必然希望有人通知我什么时候等待结束了,如果线程结束了就会调用notifyAll方法去通知等待在它上面的线程。有关notifyAll方法的调用是在JVM中,所以是使用C++的代码去调用notifyAll的。我们需要知道的是,如果这个线程结束了,不工作了,它就会自动调用notifyAll,唤醒所有等待在当前线程实例上的所有线程。所以jdk有个建议,你的应用程序中在这个线程的实例上不要使用wait,notify,notifyAll。原因是,wait,notify,notifyAll是在Object对象上的,因此任意的对象实例都能够有这些方法,所以它给的建议是在线程的对象实例上不要调用这些方法,因为这些方法是会被系统调用的,因为他没有办法保证在线程对象上调用这些方法,所以不一样能如你所愿的去执行

三、守护线程

守护线程:在后台默默地完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。

当一个Java应用程序内,只有守护线程时,Java虚拟机就会自然退出。

守护线程是指在后台默默运行的一些系统服务,也就是它帮助系统做一些后台运维方面的事情,它可能跟业务关系不大。如果main函数结束了,或者main函数开启的一些非守护线程结束了,这个时候我们可以认为java程序已经没有存在的必要了,因为程序存在的目的就是执行我们的非守护线程,非守护线程会执行我们的一些业务逻辑,一些系统的行为等等我们想要它做的事情。而非守护线程只是起到一个守护作用,比如虚拟机的GC线程,如果所有的业务逻辑都不运行了,所有的线程都死光了,那么GC没有任何意义的,所以如果主函数退出,整个虚拟机退出,不会因为还有GC线程的存在而导致整个虚拟机不退出。所以守护线程默默在后台为整个系统提供一种支撑服务,如果系统中所有的非守护线程全部都结束了,那么虚拟机就会退出,虚拟机不会因为有你守护线程的存在而继续执行,

public class DaemonDemo {
    public static class DaemonT extends Thread{
        public void run() {
            while(true) {
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException{
        Thread t = new DaemonT();
//        t.setDaemon(true);
        t.start();
        Thread.sleep(2000);
    }
}

不设置为守护线程 ,程序会一直运行,一直打印I am alive,程序不会退出。

public class DaemonDemo {
    public static class DaemonT extends Thread{
        public void run() {
            while(true) {
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException{
        Thread t = new DaemonT();
t.setDaemon(true); t.start(); Thread.sleep(2000); } }

运行结果:程序打印两次会退出。

设置守护线程必须在程序开启之前就告诉虚拟机这是一个守护线程。

四、线程的优先级

线程是有优先级的,优先级高的线程更有可能抢占到资源,更快执行。

代码案例:

 1 package com.sishisan.ch2;
 2 
 3 public class PriorityDemo {
 4     public static class HighPriority extends Thread{
 5         static int count = 0;
 6         public void run() {
 7             while(true) {
 8                 synchronized(PriorityDemo.class) {
 9                     count++;
10                     if(count>1000) {
11                         System.out.println("HighPriority is complete");
12                         break;
13                     }
14                 }
15             }
16         }
17     }
18     
19     public static class LowPriority extends Thread{
20         static int count = 0;
21         public void run() {
22             while(true) {
23                 synchronized(PriorityDemo.class) {
24                     count++;
25                     if(count>1000) {
26                         System.out.println("LowPriority is complete");
27                         break;
28                     }
29                 }
30             }
31         }
32     }
33     
34     public static void main(String[] args) {
35         Thread high = new HighPriority();
36         LowPriority low = new LowPriority();
37         high.setPriority(Thread.MAX_PRIORITY);
38         low.setPriority(Thread.MIN_PRIORITY);
39         low.start();
40         high.start();
41     }
42 }

运行结果:

设置线程优先级的方式是在线程对象上调用setPriority函数。

高优先级有更高的概率抢占到系统资源,但并不保证一定抢占到系统资源。

五、基本的线程同步操作

在多线程执行过程中,非常重要的就是多线程之间如何去通信。第一个,一个线程如果被挂起了,别的线程如何来唤醒或通知我应该可以继续执行了呢?第二个,如果多线程之间有数据竞争,彼此之间如何协调竞争呢?这里java提供了最基本的方法,一个是Object.wait()和Object.notify()这两个方法,另一个是Synchronized关键字。

1.synchronized

指定加锁对象:对给定对象加锁,进入同步代码前要获得指定对象的锁。

直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

synchronized关键字是java内置的,有关这个关键字的所有实现都是在java虚拟机内部去做的。包括去拿一把锁,把线程挂起,或者在执行过程中挂起之前做一些优化,比如自旋等待等等一系列都是在虚拟机内部实现的。synchronized关键字总的来说有三种用法,第一种是指定加锁的对象,synchronized关键字后面跟上一个对象的实例,然后我就去拿这个实例的monitor,进入synchronized同步块的代码的线程必须拿到给定对象的锁。第二种用法是用synchronized关键字修饰一个实例方法,这就相当于对当前实例对象加锁,也就是说进入我这个方法之前必须拿到我这个当前对象的一把锁。第三种用法是把synchronized关键字用于静态方法,这就相当于对当前这个类实例加锁,也就是在进入静态的synchronized方法之前也要先获得class的锁才能进去。

第一种用法,指定加锁对象:

 1 public class AccountingSync implements Runnable {
 2     
 3     static AccountingSync instance = new AccountingSync();
 4     static int i=0;
 5     
 6     @Override
 7     public void run() {
 8         for(int j = 0; j < 1000; j++) {
 9             synchronized(instance) {
10                 i++;
11             }
12         }
13     }
14     
15     public static void main(String[] args) throws InterruptedException {
16         Thread t1 = new Thread(instance);
17         Thread t2 = new Thread(instance);
18         t1.start();
19         t2.start();
20         t1.join();
21         t2.join();
22         System.out.println();
23     }
24 }

第二种用法,用于实例方法

 1 public class AccountingSync2 implements Runnable {
 2     static AccountingSync2 instance = new AccountingSync2();
 3     static int i = 0 ;
 4     public synchronized void increase() {
 5         i++;
 6     }
 7     @Override
 8     public void run() {
 9         for(int j = 0; j < 1000; j++) {
10             increase();
11         }
12     }
13     public static void main(String[] args) throws InterruptedException {
14         Thread t1 = new Thread(instance);
15         Thread t2 = new Thread(instance);
16         t1.start();
17         t2.start();
18         t1.join();
19         t2.join();
20         System.out.println(i);
21     }
22 }

第二种用法反例如下,因为在两个对象实例上加锁,所以结果一定是错的。

 1 public class AccountingSyncBad implements Runnable {
 2     static int i = 0;
 3     public synchronized void increase() {
 4         i++;
 5     }
 6     @Override
 7     public void run() {
 8         for(int j = 0; j < 1000; j++) {
 9             increase();
10         }
11     }
12     public static void main(String[] args) throws InterruptedException {
13         Thread t1 = new Thread(new AccountingSyncBad());
14         Thread t2 = new Thread(new AccountingSyncBad());
15         t1.start();
16         t2.start();
17         t1.join();
18         t2.join();
19         System.out.println(i);
20     }
21 }

第三种用法,用于静态方法

 1 public class AccountingSyncClass implements Runnable{
 2     static int i = 0;
 3     public static synchronized void increase() {
 4         i++;
 5     }
 6     @Override
 7     public void run() {
 8         for(int j = 0; j < 1000; j++) {
 9             increase();
10         }
11     }
12     public static void main(String[] args) throws InterruptedException {
13         Thread t1 = new Thread(new AccountingSyncClass());
14         Thread t2 = new Thread(new AccountingSyncClass());
15         t1.start();
16         t2.start();
17         t1.join();
18         t2.join();
19         System.out.println(i);
20     }
21 }

2.Object.wait()和Object.notify()

 Object.wait()线程等待在当前对象上,Object.notify()通知等待在这个对象的线程让它从wait这个函数中返回。也就是如果一个线程调用了Object.wait之后它就不会继续往下走了,它就会进入一个wait的状态,它不会再往下走,它会等待,但是wait操作有一个需要注意的地方就是它必须要获得Object对象的监视器,它才能够调用这个方法。

注意:wait方法必须写到synchronized对象锁的里面,并且wait方法会使得当前线程释放Object的锁。

一个等待在对象实例上的线程它会被收到notify的通知。

notify和wait很像的地方是notify也要获得对象实例的监视器。

notify之后不会立即执行,因为wait方法要等到notify释放锁之后,拿到monitor才可以继续执行。

notifyAll:notifyAll与notify的区别是,notify通知等待当前对象上的一个线程。可能object.wait有好多线程都等待再这个对象实例上面,notify只是随机唤醒其中一个,其它的不管。notifyAll会唤醒所有等待在当前对象上的线程。

 1 public class SimpleWNA {
 2     final static Object object = new Object();
 3     public static class T1 extends Thread {
 4         public void run() {
 5             synchronized(object) {
 6                 System.out.println("T1 start!");
 7                 try {
 8                     object.wait();
 9                     Thread.sleep(1000);
10                 } catch (InterruptedException e) {
11                     // TODO Auto-generated catch block
12                     e.printStackTrace();
13                 }
14                 System.out.println("T1 end");
15             }
16         }
17     }
18     public static class T2 extends Thread {
19         public void run() {
20             synchronized(object) {
21                 System.out.println("T2 start! notify all threads");
22                 object.notifyAll();
23                 System.out.println("T2 end!");
24                 try {
25                     Thread.sleep(2000);
26                 } catch (InterruptedException e) {
27                     // TODO Auto-generated catch block
28                     e.printStackTrace();
29                 }
30             }
31         }
32     }
33     public static void main(String[] args) throws InterruptedException {
34         Thread t1 = new T1();
35         Thread t1_1 = new T1();
36         t1.start();
37         t1_1.start();
38         Thread.sleep(1000);
39         Thread t2 = new T2();
40         t2.start();
41     }
42 }

notifyAll条件同notify,所以唤醒所有线程的结果是指所有等待在对象上的线程一起来征用对象实例的监视器,谁抢到了谁先执行,抢不到的等到下一轮抢到了再执行。

猜你喜欢

转载自www.cnblogs.com/upyang/p/12253908.html