Java 多线程学习笔记

如要转载请标明作者zjrodger和出处:http://blog.csdn.net/zjrodger/,谢谢

 

                                                                                多线程笔记目录

·多线程的概念

(·)线程和进程

1.进程的概念

2.线程的概念

3.多线程的特性

4.注意区别并行并发

 

·多线程的创建和启动

(·)实现多线程的综述

1.综述

2.为什么要覆盖run()方法?

3. run()方法和start()方法的区别

(·)继承Thread

(·)实现Runnable接口实现多线程

 

·多线程的生命周期

1.线程的5种状态

(1)新建状态(new)

(2)就绪状态(Runnable)

(3)运行状态(Running)

(4)阻塞状态(Blocked)

(5)死亡状态(Dead)

2.线程运行和线程调度策略

 

·控制线程

(·)join线程

(·)后台线程

       1.何为后台线程

       2.后台线程的特征

       3.与后台线程相关的函数

(·)线程睡眠

(·)线程让步yield

(·)改变线程优先级

 

 

·多线程的安全问题——同步

(·)发生线程安全问题的上下文情景——提出问题和解决问题

1.提出问题

2.解决问题

(·)同步机制的解释

(·)同步机制的实现

同步能够保证线程安全的前提重要

方法一:同步代码块

1.语法格式和说明

2.如何去确定哪些语句需要放在同步代码块中(临界区的确定

3.同步监视器对象的指定

4.代码示例

(1)示例代码1:模拟多线程银行取款机的实现。

方法二:同步方法

1.什么是同步方法

2.构造线程安全的类

3.代码示例

(1)代码示例1:模拟多线程一行取款机的运行。

(·)释放同步监视器的锁定

1.线程释放同步监视器锁定的情况

2.线程不会释放同步监视器锁定的情况

(·)同步锁

(·)线程同步和单例设计模式的结合(重要)

1.饿汉式

    (1)饿汉式的代码写法

    (2)饿汉式的特点:线程安全。

2.懒汉式

    (1)懒汉式的“线程不安全”的写法

    (2)懒汉式的“线程不安全”的原因

    (3)懒汉式能够保障“线程安全”的正确的代码写法

        3.1) 效率比较的写法

        3.2) 效率比较的写法

(·)死锁

 

·线程间通信

一.引子1

引子2

二.线程的等待和唤醒过程








 

·多线程的概念

(·)线程和进程

1. 进程的概念

       几乎所有的OS都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。当一个程序进入到内存中运行时,即变成一个进程。进程是处于运行中的程序,并具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单元。

       进程包含如下三个特性:

(1)独立性。每个进程都有自己私有的地址空间。

(2)动态性。

(3)并发性。

 

2. 线程的概念

(1)线程是轻量级的进程

(2)线程没有自己独立的地址空间

(3)线程是由进程创建的(寄生在进程中)

(4)一个进程可以拥有多个线程,一个线程必须有一个父进程。

在实际应用中,多线程是非常有用的,一个浏览器必须能够同时下载多个图片;一个Web服务器必须能够同时响应多个用户请求;Java虚拟机本省就在后台提供了一个线程来进行垃圾回收;图形界面(GUI)应用也需要启动单独的线程从主机环境收集用户界面事件。总之,只要程序涉及到并发,就离不开多线程编程。

       归纳起来可以这样说:

单个OS可以同时执行多个任务,每个任务就是一个进程;

单个进程可以同时执行多个任务,每个任务就是一个线程。

 

3.多线程的特性

       多线程的特性是随机性,多线程在运行时可以理解为他们在互相抢夺CPU资源,谁抢到谁就被执行。至于每个抢到CPU资源的线程执行多长时间,则由CPU说了算。      

 

4.注意区别并行并发

(1)并行:parallel,指在同一时刻,有多条指令在多个处理器上同时执行。

(2)并发:concurrency,指在同一时刻,只能有一条指令执行,但多个进程指令被快速切换执行,使得在宏观上具有有多个进程同时执行的效果。

 

 

 

 

·多线程的创建和启动

(·)实现多线程的综述

1.综述

在java中,一个类要当成线程来使用有两种方法(任何一个类都可以变成线程来使用):

(1)继承thread类,并重写run方法。

(2)实现Runnable接口,并重写run方法。

假设要将类A变为多线程类,一般情况下,建议使用“实现Runnable接口”的方法来实现多线程类A的。因为Java是单继承的,通过实现Runable接口做可以为A留下一个继承其它类的机会

调用线程类的start()方法使该线程开始执行;Java 虚拟机会去自动调用该线程的run 方法,主线程的代码都存放在main()方法中。

 

2.为什么要覆盖run()方法?

       目的:将自定义的代码存储在run()方法中,让线程运行。Thread父类仅仅是提供了一个存放代码的空间(即,run()的方法体)。

      

3.run()方法和start()方法的区别

如果仅仅调用run()方法,则结果就是程序还是一个单线程程序;只有调用start()方法,程序才能启动多线程。

run()方法就像个容器,仅仅是封装多线程要运行的代码,并不能启动多线程;Java程序不会创建线程,Java程序通过调用start()方法,然后start()方法调用OS的底层代码,去启动多线程。

 

             

 

 

(·)继承Thread

package test1;
 
class ThreadTestextends Thread{ 
   public void run(){
      while(true){
         System.out.println("run()方法,返回当前正在执行的线程对象的引用名:"+Thread.currentThread().getName());
      }
   }
}
 
public class Demo1 {
   public static void main(String[] args) {
      new ThreadTest().start();
      while(true){
         System.out.println("在主类中的main():返回当前正在执行的线程对象的引用名:"+Thread.currentThread().getName());
      }
   }
}

代码分析:

0.API解释:public staticThread currentThread() 返回对当前正在执行的线程对象的引用。

1.线程的建立:想要将一段代码放在一个线程中,可以继承Thread类,或者重新Rnnable接口。

2.线程的启动start()àrun()的本质

class A extends Thread

(1)继承Thread类的那个子类A中并没有start()方法,而start()方法是父类Thread类中的方法,但由于子类A继承了Thread类,所以子类A也可以调用父类中的start()方法。start()使该线程开始执行;Java虚拟机会自动调用该线程的run 方法。

(2)Thread类中的run()方法是空的,子类A继承Thread类时必须重写父类Thread中的run()方法。

3.一个线程类只能启动一次。不管这个线程是通过继承Thread类来实现的,还是实现Runnable接口实现的

4.关于资源共享要注意的问题

使用继承Thread类的子类来创建线程类时,多个线程类之间无法共享线程类的实例变量,即,无法共享资源,因为在每次创建线程类对象时,都要重新创建一个对象。

       这点和实现Runnable接口来创建线程类有很大的不同。

 

 

(·)实现Runnable接口实现多线程

使用Runnable接口创建多线程:

(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效分离,较好的体现了面向对象的设计思想。

(2)可以避免由于Java的单继承特性带来的局限。

(3)当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口类的实例。

(4)事实上,几乎所有多线程应用都可用Runnable接口方式。

(5)Runnable对象仅仅作为Thread对象的target,Runnable实现类中包含的run()方法仅仅作为线程的执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行Runnbale实现类中的run()方法。

(6)通过实现Runnable接口和继承Thread类来实现多线程的区别:

前者可以实现资源共享,多个线程可以同时作用于一个资源,而后者则不可以。

 

注意:实现Runnable接口的类并不是线程类,只有Thread类和继承Thread类的子才是线程类。 

通过实现Runnable接口的方式实现多线程的好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。 

/**
 * 功能:模拟铁路售票系统
 * */
package test1;
//创建一个线程对象时,只能创建一个线程,
//想要实现售票系统,则必须创建一个共有的资源对象,
//再让多个线程共同去使用这个资源对象。
//可以通过让一个类ThreadTest实现Runnable接口去实现这个需求
//实现方法:
//首先,创建一个ThreadTest对象(已经继承了Runnable接口),由于ThreadTest对象中已经有了
//tickets这个属性,所以就可以作为即将创建的多个线程的共有的资源
//其次,通过Thread类生成多个线程,这时每一个线程都可以使用tickets这个共同的资源了。
class ThreadTestimplements Runnable{
   int tickets = 40; 
   public void run(){
      while(true){
         if(tickets>0){
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }           System.out.println(Thread.currentThread().getName()+"正在卖"+tickets+"号票。");
            --tickets;
         }
      }
   }
}
 
public class Demo1{
   public static void main(String[] args){
      ThreadTest tt = new ThreadTest();
      Thread t1 = new Thread(tt);
      t1.start();
      Thread t2 = new Thread(tt);
      t2.start();
      Thread t3 = new Thread(tt);
      t3.start();
      Thread t4 = new Thread(tt);
      t4.start();
   }
}

注意,若运行的结果为:

会出现“票数变为负数”和“同一张票数被打印多次的问题”

这就涉及到了“多线程的同步(多线程的安全,线程死锁问题)”

 

代码分析:

根据上边的代码片段

System.out.println(Thread.currentThread().getName()+"正在卖"+tickets+"号票。");

            --tickets;

下面截取多个线程运行时的某个片段来分析。

当tickets减少到1时,CPU切换到线程1,由于tickets的值满足if(tickets>0)的条件,线程1会先打印出"正在卖1号票。"在线程1还没来得及对tickets进行减减运算前,CPU又切换到线程2,tickets=1的值满足线程2if(tickets>0)的条件,线程2又会打印出"正在卖1号票。"(这就是为什么会出现“同一张票数会被打印多次”的问题)之后线程2再对tickets进行减减运算,此时tickets就变成0,但此时CPU又切换到线程1,由于刚才已经判断过了if()条件,而且线程1已经完成了“打印”的任务,所以线程1剩余的任务就是将tickets的值进行减减运算,运算后,tickets的值就变成了-1,之后线程1再次去判断tickets的条件看是否能满足下次运行的条件,经判断不满足,此时线程1就结束了。

这就是为什么虽然打印条件的判断语句为“if(tickets> 0)才能打印”,但程序依然能够打印,而且“票数会变成负值”的原因。如果线程的数量更多,tickets还可能为-2,或者-3.

 

 

 ·多线程的生命周期

Java 多线程学习笔记_第1张图片

1.线程的5种状态

(1)新建状态(new)

    当程序用new关键字创建了一个线程之后,该线程就处于新建状态,此时,它和其他的Java对象一样,仅仅由Java虚拟机分配内存,并且初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特性,程序也不会执行线程的线程主体。   

(2)就绪状态(Runnable)

    简而言之:此时的线程具备运行资格,但没有CPU执行权,就处于等待状态。

    当某线程对象调用了start()方法后,该线程对象就处于就绪状态,该线程在等待获得CPU资源的执行权,此时,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

    注意点:

    ①启动线程使用start()方法,而不是run()方法。

    ②只能对新建状态的线程使用start()方法,否则,将引发IllegalThreadStateException。

(3)运行状态(Running)

    如果处于就绪状态的线程获得了CPU,则开始执行run()方法的线程执行体,则该线程就处于运行状态。

    当发生如下状况时,线程间进入阻塞状态:

    ①线程调用sleep()方法主动放弃所占用的CPU资源。

    ②线程调用了一个阻塞式的IO方法(即等待用户输入的IO方法),在该方法返回之前,该线程被阻塞。

    ③线程试图获得一个“同步监视器(即同步锁对象)”,但该同步监视器正被其他县城所持有。

    ④线程在等待某个通知(notify)

    ⑤程序调用了线程的suspend()方法,将该线程挂起。但这个方法容易造成死锁,所以应该尽量避免使用该方法。

(4)阻塞状态(Blocked)

    被阻塞的线程在合适的时候就进入就绪状态,注意是就绪状态而不是运行状态。即被阻塞的线程的阻塞被解除后,必须重新等待县城调度器再次调用它。

    当发生如下特定的情况时,可以解除线程的阻塞,让该线程重新进入就绪状态。

    ①调用sleep()方法经过了指定的等待时间。

    ②线程调用的阻塞式IO方法已经返回。

    ③线程成功地获得了试图取得的同步监视器对象。

    ④线程正在等待某个通知时,其他线程发出了一个通知。

    ⑤处于挂起状态(suspend()状态)的线程被调用了resume()方法。

(5)死亡状态(Dead)

    线程会以如下3种方式结束,结束后就处于死亡状态。

    ①run()或call()方法执行完成,线程正常结束。

    ②线程抛出一个未捕获的Exception或者Error。

    ③直接调用该线程的stop()方法来结束线程——该方法容易导致死锁,通常不推荐使用。

    当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动之后,它就拥有和主线程相同的地位,他不会受主线程的影响。     

 

2.线程运行和线程调度策略

       当一个线程开始运行后,他不可能一直处于运行状态(除非它的线程执行时间足够短,瞬间就能够结束了),线程在运行过程中会被中断,目的是使其他线程获得被执行的机会。

线程调度的细节取决于底层平台所采用的策略。对于抢占式调度策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完之后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

       所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机,则可能采用协调式策略,在这样的系统中,只有当一个线程调用了它的sleep()或者yield()方法后才会放弃所占用的资源——也就是必须由该线程主动放弃所占用的资源。

      

 

 

 

·控制线程

(·)join线程

Thread提供了让一个线程等待领一个线程完成的方法——join()方法。当在程序的执行流程中,某个线程A调用另一个线程B的join()方法时,调用线程A将会被阻塞,直到被调用join()方法的线程B执行完毕为止。

join()方法通常由使用线程的程序调用,用来将大问题划分成许多小问题,每个小问题分配一个线程。但所有的问题处理完毕后,在调用主线程进一步操作。比如,pp.join()的作用是把pp线程对象所对应的线程合并到“调用pp.join();语句”的主调线程中。

示例代码

package com.selfpractise;
 
public class ThreadTest extends Thread{
   
    //提供一个有参构造器,用于设置该线程的名字
    public ThreadTest(String name) {
       super(name);
    }
   
    public void run() {     
       for(int i=0; i<100; i++) {
           try {
              Thread.sleep(300);
           } catch (Exception e) {
              e.printStackTrace();
           }         
           System.out.println(Thread.currentThread().getName()+"  "+i);        
       }     
    }
   
    public static void main(String args[]) {
       new ThreadTest("第一个子线程").start();
      
       for(int i=0; i<100; i++) {
           try {
              Thread.sleep(200);
           } catch (Exception e) {
              e.printStackTrace();
           }
           if(i == 20) {
              ThreadTest tt1 = new ThreadTest("被join的为了处理问题一的线程A");
              tt1.start(); 
              try {
                  //main线程调用了tt1线程的join()方法,
                  //main线程必须等tt1线程执行完毕后才会向下执行。
                  tt1.join();  
              } catch (Exception e) {
                  e.printStackTrace();
              }            
           }     
          
           if(i == 50) {
              ThreadTest tt2 = new ThreadTest("被join的为了处理问题二的线程B");
              tt2.start(); 
              try {
                  //main线程调用了tt2线程的join()方法,
                  //main线程必须等tt2线程执行完毕后才会向下执行。
                  tt2.join();  
              } catch (Exception e) {
                  e.printStackTrace();
              }            
           }  
          
           if(i == 80) {
              ThreadTest tt3 = new ThreadTest("被join的为了处理问题三的线程C");
              tt3.start(); 
              try {
                  //main线程调用了tt3线程的join()方法,
                  //main线程必须等tt3线程执行完毕后才会向下执行。
                  tt3.join();  
              } catch (Exception e) {
                  e.printStackTrace();
              }            
           }         
          
           System.out.println(Thread.currentThread().getName()+"   "+i);
       }
    }
}

 


(·)后台线程

1.何为后台线程

       有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(DaemonThread)”,又称为“守护线程”或者“精灵线程”。例如,JVM的垃圾回收机制就是典型的后台线程。

 

2.后台线程的特征

后台线程的特种:如果所有的前台线程都死亡,后台线程就会自动死亡。

       并不是所有的线程默认都是前台线程,有些线程默认就是后台线程——前台线程创建的子线程默认是前台线程;后台线程创建的子线程默认是后台线程。

 

3.与后台线程相关的函数

(1)后台线程的设定

       ①调用Thread对象的“setDaemon(true)”方法,可以将指定的线程设置成后台线程。

       ②要将某线程设置为后台线程必须在启动该线程之前。即setDaemon(true)方法必须在start()方发之前。

(2)判断执行线程是否为后台线程:isDaemon()

 

 

(·)线程睡眠

       sleep()方法让当前线程直接进入到“阻塞状态”,直到经过一定的阻塞时间,才会让当前线程进入到“就绪状态”。

 

(·)线程让步yield

1.线程让步简述

       yield()方法和sleep()方法都可以让当前正在执行的线程暂停执行,但yield()方法不会阻塞当前线程,它只是将当前线程转入到就绪状态,让OS的线程调度器从新调度一次。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理其资源被执行。

 

2.sleep()方法和yield()方法的区别:

从优先级的角度

sleep()方法暂停当前线程后,会给其他线程执行机会,并不理会其他线程的优先级;但yield()方法只会给优先级相同,甚至更高的线程执行机会。

从线程生命周期的角度

sleep()方法让当前线程直接进入到“阻塞状态”,直到经过一定的阻塞时间,才会让当前线程进入到“就绪状态”;而yield()方法不会将线程转入阻塞状态,它只是将当前线程进入到就绪状态,因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理其资源被执行。

sleep()方法比yield()方法具有更好的移植性,通常不建议使用yield()方法来控制并发线程的执行。

 

(·)改变线程优先级

       每个线程默认的优先级都与创建它的父线程的优先级相同,在默认的情况下,main线程通常具有普通优先级(NORM_PRIORITY:5),由main线程创建的子线程也具有普通优先级。

 

 

 

 

·多线程的安全问题——同步

(·)发生线程安全问题的上下文情景——提出问题和解决问题

1.提出问题

       多线程并发,也给我们的编程带来好处,完成更有效率的程序,但是,也会带来线程安全问题。

       对于经典的“买票程序”来说,极可能碰到这样一个意外,就是同一张票号可能被多个售票窗口出售,惹祸的凶手代码就是:

   if(tickets>0){

      System.out.println(Thread.currentThread().getName()+"正在卖"+tickets+"号票。");

      --tickets;

   }

       假如,下载tickets=1,A线程刚执行完语句,还没开始执行语句时,CPU资源就被B线程抢夺去了,B线程执行“if(tickets>0)”,因为在此时num还是1,所以B线程将执行语句,在“--tickets”之后,ticket的数量就变成0,而此时CPU资源就被A线程抢夺去了,接着还没有执行的语句时,由于此时“ticket=0”,在执行完语句后,ticket的数量就变成-1了。

这样就相当于1号票被卖了两次,而且买完票后,ticket的数量也不合理(为负数)。这样多并发就给我们带来了线程安全地麻烦。

 

2.解决问题

       解决问题的关键就是要办证容易出现问题的代码的原子性,所谓原子性就是指:当A线程在执行某段代码的时候,别的线程必须等到A线程将该段代码执行完毕后,才能执行该代码。

       就像人们排队上厕所一样,厕所只有一个,得一个一个得解决。

      

       Java处理线程同步的方法非常简单,只需要在有待同步的代码段用

synchronized(Object){  你要同步的代码  }即可。

       就像某位同志上厕所前先把门关上(上锁),完事后再出来(解锁),那么其他人就可以使用厕所了,如下图所示。

Java 多线程学习笔记_第2张图片

 

 

(·)同步机制的解释

       Java任意类型的对象都有一个标志位(或者对象锁),该标志位具有0,1两种状态,其开始状态为1,当某个线程执行了“synchronized(Object)”语句后,Object对象(也叫做同步监视器)的标志位(或者对象锁)变为0的状态,直到执行完整个synchronized语句中的代码块后,该Object对象的标志位(或者对象锁)又回到了1状态。

       当一个线程A执行到了“synchronized(Object)”语句的时候,先检查Object对象(也叫做同步监视器)的标志位(或者对象锁)。如果为0状态,则表明已经有其他的线程(假设为线程X)正在执“synchronized()”,那么这个线程A将会暂时阻塞,让出CPU资源,进入到线程池中去等待,直到另外的线程X执行完相关的同步代码,线程X并将Object对象(也叫做同步监视器)的标志位变为1状态,此时,线程A的阻塞就会被取消,线程A继续运行,该线程会将Object对象(或同步监视器)的标志位(或者对象锁)变为0状态,防止其他的线程再次进入到相关的同步代码块中。

      

 

(·)同步机制的实现

线程同步的机制:是靠检查“同步监视器对象”的标志位(锁旗标,1可以使用代码块,0不可以使用代码块)来实现的。使用同一个监视器对象的线程之间才能同步

(属于使用同一资源的类型,符合“通过继承Runnable接口来实现多线程”这种情形所能提供的便利。也就是说,想要实现线程的同步,最好是使用“继承Runnable接口的多线程类”)。

 

同步能够保证线程安全的前提重要

①必须要要2个或者2个以上的线程,才能进行同步。

②必须是多个线程使用同一个锁

同步的好处:保证了多线程的安全。

同步的弊端:多个线程的每一个都需要判断锁,消耗资源。

 

编写同步代码的思路:

①明确哪些代码是多线程运行代码。

②明确共享数据。

③明确多线程运行代码中哪些语句是操作共享数据的。

 

方法一:同步代码块

1.语法格式和说明:

synchronized(Object)

你要同步的代码 

}

       上边synchronized后括号中的“Object”就是同步监视器,上边代码的含义:线程开始执行同步代码前,必须先获得对同步监视器的锁定

       任何一个时刻,只能有一个线程可以获得同步监视器的锁定,当同步代码块执行完毕后,该线程会释放对该同步监视器的锁定。

       通过同步代码块的方式,可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻内最多只有一个线程处于临界区内,从而保证了线程的安全。

 

2.如何去确定哪些语句需要放在同步代码块中(临界区的确定

       要达到上述目的,只要确定run()方法中的哪些代码在操作“共享数据”,则那些代码就要被放在同步代码块中。

       这些代码所在的区域也被称为“临界区”——对共享资源进行操作的代码片段。  

 

3.同步监视器对象的指定

       虽然Java允许使用任何对象作为同步监视器,但是,想一下同步监视器对象的作用:阻止两个线程对同一桐乡资源进行并发访问,因此通常推荐使用“可能被可能被并发访问的共享资源”对象充当同步监视器。

使用线程同步比简简单单地使用多线程(没有同步)的处理速度慢得多,这就是因为系统要不停的对同步监视器进行检查,需要额外的开销,这就说明同步是以牺牲程序的性能为代价的

 

4.代码示例

(1)示例代码1:模拟多线程银行取款机的实现。

package com.selfpractise;
 
/**
 * 模拟多线程银行取款机的运行
 */
class BankAccount{
    private StringaccountNO =null;
    private double balance = 0.0f;
   
    public BankAccount(String accountNO,double balance) {
       this.accountNO = accountNO;
       this.balance = balance;
    }
 
    public String getAccountNO() {
       returnaccountNO;
    }
 
    public void setAccountNO(String accountNO) {
       this.accountNO = accountNO;
    }
 
    public double getBalance() {
       returnbalance;
    }
 
    public void setBalance(double balance) {
       this.balance = balance;
    }  
}
 
class ATMWithdrawerextends Thread{
    private BankAccountmyBankAccount =null;
    private double withdrawAmount = 0.0f;
   
    public ATMWithdrawer(String name, BankAccount myBankAccount,double withdrawAmount) {
       super(name);
       this.myBankAccount = myBankAccount;
       this.withdrawAmount = withdrawAmount;
    }
   
    public void run() {
       while(true) {
           //使用myBankAccount作为同步监视器,任何其他线程进入下边同步代码块之前必须首先获得myBankAccount账户的锁定,其他线程无法获得锁,也就无法修改共享资源对象的数据。
           //这种做法符合:“加锁--->修改--->释放锁”的逻辑。
           synchronized(myBankAccount) {
              //如果银行账户余额大于取款数额,则可以取款。
              if(this.myBankAccount.getBalance() >=this.withdrawAmount) {
                  //取款成功后,计算账户余额。
                  this.myBankAccount.setBalance(this.myBankAccount.getBalance()-this.withdrawAmount);
                  System.out.println(Thread.currentThread().getName()+",您的"+this.withdrawAmount+"元取款成功,余额:"+this.myBankAccount.getBalance());
              }
              else {
                  System.out.println("对不起,"+Thread.currentThread().getName()+",当前账户余额为:¥"+this.myBankAccount.getBalance()+",您的余额不足。");
                  break;
              }            
              try {
                  Thread.sleep(280);
              } catch (Exception e) {
                  e.printStackTrace();
              }
           }
       }
    }
}
 
public class MoneyWithdrawThreadTest extends Thread{
    public static void main(String[] args) {
       BankAccount myBankAccount = new BankAccount("123456789",1500);
       ATMWithdrawer atm01 = new ATMWithdrawer("张三",myBankAccount,50);
       ATMWithdrawer atm02 = new ATMWithdrawer("李四",myBankAccount,80);
      
       atm01.start();
       atm02.start();
       try {
           atm01.join();
           atm02.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }     
      
       System.out.println("经过多人取款,银行账户\""+myBankAccount.getAccountNO()+"\"上的余额为"+myBankAccount.getBalance());
    }
}

 

示例代码2

package test2;
 
public class Demo1 {
   public static void main(String[] args) {
      // TODO Auto-generated method stub
      SaleTickets t = new SaleTickets();
      Thread t1 = new Thread(t);
      t1.start();
      Thread t2 = new Thread(t);
      t2.start();
      Thread t3 = new Thread(t);
      t3.start();
      Thread t4 = new Thread(t);
      t4.start();
   }
}
 
//其作用为“看门狗”,相当于对象锁,其中没有任何代码
//因为java中任意类型的对象都可以作为一个“对象锁”
class GuardDog{  }
 
class SaleTicketsimplements Runnable{
   int all_tickets =20;
 
   GuardDog my_guard_dog =new GuardDog();
  
   public void run(){
      while(true){
 
         synchronized(my_guard_dog){
            if(this.all_tickets > 0){
               System.out.println(Thread.currentThread().getName()+"线程正在卖第"+this.all_tickets+"张票。");
                --this.all_tickets;
            }else{
                System.out.println("对不起,票已经卖完了。");
                break;
            }
         }
         //在整个synchronized代码块执行完毕后,就然线程休息1秒
         try{
            Thread.sleep(1000);
         }catch(Exception e){
            e.printStackTrace();
         }
      }
   }
}

 

示例代码3.  synchornized同步监视器中的标志位(0 或者 1)

package com.selfpractise;
/**
 * 功能:模拟铁路售票系统
 * */
public class Demo1{
    public static void main(String[] args){
       ThreadTest tt = new ThreadTest();
       Thread t1 = new Thread(tt);
       t1.start();
       Thread t2 = new Thread(tt);
       t2.start();
       Thread t3 = new Thread(tt);
       t3.start();
    }
}
 
//创建一个线程对象时,只能创建一个线程,
//想要实现售票系统,则必须创建一个共有的资源对象,
//再让多个线程共同去使用之一个资源对象、
//可以通过让一个类ThreadTest实现Runnable接口去实现这个需求
//实现方法:
//首先,创建一个ThreadTest对象(已经继承了Runnable接口),由于ThreadTest对象中已经有了
//tickets这个属性,所以就可以作为即将创建的多个线程的共有的资源
//其次,通过Thread类生成多个线程,这时每一个线程都可以使用tickets这个共同的资源了。
class ThreadTestimplements Runnable{
   
    int tickets = 40;
 
    //注意:要想线程同步,这个String是必写的,他作为“监视器或对象锁的持有者”被放在synchronized后边
    //而且,这个String对象不能被写在run()方法里边。
    //为什么???
    //因为如果String监视器对象要是被写在run()方法里边,那么这4个
    //线程在调用run()方法时就都会产生自己的"监视器对象"
    //而每一个监视器对象在各自的线程进入到synchronized语句时,
    //它的锁旗标将都会是1,这样就会到底是那个线程正在执行代码,而其还没有执行完
    //这样就不能达到“线程同步”的效果。
    //切记切记切记::如果多个线程要同步,那么他们使用的监视器对象必须是同一个对象
    String stringObject = new String("");
   
    public void run(){
       while(true){        
           //注意不能将String对象的创建写在run()方法里边
           //String str = new String("");
 
           //(这里的str是一个对象,也是一个监视器,这个监视器中有一个标志位)
           //同步代码块
synchronized(stringObject){
              if(tickets> 0 ){
                  try {
                     Thread.sleep(300);
                  } catch (InterruptedException e) {
                     e.printStackTrace();
                  }                 System.out.println(Thread.currentThread().getName()+"正在卖"+tickets+"号票。");
                  --tickets;
              }
           }
       }
    }
}
代码分析:

线程1执行到synchornized代码块时,就会将stringObject对象(即同步监视器)的标志位(或者对象锁)设置为0,在运行过程中,如果当线程1的时间片用完后,CPU就会切换到另一个线程2线程2想进入到同一块代码块时,首先会检查stringObject对象(即同步监视器)的标志位(或者对象锁),在读取到其中的值为0后,线程2就会停止执行,将CPU资源让给其他线程,从而处于在线程等待池中处于等待状态,而其他线程也会遇到相似的情况,这样最终CPU的资源就会给线程1线程1就会接着执行刚才没有完成的任务,执行完任务后,就会将stringObject对象的标志位(或者对象锁)设置为1这样当CPU切换到下一个线程时,下一个线程确定stringObject对象的标志位(或者对象锁),当该标志位为1时,该线程就能够执行代码块,在该进程进入到代码块之前也会将stringObject对象的标志位(或者对象锁)设置为0这样就能保证下一个进程的执行结果不被其它线程影响,从而保证了线程的“原子性”。

 

方法二:同步方法

1.什么是同步方法

       同步方法就是用“synchronized”关键字来修饰一个方法,这样的方法被称为同步方法。

对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器为this,也就是该对象本身。

       函数需要被对象所调用,那么函数都有一个所属对象的引用,就是this,所以同步函数使用的锁就是this。

 

2.构造线程安全的类

通过同步方法可以非常方便的实现线程安全的类,线程安全的类具有以下特征:

①改类的对象可以被多个线程安全的访问。

②每个线程调用该对象的任意方法之后,都将得到正确的结果。

③每个线程调用该对象的任意方法之后,该对象的状态依然保持合理的状态。

       类可以分为“可变类”和“不可变类”,其中“不可变类”总是线程安全的,因为它的对象状态不可改变;但可变类需要额外的方法来保证其线程安全。

      

       可变类的线程安全是以降低程序性能为代价的,为了减少线程安全所带来的负面影响,程序可采用如下策略:

(1)不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源的方法进行同步。例如,银行账户BankAccount类中的账号属性accountNO就无须同步,只对取款方法进行同步。

(2)如果可变类有两种运行环境:单线程环境和多线程环境,则应该为可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中,可以使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

JDK所提供的StringBuilder,StringBuffer就是为了照顾单线程环境和多线程环境所提供的类。在单线程环境下,应该使用StringBuilder来保证性能;在多线程环境下,应该使用StringBuffer来保证线程安全。

 

3.静态函数的锁是Class对象。

如果同步函数被静态static修饰符修饰后,则此方法的同步锁就是class对象。应为静态方法中不可能定义this。

静态进入内存时,内存中没有本类的实例对象,但内存中一定有该类对应的字节码文件对象,即“类名.class”,该对象的类型是Class。

【参考资料】

毕向东Java基础视频 第11天-13-多线程(多线程-静态同步函数的锁是Class对象)

 

4.代码示例

(1)代码示例1:模拟多线程一行取款机的运行。

package com.selfpractise;
 
/**
 * 模拟多线程银行取款机的运行
 * 利用同步方法的机制,提供一个线程安全的BankAccount类,该类有自己的取款方法,以保证线程安全。
 */
class BankAccount{
    private StringaccountNO =null;
    private double balance = 0.0f;
   
    public BankAccount(String accountNO,double balance) {
       this.accountNO = accountNO;
       this.balance = balance;
    }
 
    public String getAccountNO() {
       returnaccountNO;
    }
 
    public double getBalance() {
       returnbalance;
    }
   
    //提供一个线程安全地withdrawMoney方法来完成取钱操作。
    public synchronized void withdrawMoney(double withdrawAmount) {
       if(this.balance >= withdrawAmount) {
           this.balance =this.balance - withdrawAmount;
           System.out.println(Thread.currentThread().getName()+"取款"+withdrawAmount+"元成功,您的账余额:¥"+this.balance);
       }else {
           System.out.println(Thread.currentThread().getName()+"您的账户余额不足。");
       }
       try {
           Thread.sleep(130);
       } catch (Exception e) {
           e.printStackTrace();
       }
    }
}
 
class ATMWithdrawerextends Thread{
    private BankAccountmyBankAccount =null;
    private double withdrawAmount = 0.0f;
   
    public ATMWithdrawer(String name, BankAccount myBankAccount,double withdrawAmount) {
       super(name);
       this.myBankAccount = myBankAccount;
       this.withdrawAmount = withdrawAmount;
    }
   
    public void run() {
       //若银行账户的余额 >取款数目,则一直让线程执行取款操作。
       while(this.myBankAccount.getBalance() >=withdrawAmount) {
           this.myBankAccount.withdrawMoney(this.withdrawAmount);
       }
    }
}
 
public class MoneyWithdrawThreadTest extends Thread{
    public static void main(String[] args) { 
       BankAccount myBankAccount = new BankAccount("123456789", 1000);
       ATMWithdrawer atm01 = new ATMWithdrawer("王五", myBankAccount, 50);
       ATMWithdrawer atm02 = new ATMWithdrawer("陈六", myBankAccount, 30);
      
       atm01.start();
       atm02.start();
       try {
           atm01.join();
           atm02.join();
       } catch (Exception e) {
           e.printStackTrace();
       }     
       System.out.println("经过多人取款,银行账户"+myBankAccount.getAccountNO()+"上的余额为"+myBankAccount.getBalance());
    }
}

 

(2)代码示例2

class ThreadTestimplementsRunnable{
   int tickets = 40; 
   public void run(){
      while(true){
         this.saleTickets();
      }
   }
   public synchronized void saleTickets(){
      if(this.tickets > 0){
         System.out.println(Thread.currentThread().getName()+"正在卖"+tickets+"号票。");
         --tickets;
      }
   }
}
 
public class Demo1{
   public static void main(String[] args){
      ThreadTest tt = new ThreadTest();
      Thread t1 = new Thread(tt);
      t1.start();
      Thread t2 = new Thread(tt);
      t2.start();
      Thread t3 = new Thread(tt);
      t3.start();
   }
}

代码分析:

1.当一个线程进入到了标有synchronized关键字的方法时,他就得到了监视器,并且锁定了监视器,只有这个方法执行完后,其他的线程才可以执行这个方法。

2.代码块和函数之间的同步:

根据线程同步的机制 {是靠检查“监视器对象”的标志位(锁旗标,1可以使用代码块,0不可以使用代码块)来实现的},只要让代码块和函数使用同一个“监视器对象”,就可以实现代码块和函数之间的同步。

 

 

(·)释放同步监视器的锁定

1.线程释放同步监视器锁定的情况

2.线程不会释放同步监视器锁定的情况

       ①线程在执行同步代码块或同步方法时,程序调用Thread.sleep()方法,Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器的锁定。

②线程在执行同步代码块,其他线程执行了该线程的suspend()方法来将该线程挂起,该线程不会释放同步监视器的锁定。当然,我们应当尽量避免使用suspend()和resume()方法来控制线程。

 


 

(·)线程同步和单例设计模式的结合(重要)

1.饿汉式

(1)饿汉式的代码写法

//单例模式————"饿汉式"的代码规范
class SingleHungry{
 //此处有关键字final修饰aSingleInstanceObjRef,则表明次对象不可修改,
 //虽然不要final也可以实现效果,但这样做更加规范。
 private static final SingleHungry aSingleInstanceObjRef = new Single();
 private SingleHungry() {}
 public SingleHungry getSingle() {
  return aSingleInstanceObjRef;
 }
}
(2)饿汉式的特点:线程安全。

 

2.懒汉式

(1)懒汉式的“线程不安全”的写法

//单例模式————"懒汉式"的代码
//下列代码是“线程不安全”的写法。
class SingleLanHan{
 //此处不能有关键字final,否则aSingleInstanceObjRef引用就无法被复制了,
 //因为它被定为以为一个常量null
 private static SingleLanHan aSingleInstanceObjRef = null;
 private SingleLanHan() {}
 public SingleLanHan getSingleLanHan(){
  if(aSingleInstanceObjRef == null) {
   aSingleInstanceObjRef = new SingleLanHan();
   return aSingleInstanceObjRef;
  }
 }
}
上述“懒汉式”的单例模式代码存在“ 线程不安全”的隐患。

    

(2)懒汉式的“线程不安全”的原因

public SingleLanHan getSingleLanHan(){
  if(aSingleInstanceObjRef == null) {
    //--->此时线程A进入该代码处①
    //--->此时线程B进入该代码处②
    //--->此时线程C进入该代码处③
   aSingleInstanceObjRef = new SingleLanHan();
   return aSingleInstanceObjRef;
  }
 } 

代码存在隐患的原因:若线程A在执行到 时,线程A还没有创建单例对象,CPU就被切换到线程B,并且当线程B执行到代码 时,CPU又被切换到线程C,如此,就会导致程序创建多个SingleLanHan对象,这个就不符合单例模式的要求。


(3)懒汉式能够保障“线程安全”的正确的代码写法

3.1) 效率比较的写法

//单例模式————"懒汉式"的代码
//下列代码是“线程不安全”的写法。
class SingleLanHan{
 //此处不能有关键字final,否则aSingleInstanceObjRef引用就无法被复制了,
 //因为它被定为以为一个常量null
 private static SingleLanHan aSingleInstanceObjRef = null;
 private SingleLanHan() {}
 public synchronized SingleLanHan getSingleLanHan(){
  if(aSingleInstanceObjRef == null) {
   aSingleInstanceObjRef = new SingleLanHan();
   return aSingleInstanceObjRef;
  }
 }
}
效率低的原因分析:

当每一个线程访问SingleLanHan()方法时,都需要判断是否有其他的线程在占用着锁,因此效率比较低。

 

3.2) 效率比较的写法

//单例模式————"懒汉式"的代码规范
class SingleLanHan{
 //此处不能有关键字final,否则aSingleInstanceObjRef引用就无法被复制了,
 //因为它被定为以为一个常量null
 private static SingleLanHan aSingleInstanceObjRef = null;
 private SingleLanHan() {}
 public SingleLanHan getSingleLanHan(){
    //--->此时线程C进入该代码处③
  if(aSingleInstanceObjRef == null) {
    //--->此时线程B进入该代码处②
   synchronized(SingleLanHan.class) {
    //--->此时线程A进入该代码处①
    if(aSingleInstanceObjRef == null) {
     aSingleInstanceObjRef = new SingleLanHan();
     return aSingleInstanceObjRef;
    }
   }
  }else {
   return aSingleInstanceObjRef;
  }
 }
}
效率高的原因分析:减少多个线程判断锁的次数。

  线程A在执行到时,先拿到了锁,此时CPU切换给线程B,但由于此时线程A没有释放锁,因此线程B不能进入同步代码块。

  之后CPU切换给线程A线程A成功的创建了单例对象,并且释放锁,之后线程B通过判断所锁的琐旗标拿到了锁,但此时“aSingleInstanceObjRef不为 null”,因此就不能再次创建单例对象。

  若此时线程C进入到代码处,则会对最外层的代码“if(aSingleInstanceObjRef == null)”挡在外边,因此,线程C就不用判断锁的琐旗标了。

  同理,若有其他的线程D,线程E,线程F..........等等多个线程,这些线程都不用判断锁的琐旗标,这样就减少多个线程判断锁的次数,从而提高了程序的效率。

  总而言之,用双重判断的方式来减少多个线程判断锁的次数,从而提高了效率。

 

(·)死锁

       当两个对象相互等待对方释放同步监视器时,就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁。所以多线程编程时应采取措施避免死锁。一旦出现死锁,整个程序既不会发生任何异常,也不会有任何提示,只是所有线程都处于阻塞状态,无法继续。

 

·线程间通信

一.引子1


       不同的线程之间对CPU资源是抢占式的。

       在“卖火车票”的实验中,同一个线程thread-2有可能打印多次,这也就说明了,线程thread-2被连续执行了多次,这是因为线程thread-2多次成功抢占CPU资源。

 

重要

虽然线程的同步可以保证每个线程在执行时的“原子性”,但如何保证线程A已经写入的而且还没来得及被输出的数据会由于同一个线程A的再次执行而不被覆盖呢(同一个线程A之所以会再次执行,可能是因为此时线程A的时间片还没有用完)??

这就涉及到了“线程间通信”

引子2

package com.selfpractise;
/**
 * 演示可能出现的错误:
 * 1.同一个线程有可能抢占资源,也就是同一个线程在执行完一次代码后,
 *   再次抢占CPU资源,再次执行代码。
 * */
class Buffer{
    String name;
    String gender;
}
 
class Producerimplements Runnable{
    Buffer buffer = null;   
   
    public Producer(Buffer b){
       this.buffer = b;
    }  
    public void run() {
       int i=0;
       while(true){
          
           //如果多个线程共享同一个对象资源,那么这个对象就可以作为“监视器对象”
           //在本例中,Producer和Customer多线程类都可以共同访问buffer对象
           //因此buffer对象是Producer和Customer多线程类的“监视器对象”
           synchronized(buffer){
              if(i == 0){
                  buffer.name ="苏林";
                  System.out.println("生产者已经将name="+buffer.name+"放入缓冲区中了。");
                  buffer.gender ="男";
                  System.out.println("生产者已经将gender="+buffer.gender+"放入缓冲区中了。");
              }
              else{
                  buffer.name ="厂长";
                  System.out.println("生产者已经将name="+buffer.name+"放入缓冲区中了。");
                  buffer.gender ="女";
                  System.out.println("生产者已经将gender="+buffer.gender+"放入缓冲区中了。");
              }
              i = ((++i)%2);
           }         
       }
    }
}
 
class Customerimplements Runnable{   
    Buffer buffer = null;   
    public Customer(Buffer b){
       this.buffer = b;
    }
   
    public void run(){
       while(true){
           synchronized(buffer){
              System.out.print("在消费者中取数据:"+buffer.name+"   ");
              System.out.println(buffer.gender);
           }
       }
    }
}
 
public class InternalThreadCommunication {
    public static void main(String[] args) {
       Buffer buffer = new Buffer();
       Producer p = new Producer(buffer);
       Customer c = new Customer(buffer);
      
       Thread t_p = new Thread(p);
       t_p.start();
      
       Thread t_c = new Thread(c);
       t_c.start();
    }
}

1.预期的理想状态:

生产者线程产生一组新的数据1后就被消费者线程读取,生产者线程产生一组新的数据2后就被消费者线程读取…………生产者线程产生一组新的数据n后就被消费者线程读取。

2.但上述程序的实际结果显示:

生产者线程产生一组新的数据1后,由于他的时间片还没有用完,CPU就无法切换到消费者线程,去读取生产者线程刚刚已经产生的并且已经存储到缓冲区中的数据1,这样生产者线程就会再次生产数据2,并将原来原本应该被消费者线程读取的数据1给覆盖掉了。如果此时生产者线程的时间片还没有被用完的话,那么它就会继续产生数据3数据4数据5……数据n

       而当生产者线程的时间片用完后,CPU切换到消费者线程去读取已经产生的数据,就有可能会出现下述情况:消费者线程其实是想读取数据1的,但由于生产者线程的时间片一直都没有用完,CPU就无法切换到消费者线程,从而导致数据1被后来的数据2数据3数据4……数据n等所覆盖,这样消费者线程就无法得到它真正想要的数据。

 

解决方法:线程间通信。

 

重要

虽然线程的同步可以保证每个线程在执行时的“原子性”,但如何保证线程A已经写入的而且还没来得及被输出的数据会由于同一个线程A的再次执行而不被覆盖呢(同一个线程A之所以会再次执行,可能是因为此时线程A的时间片还没有用完)??

这就涉及到了“线程间通信”

 

 

二.线程的等待和唤醒过程

 Java 多线程学习笔记_第3张图片

示例代码:

/**
 * 功能:线程间通信机制的演示
 * 不同点:将存储数据的成员变量和操纵数据的方法(写入和输出)
 *         都封装到一个类中去,这样是利用面向对象的思想,
 *         实现了代码的简介,美观,以及安全。
 * */
package test2;
 
public class InterThreadCommunicationUpgratedVersion {
 
   public static void main(String[] args) {
     
      Buffer buffer = new Buffer();
      Producer p = new Producer(buffer);
      Customer c = new Customer(buffer);
     
      Thread t_p = new Thread(p);
      t_p.start();
      Thread t_c = new Thread(c);
      t_c.start();
   }
}
 
class Buffer{
   private Stringname;
   private Stringgender;
   private boolean isFull = false;//判断存储缓冲区的缓冲区是否为满
  
   //写入信息的方法
   public synchronized void setInfo(String name, String gender){
      if(this.isFull ==false){
         this.name = name;
         this.gender = gender;
         System.out.println("生产者线程产生信息————姓名:"+this.name+" 性别:"+this.gender);
        
         this.isFull =true;
         this.notify();
        
      }else if(this.isFull ==true){
         try {
            this.wait();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
  
   //读取信息的方法
   public synchronized void getInfo(){
      if(this.isFull ==false){
         try {
            this.wait();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }else if(this.isFull ==true){
         System.out.println("消费者线程获取信息————姓名:"+this.name+" 性别:"+this.gender);
        
         this.isFull =false;
         this.notify();
      }
   }
}
 
 
class Producerimplements Runnable{
  
   Buffer buffer = null;
  
   public Producer(Buffer buffer){
      this.buffer = buffer;
   }
  
   public void run(){
      int i=0;
      while(true){
         if(i == 0){
           
            buffer.setInfo("张健","男");
         }else{
            buffer.setInfo("陈阳","女");
         }
         i = ((++i)%2);
      }
   }
}
 
class Customerimplements Runnable{
  
   Buffer buffer = null;
  
   public Customer(Buffer buffer){
      this.buffer = buffer;
   }
  
   public void run(){
      while(true){
         buffer.getInfo();
      }
   }
}

 

·毕向东主讲线程间通信

(·)思考

思考1:wait(),notify(),notifyAll()这些用来操作线程的方法为什么定义在了Object类中?

 Java 多线程学习笔记_第4张图片

 

思考2:调用什么对象的wait(),notify(),notifyAll()的方法来实现线程通讯?

       都是调用同步监视器(锁)的wait(),notify()方法,因为要对同步监视器(锁)的线程进行操作。等待的线程都会被存放同步监视器的在“线程池”中。

等待唤醒机制
wait():让线程等待。将线程存储到一个线程池中。
notify():唤醒被等待的线程。通常都唤醒线程池中的第一个。让被唤醒的线程处于临时阻塞状态。
notifyAll(): 唤醒所有的等待线程。将线程池中的所有线程都唤醒,让它们从冻结状体转到临时阻塞状态.
这三个方法用于操作线程,可是定义在了Object类中,为什么呢?
因为,这三个方法在使用时,都需要定义在同步中,要明确这些方法所操作的线程所属于锁。
简单说。在A锁被wait的线程,只能被A锁的notify方法唤醒。
所以必须要表示waitnotify方法所属的锁对象,而锁对象可以是任意的对象。
可以被任意的对象调用的方法肯定定义在Object类中。
注意:等待唤醒机制,通常都用在同步中,因为需要锁的支持。
而且必须要明确waitnotify 所作用的锁对象。

 

思考3:Wait()和sleep()方法的区别

wait()释放资源,释放锁。

sleep()释放资源,不释放锁。

 




你可能感兴趣的:(J2SE)