黑马程序员——007——面向对象④(包)、多线程①(创建线程、同步、死锁)

来源:互联网 发布:网络女主播的真实照片 编辑:程序博客网 时间:2024/05/18 02:32
——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-

包package
—我们会发现,我们会写出一些类,类的内容不一样,但是类的名字重复,又由于一个文件夹只能存在唯一名字的文件,因此就需要对类文件进行分类管理;
—包就是这样一种机制,能够给类提供多层命名空间;
—注意:
——①写在程序的代码第一行;
——②使用了包的话,类名的全程是“包名.类名”;
———好比深圳有101号公路,北京也有,我们说要去101号公路的时候是不知道要去哪里的101公路的,因此前面加上地名,北京的101号公路。
——③包也是一种封装形式
——————————————————————
当我们在cmd下使用命令“javac”编译的时候,我们可以简单输入“javac”查看一下参数:
这里写图片描述
可以看到我们可以使用“javac -d 指定一个目录a 需要编译的java文件路径p”命令来对一个java文件进行编译,这时候我们就能够将java源代码文件和class字节码文件分开存放了,保证源代码不泄露。
看下面的示例:
————————————————————————————
有一个java源代码文件“Demo7_1.java”:
—————————————————

package halohoop; //首行使用了包,//代表这个类完整的类名应该是"halohoop.Demo7_1"class Demo7_1{        public static void main(String[] args)        {                System.out.println("Hello World!");        }}

—————————————————
我们使用cmd定位到该java文件所在目录下,然后用如下命令进行编译:
—javac -d . Demo7_1.java回车
——(“javac -d 指定一个目录a 需要编译的java文件路径p”)
注意1:命令这个的目录a使用了一个点“.”来代替,表示当前目录;
—另外,若使用两点“..”则表示当前目录的上一级目录;
——javac -d .. Demo7_1.java回车
—或者我们可以使用当前目录中一个确实存在的文件夹名;
——javac -d abc Demo7_1.java回车
—又或者我们可以使用确实存在的一个目录绝地路径;
——javac -d d:\abc Demo7_1.java回车
注意2:需要编译的java文件路径p,若是在该文件的当前目录,则只需要一个文件名字即可,否则需要绝对路径。
————————————————————————————
加了包的类在编译完成之后,运行和前面就有所区别的,需要写完整的类名,也就是“包名.类名”,如下示例:
————————————————————————————
javac -d . Demo7_1.java回车
—这时候当前目录会自动生成一个halohoop的文件夹,并且里面存放了“Demo7_1.class”字节码文件,这时候使用命令:
java halohoop.Demo7_1.java回车
—程序就运行了。
————————————————————————————
包与包之间类的访问
示例:
————————————————————————————
文件:Demo7_2.java
package halohoop1;
class Demo7_2
{
public static void main(String[] args)
{
new Demo7_3();
}
}
—————————————
文件:Demo7_3.java
package halohoop2;
class Demo7_3
{
public static void main(String[] args)
{
new Demo7_2();
}
}
—————————————
使用编译命令:
—①javac -d . Demo7_2.java回车;
——出现错误:
这里写图片描述
—②javac -d . Demo7_3.java回车;
——出现错误:
这里写图片描述
————————————————————————————
我们将构造函数变成完整的类名后在编译还是有错误:
这里写图片描述
这里写图片描述
这是由于我们需要给java虚拟机指定一个寻找class文件的位置,这个位置我们可以在系统属性的环境变量中配置:
这里写图片描述
当然每修改一次类的存放位置就来这里修改的话显然是非常麻烦的,因此我们可以使用“set classpath”命令来帮助我们修改;
我们在打开的CMD窗口下输入:
—set classpath=包halohoop1和包halohoop2所在目录绝对路径,回车
——这个命令执行之后,并不会修改我们系统中的环境变变量,而仅仅是修改了当前CMD窗口使用的环境变量;
—这时候我们在使用编译命令:
—①javac -d . Demo7_2.java回车;
——出现错误:
这里写图片描述
—②javac -d . Demo7_3.java回车;
——出现错误:
这里写图片描述
—这时候虽然类找不到的问题解决了,但是引来第二个问题,这就涉及到接下来讲的内容了:
—包与包之间进行访问,被访问的包中的类以及类中的成员,需要被public修饰才能被其他包中的类访问。
——我们将两个类的定义,class的前面加上public修饰符,再重复编译,注意这时候我们需要使用的命令要变一下了:
———javac -d . *.java
———编译当前目录下的所有java文件;
———运行:
———java halohoop1.Demo7_2回车
———java halohoop2.Demo7_3回车
————————————————————————————
因此我们能够总结:
—①包与包之间进行访问,被访问的包中的类以及类中的成员,需要被public修饰才能被其他包中的类访问;
—另外
—②不同包中的子类还可以直接访问父类中被protected权限修饰的成员;
—包与包之间可以使用的权限只有两种,public protected;
————————————————————————————
类与类中成员的权限问题:
这里写图片描述
覆盖方法要求权限只能>=父类的权限。
—也就是原来如果是default,那么子类的该方法可以为protected或者public;
注意:
—①一个Java文件不能出现两个或以上的共有类或者接口,如下图:
这里写图片描述
—②包中还可以有包(子包):
这里写图片描述
————————————————————————————
import关键字
—import关键则能够帮助我们简化类名的书写:
—使用:
——import 包名;写在package的下面class的上面;
——import 包名1.包名2..包名n*;表示导入的是包名1.包名2..包名n中的所有的类;
—注意1:
——import 包名1.*;这样只是导入包名1中的所有类,并不包括其中的包及其中的包的所有类;
——开发中不建议使用*号,用哪个导入哪个,为了不占内存空间;
—如果使用开发工具如Eclipse,我们只需要按ctrl + shift + O;那么类就能够全部导入了;
—注意2:
——当导入不同包,出现同名类,这时候我们需要加包名,精确调用哪个包的那个同名类;
——因此,我们建立包名的时候为了不要重复,建议使用url来定义,因为url和域名一样是唯一的,如:
———net.halohoop
———com.halohoop
————————————————————————————
————————————————————————————
多线程
—首先了解进程的概念,进程就是一个正在进行中的程序;
—每一个进程执行,都有一个执行的顺序,该顺序是一个执行路径(或者叫一个控制单元);
线程才是进程中一个独立的控制单元;线程在控制着进程的执行;
—因此,一个进程至少有一个线程;
—JVM启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行;而且这个线程运行的代码存在于main方法中;该线程称之为主线程;
—(了解)我们前面写过的程序都是单线程,这样理解其实没有错的,但是如果要深究其实虚拟机启动就是多线程;因为虚拟机在执行java程序的同时,还会发生垃圾回收,所以虚拟机中肯定至少有两个线程在同时执行;
—当有多条执行路径的程序,我们就成这个程序为多线程程序;
——举例:下载软件,每个线程都从文件的不同位置开始获取数据,以提高整体下载速度;
————————————————————————————
线程在Java被Thread类进行描述,创建新线程的方法之一就是生命Thread的子类,重写其run方法,如下示例:
————————————————————————————

class Demo7_4{        public static void main(String[] args)        {                //新建一个线程,匿名内部类的方式                new Thread()                {                        //新线程的任务                        public void run()                        {                                for(int i=0;i<100;i++)                                        System.out.println("new thread "+i);                        }                }.start(); //调用start方法开启线程,运行run里面的方法                //主线程的任务                for(int i=0;i<100;i++)                        System.out.println("main thread "+i);        }}

————————————————————————————
这个程序现在就有了两条执行路径
—形象的说,两个线程,一个主线程、一个新线程都在抢夺CPU执行权,某一时刻谁抢到就运行谁。
——真实的情况是:对于单核的CPU来说,CPU想执行谁就执行谁的。
——我们如果把程序运行多几次,可以发现每一次的运行结果都不尽相同的。在某一个时刻,只能有一个程序在运行(多核CPU除外),CPU在做这个飞快的切换,以达到看上去是同时运行的效果;这就是多线程的一个特性——随机性;
————————————————————————————
run方法和start方法特点
—Thread类用于描述线程,该类就定义了一个功能,用于存储程要运行的代码,该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码;而我们主线程要运行的代码就存储在main方法中。
—总结一下创建新线程并且运行的步骤:
——①定义类继承Thread(或者使用匿名内部类的形式);
——②复写Thread类中的run方法;
———目的:将自定义代码存储在run方法,让线程运行;
——③调用线程的start方法,该方法两个作用:启动线程,并且调用run方法;
———要注意不能够重复调用start方法,否则会抛出IllegalThreadStateException异常,线程状态异常。
—再看下面的实例:
————————————————————————————

class Demo7_5{        public static void main(String[] args)        {                //第一种调用run方法的方式                Thread nt1 = new NewThread("线程对象1");                nt1.run();//不用start方法,直接使用对象调用run方法                //第二种调用run方法的方式                Thread nt2 = new NewThread("线程对象2");                nt2.start();//用start方法,通过start,才能创建新线程调用run方法;        }}class NewThread extends Thread{        private String name;        public NewThread(String name)        {                this.name = name;        }        public void run()        {                for(int i=0;i<100;i++)                        System.out.println(name+"-"+i);        }}

————————————————————————————
执行结果:
这里写图片描述
结果没有和前一个实例代码那样两个线程随机穿插打印数据,而是线程对象1执行完再运行线程2。
**—这是由于没有调用start方法开启新线程,而直接用普通的对象调用方法的方式,根据方法中顺序执行的原则,当然需要先执行完“nt1.run();”才能继续执行下面的代码。
—因此我们知道了线程对象一定要使用start才能创建新线程。**
————————————————————————————
线程运行状态
—用一张图示就能说明。
这里写图片描述
————————————————————————————
线程对象API
—线程的名字的获取与设置:
——getName()方法:线程对象被创建了,当然是有自己的名字的,通过API文档查看,我们可以使用getName方法获取线程的名字;以”Thread-编号”为格式,编号从0开始;
——setName(String name)方法:通过调用setName方法,我们也可手动修改线程的名字;
——同时我们新建线程的时候可以直接通过构造函数传进去一个名字:
这里写图片描述
,如下实例:
————————————————————————————

class NewThread extends Thread{        //private String name;        public NewThread(String name)        {                super(name); //通过父类构造函数Thread(String name)        }        public void run()        {                for(int i=0;i<100;i++)                        System.out.println(name+"-"+i);        }}

————————————————————————————
—当前线程对象的获取:
——currentThread方法:静态的,实例代码:
————————————————————————————

class Demo7_6{        public static void main(String[] args)        {                Thread nt = new NewThread("新线程1");                nt.start();//打印true        }}class NewThread extends Thread{        public NewThread(String name)        {                super(name); //通过父类构造函数Thread(String name)        }        public void run()        {                System.out.println(Thread.currentThread()==this);//打印true        }}

————————————————————————————
从运行结果,我们可以发现通过“Thread.currentThread()”得到的Thread对象和this是相等的关系,另外,Thread.currentThread()的方式比this更通用些。
————————————————————————————
下面再看一个实例代码,:
————————————————————————————

class Demo7_7{        public static void main(String[] args)        {                //七台卖票终端                Thread t1 = new TicketThread();                Thread t2 = new TicketThread();                Thread t3 = new TicketThread();                Thread t4 = new TicketThread();                Thread t5 = new TicketThread();                Thread t6 = new TicketThread();                Thread t7 = new TicketThread();                //开始卖票                t1.start();                t2.start();                t3.start();                t4.start();                t5.start();                t6.start();                t7.start();        }}class TicketThread extends Thread{        //因为多个线程共享同一个数据,因此设置成静态的,在内存中只一份        private static int ticketAmount = 100;        public void run()        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                {                        //卖出的票打印出来                        System.out.println(threadName+"-"+ticketAmount); //①                        //卖出一张,则票数减1                        ticketAmount--; //②                }        }}

————————————————————————————
执行结果:
这里写图片描述
分析:结果中我们发现几个线程都同时卖出了同一张票,这当然是不允许的。
这是由于我们几个线程都执行到了①处,然后还没执行②的时候就交出了CPU执行权,因此就出现了票数还没减1就被其他线程打印了的情况。
—程序安全隐患(卖出同一张票),而且也有缺陷(static的变量声明周期太长,一般如果非静态能解决的就不用静态),我觉得可以这样来思考:
——如果使用面向对象编程的思想,我们把卖票这个动作封装成一个对象,将票数变成类的成员,卖票变成成员方法,那是不是可以首先解决一下static变量周期长的小问题呢?
——当然,Java也给我们提供了这种方式,我们查看Thread类的构造函数可以发现两个构造方法:
——Thread(Runnable target) ;
——Thread(Runnable target, String name) ;
——用通俗的语言解释就是,创建一个线程对象,将线程要执行的任务在封装的对象通过参数丢给线程对象,然后线程对象就可以通过start方法开启新线程去执行了。
——那么这个任务java就使用Runnable来描述了,我们再看到Runnable的文档解释,看到其就只包含一个方法,那就是和Thread中的run方法一样返回值是void的方法,那就是说如果我们定义一个任务类来实现Runnable接口并实现run方法,将要做的任务写入run方法中,然后将这个任务类的一个实例丢给多个用Thread创建的新线程去分配执行,那是不是就可以搞定呢。
—现在我们再看下面的实例:
————————————————————————————

class Demo7_8  {        public static void main(String[] args)        {                //新建卖票任务对象                Runnable tt = new TicketTask();                //新建多线程,将一个任务分配给他们                Thread t1 = new Thread(tt);                Thread t2 = new Thread(tt);                Thread t3 = new Thread(tt);                Thread t4 = new Thread(tt);                Thread t5 = new Thread(tt);                Thread t6 = new Thread(tt);                Thread t7 = new Thread(tt);                //开启线程                t1.start();                t2.start();                t3.start();                t4.start();                t5.start();                t6.start();                t7.start();        }}class TicketTask implements Runnable{        private int ticketAmount = 100;        public void run() //线程开启的时候会自动帮我们调用这个方法的了        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                {                        System.out.println(threadName+"-"+ticketAmount);                        ticketAmount--;                }        }}

————————————————————————————
运行结果:
这里写图片描述
我们发现票数还是没有能够同步成功;究其原因我们可以想到是因为多个线程同时进入了run方法中,java中给我们提供了方式可以让方法只被一个线程执行,通过使用synchronized关键字的方式,我们修改程序如下,在run方法中加入关键字,下面只列出修改的run方法,其他代码和上面一致:
————————————————————————————

        public synchronized void run() //加入关键字synchronized在类型返回值的前面        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                {                        System.out.println(threadName+"-"+ticketAmount);                        ticketAmount--;                }        }

但是新的问题又来了,我们发现加了关键字synchronized之后,票只有一个终端在卖了:
这里写图片描述
原因也非常简单,因为run方法中有while循环,只要Thread-1还在run方法里面执行,其他线程就进不来。这是我们初步了解到关键字synchronized的使用方法,那么Java中对synchronized关键字还有另一种使用方式,就是同步代码块,现在我们还是修改run方法其他代码一致,将需要操作共享变量的语句都用同步代码块括起来:
————————————————————————————

        public void run()        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                {                        Object obj = new Object(); //①                        synchronized(obj)//使用同步代码块                        {//将需要操作共享变量的语句都用同步代码块括起来                                System.out.println(threadName+"-"+ticketAmount);                                ticketAmount--;                        }                }        }

————————————————————————————
我们发现问题又回来了:
这里写图片描述
其实原因设计到同步代码块的使用,同步代码的使用格式如下:
——————————————————
synchronized(一个对象(锁)③)
{
//需要多线程操作共享数据的代码
}
——————————————————
我们在③位置,放入的必须要是一个相同的对象,才能够保证一个线程进去的时候其他线程被挡在外面,我们看到代码中,锁的位置我们确实是放入了一个Object对象,但是是每次新线程进来都new了一个Object对象,看到代码中的①位置,并不是同一个对象,因此每个线程都不能够被挡在外面,直接就能够进来了。
—因此我们可以将Object对象变成这个类的成员变量就可以了:
————————————————————————————

        Object obj = new Object(); //TicketTask类的成员变量        public void run()        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                { //①                        synchronized(obj)//使用同步代码块                        {//将需要操作共享变量的语句都用同步代码块括起来                                System.out.println(threadName+"-"+ticketAmount);                                ticketAmount--;                        }                }        }

————————————————————————————
现在我们又发现新问题那就是,就是票数变成了负数,按道理买完了是不能够再卖了的:
这里写图片描述
原因:由于同步代码块的原因,当有一个线程在run方法中的时候,我们的其他线程都被挡在了①处,也就是已经判断完while循环的条件,但是在run中的线程还没有释放锁给其他线程进去同步代码块中,因此我可以在同步代码块中再判断一次:
————————————————————————————

        Object obj = new Object(); //TicketTask类的成员变量        public void run()        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                {                        synchronized(obj)//使用同步代码块                        {//将需要操作共享变量的语句都用同步代码块括起来                                if(ticketAmount>0)                                {                                        System.out.println(threadName+"-"+ticketAmount);                                        ticketAmount--;                                }                        }                }        }

————————————————————————————
这时候我们就能够看到程序按照票数100到1的来卖了,并且每次执行的结果都是不同线程穿插着来的。
————————————————————————————
那么这个实例到这里就结束了,这就是创建线程的第二种方式,我们来总结一下步骤:
—①定义类实现Runnable接口;
—②实现Runnable接口中的run方法;
—③通过Thread类建立线程对象;
—④将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;
—⑤调用Thread类的start方法开启线程(会自动调用Runnable接口子类的run方法);
————————————————————————————
这里写图片描述
看上图,我们要明白为什么要设计出Runnable接口:
—我是这样理解的:情况①是Student类中有一段代码需要多线程去执行,因此我们拿Student类去继承Thread类然后把需要多线程执行的代码写到run方法中(复写Thread的run方法);
—但是构造类的时候,比如我们如果已经将Student类去继承了一个父类Person的话,由于Java不允许多继承的机制,那么导致Student不能够再继承Thread。因此Sun公司有一中新的方式,就是将Student去实现Runnable接口。然后将这个Student的实例(符合Runnable规则的实例)传给Thread构造函数,也能够完成多线程的调用!
—因此实现Runnable方式的多线程好处就是避免了单继承的局限性;建立线程也推荐使用这种方式,因为正如上面卖票程序的实例,这也是面向对象思想的一种体现;
————————————————————————————
————————————————————————————
多项成两种实现方式的区别:
—①继承Thread类:线程代码存放在Thread子类run方法中;
—②实现Runnable接口:线程代码存放在接口的子类的run方法中;
————————————————————————————
synchronized机制虽然解决了线程同步问题,我们还是要注意一下几个问题:
—①必须有两个或以上的线程;
—②必须是多个线程使用同一个锁(同一个对象);
—③好处:解决了多线程的安全问题;
—④弊端:多线程需要判断锁,较为消耗资源;
总结,synchronized机制有两种使用方式:
—①在方法上:
public synchronized 返回值类型 函数名([参数]){…}
——————————————————————
—②同步代码块:
synchronized(对象)
{…}
——————————————————————
我们都知道第②种方式中我们可以自定义一个锁对象,使用同一个锁对象的线程能够实现同步;对于第①种,使用的锁又是什么咧?
—我们知道,类中的非静态函数为了能够使用非静态成员变量,都会持有一个类所属对象的引用,因此我们可以猜想第①种同步方式用的锁可能就是this,也就是类所属对象的引用。验证:
————————————————————————————

/*我们采用第②种方式验证,将锁设置成this*/class Demo7_9{        public static void main(String[] args)        {                Runnable tt = new TicketTask();                Thread t1 = new Thread(tt);                Thread t2 = new Thread(tt);                Thread t3 = new Thread(tt);                Thread t4 = new Thread(tt);                t1.start();                t2.start();                t3.start();                t4.start();        }}class TicketTask implements Runnable{        private int ticketAmount = 100;        public void run()        {                //获取当前线程                Thread t = Thread.currentThread();                //获取当前线程名字                String threadName = t.getName();                while(ticketAmount>0)                {                        synchronized(this)//使用同步代码块                        {//将需要操作共享变量的语句都用同步代码块括起来                                if(ticketAmount>0)                                {                                        System.out.println(threadName+"-"+ticketAmount);                                        ticketAmount--;                                }                        }                }        }}

————————————————————————————
执行结果各个线程穿插执行,表明:方法上使用synchronized使用的锁确实是this。
那么问题又来了,如果是静态方法上使用synchronized的话,使用的锁是什么呢?
答案是:类的字节码文件对象。
这里又设计到一个新但是又不算新的概念:之所以这样说,是因为万物皆对象,类的字节码文件在java中也用一个类进行描述了,那就是Class类,当一个class文件被加载进内存的时候,静态成员以及该类对应的一个字节码文件对象也在内存中了,这个对象使用“类名.class”的方式表示,类型就是Class。而且在内存中也是唯一的,因此,我们把程序的“synchronized(this)”改成“synchronized(TicketTask.class)”也是没有问题的。
这里我们就不验证静态方法使用的锁就是“TicketTask.class”了,大家记住就好:
—①非静态方法上使用synchronized使用的锁是“this”;
—②静态方法上使用synchronized使用的锁是“该类.class”;
————————————————————————————
还记得前面我们讲过的单例模式么?
—当我们使用单例设计模式-懒汉式在多线程的情况下如果不做同步会有安全隐患。
原来的程序是↓;
————————————————————————————
class Single1
{
//懒汉式1,需要的时候才生成实例
static private Single1 s = null;
private Single1(){}
public static Single1 getInstance()
{
if(s==null)
{
s = new Single1();
}
return s;
}
}
————————————————————————————
多线程并发访问的时候可能多个线程都同时执行到了if(s==null)里面去了;
这时候得到的s对象就是不唯一的了;
因此做了同步优化后是这样的↓:
————————————————————————————

class Single2{        //懒汉式2,需要的时候才生成实例        static private Single2 s = null;        private Single2 (){}        public static Single2 getInstance()        {                if(s==null)//双重判断减少了锁的判断                {                        //synchronized代码块只允许一个线程访问,                        //因此保证了单线程                        synchronized(Single2.class)//类的字节码文件对象作为锁                        {                                if(s==null)                                        s = new Single2();                        }                }                return s;        }}

————————————————————————————
死锁
—死锁问题是多线程的出现伴随出现的问题,当你持有一个锁,我也持有一个锁,我要到你里面运行,要跟你要锁,而你也要到我里面运行,也要跟我要锁,我不释放锁你进不来,你不释放锁我也进不去;谁都不放的情况下,就一直互相等,这就造成了死锁;
—表现:程序挂着不会动了!
—比喻:就好比两人,一人只有一根筷子,现在要两根筷子才能吃饭,然后谁都不愿意放下自己的那根给别人另一个。
—下面给出一则死锁的实例代码,大家好好体会一下,这种互相要锁的感觉
————————————————————————————

class Demo7_10{        public static void main(String[] args)        {                //创建连个线程                Thread t1 = new Thread(new Run(true));                Thread t2 = new Thread(new Run(false));                t1.start();                t2.start();        }}class Run implements Runnable{        private boolean flag;        public Run(boolean flag)        {                this.flag = flag;        }        public void run()        {                if(flag) //线程1执行的部分                {                        synchronized(LockFactory.lockA)                        {                                System.out.println("if.....lockA");                                synchronized(LockFactory.lockB) //同步之中嵌套同步容易发生死锁!                                {                                        System.out.println("if.....lockB");                                }                        }                }else //线程2执行的部分                {                        synchronized(LockFactory.lockB)                        {                                System.out.println("else...lockB");                                synchronized(LockFactory.lockA) //同步之中嵌套同步容易发生死锁!                                {                                        System.out.println("else...lockA");                                }                        }                }        }}/*生产锁的工厂类*/class LockFactory{        static LockA lockA = new LockA();        static LockB lockB = new LockB();}/*锁1*/class LockA{}/*锁2*/class LockB{}

————————————————————————————
执行结果:程序挂住不会动了
这里写图片描述

0 0
原创粉丝点击