黑马程序员——Java基础---多线程
2015-05-04 00:23
537 查看
——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
从系统上来理解:进程有单进程和多进程
单进程:就是只能运行一个程序,只有这个程序运行完了,才能运行其他的进程,例如:DOS系统,他的特点就是只要是一种病毒,就会死机,原因就是采用的是单进程
.多进程:就是可以同时运行多个程序,例如现在的Window系统,采用的就是多进程
Java虚拟机把其封装成了对象,供其使用。
进程和线程的之间的举例说明:打开qq程序,qq就是一个进程,那么线程就是其中的qq连天窗口。
2.一个进程中至少存在一个线程在执行,
例如:java虚拟机启动后,不只是存在的一个线程,一个是主线程,一个是垃圾回收的线程。
3.线程的执行必须是在有进程的基础之上,当一个线程消失了,那么进程不一定会消失,但是进程消失了,那么线程肯定会消失。
4.主线程:在系统中就是进程一旦创建,就是创建一个线程,那么就是个主线程,那么再有其他的线程生成的话,那就是其子类,在java虚拟机中的主线程是在运行主函数的线程。
5.多线程:多线程会提高执行速度(这只是多线程其中的一个功能例如:下载)当是多线程的话,那么各个线程之间会有优先级。当线程抢到了CPU,谁就会执行。
(2)复写run()方法。
(3)启动线程,使用start()方法
(2)重写run()方法。
(3)创建线程与类(A)创建联系,开启线程
2、继承方式:线程代码存放在Thread子类run方法中;
实现方式:线程代码存放在Runnable接口的子类run方法中。
从结果上可以看出,每次的运行结果都不一样,原因就是自定义的那个线程和主线程抢夺CPU,那一个先抢到CPU,那个就先执行。
区别:调用start()方法是启动线程,直接调用run()方法的话,那么就和调用普通的方法是一样的,没有启动线程。
![](http://img.blog.csdn.net/20150503235728567)
![](http://img.blog.csdn.net/20150504000150460)
设置线程名称:可以用setName()方法,可以用构造方法设置线程名称
获得线程名称:用getName()方法获得线程名称
Thread.currentThread()获得当前运行的线程对象
这样的结果相当于买了40张票,但是本来就10张票,重复卖了
结果:
Thread-0卖了第10张票
Thread-2卖了第10张票
Thread-1卖了第10张票
Thread-1卖了第9张票
Thread-2卖了第9张票
Thread-0卖了第9张票
(2).创建Thread对象,然后把类A的对象作为参数传给Thread的构造函数,这是把其关联起来。
(3).通过创建的线程对象,然后开启线程。
解答:复写run()方法,里面存的是线程要运行的代码
因为Runnble接口中只有一个run()方法,所以没有办法启动线程,所以还得使用Thread类来启动线程,所以要把Runnable的子类的对象作为参数传给Thread类构造函数。
从结果上可以看出,这是我们想要的那种结果,多个窗口共同买共有的票。
(2).Runnable实现了资源的共享,例如(售票程序的票)
(3).Runnable实增强了程序的健壮性,代码能够被多个程序共享,实现了数据 与代码是独立的。
(4).实现Runnable接口的线程的运行代码存在实现Runnable接口子类的run()方法中,继承Thread类的线程的代码存在Thread子类的run()方法中。
所以在以后的开发中,使用Runnble接口比较好,更多。
例如:售票小例子
结果出现了:产生了重复的票,并且出现了0票,那么这是不正常的现象的。
(2).格式
synchronized(对象){执行共享资源的代码}
(3).同步规则
必须是多个线程执行(至少两个)才可以产生同步,必须是多个线程使用的是同一个锁,
优点:解决了多线程的安全问题
弊端:多线程需要判断,那么就会消耗时间,消耗资源
这样结果就和我们正常想要的是一样的,没有异常现象
例如:客户向银行存钱,每次存三次,每次存100,分两个地方存户存
思路:共享资源:总价钱,银行,有两个线程(模拟两个地方存)
假如一个函数中是全是同步代码,并且执行的全是共享资源,那么就把其定义为同步函数
普通代码块:就是定义在方法中的代码块。
构造块:定义在类中的代码块,优先于构造方法,重复调用。
静态块:使用static关键字声明的,只执行一次,优先于构造块。
同步代码块:使用synchronized关键字声明的代码块,称为同步代码块。格式:synchronized(同步对象){}
验证思路:还是使用的卖票程序,使用两个线程,他们执行的代码不同,一个是同步代码块(锁的对象是this synchronized(this){}),另一个线程执行的同步函数,如果结果没有异常,那么他们就完全符合线程同步的规则,
多个线程执行的是同步代码,并且他们的是锁是同一个锁,因为同步代码块的锁对象是this,那么此时同步函数锁的对象也是this
从结果上额可以看出,结果是正常的,所以结论是正确的,同步函数的锁对象是this
验证方法:和上面的一样,把同步代码块的锁对象改为:Ticket.class
结果:和上面的一样,正常。
我们可以这样思考,静态方法是随着类的加载而加载,此时没有类的对象,但是还是要找个对象,那么就是类的字节码文件的对象。
(2). 程序中过多了同步操作会产生死锁
死锁就是程序中互相的等待。
2、 使用interrupt(中断)方法该方法是结束线程的冻结状态,是线程回到运行状态中来。
该方法在线程启动前调用。
一.进程概述
进程就是正在执行的一个程序。可以说是静态的,当进程一旦执行,那么就会在内存中给其分配一定的内存空间,供其运行。从系统上来理解:进程有单进程和多进程
单进程:就是只能运行一个程序,只有这个程序运行完了,才能运行其他的进程,例如:DOS系统,他的特点就是只要是一种病毒,就会死机,原因就是采用的是单进程
.多进程:就是可以同时运行多个程序,例如现在的Window系统,采用的就是多进程
Java虚拟机把其封装成了对象,供其使用。
进程和线程的之间的举例说明:打开qq程序,qq就是一个进程,那么线程就是其中的qq连天窗口。
二.线程的概述
1.概述:线程是动态的,是进程中的控制单元,一个进程中可以执行多个线程,线程控制着进程。2.一个进程中至少存在一个线程在执行,
例如:java虚拟机启动后,不只是存在的一个线程,一个是主线程,一个是垃圾回收的线程。
3.线程的执行必须是在有进程的基础之上,当一个线程消失了,那么进程不一定会消失,但是进程消失了,那么线程肯定会消失。
4.主线程:在系统中就是进程一旦创建,就是创建一个线程,那么就是个主线程,那么再有其他的线程生成的话,那就是其子类,在java虚拟机中的主线程是在运行主函数的线程。
5.多线程:多线程会提高执行速度(这只是多线程其中的一个功能例如:下载)当是多线程的话,那么各个线程之间会有优先级。当线程抢到了CPU,谁就会执行。
三.线程的创建方法
1.继承Thread类。
(1)定义类继承Thread类。(2)复写run()方法。
(3)启动线程,使用start()方法
2.实现接口Runnable接口。
(1)定义类(A)实现(implements)接口Runnable。(2)重写run()方法。
(3)创建线程与类(A)创建联系,开启线程
总结:实现方式和继承方式的区别
1、实现方式避免了单继承的局限性,在定义线程时,建议使用实现方法。2、继承方式:线程代码存放在Thread子类run方法中;
实现方式:线程代码存放在Runnable接口的子类run方法中。
四.线程的创建—Thread类
1.代码举例:
public class MyThread extends Thread { public void run(){ for(int i=0;i<10;i++) System.out.println(Thread.currentThread().getName()+"--"+i); } } 结果: main--0 Thread-0--0 main--1 Thread-0--1 main--2 Thread-0--2 Thread-0--3 main--3 Thread-0--4 main--4 Thread-0--5 main--5 Thread-0--6 main--6 Thread-0--7 main--7 Thread-0--8 main--8 Thread-0--9 main--9
从结果上可以看出,每次的运行结果都不一样,原因就是自定义的那个线程和主线程抢夺CPU,那一个先抢到CPU,那个就先执行。
2.start()和run()的区别和联系
联系:线程调用start()方法实际上是调用的run()方法,run()方法中存储的线程运行的代码区别:调用start()方法是启动线程,直接调用run()方法的话,那么就和调用普通的方法是一样的,没有启动线程。
public class MyThread extends Thread { public void run(){ for(int i=0;i<10;i++) System.out.println(Thread.currentThread().getName()+"--"+i); } } public class Text { public static void main(String[] agrs) { MyThread tt = new MyThread(); tt.run(); for (int i = 0; i < 10; i++) System.out.println(Thread.currentThread().getName() + "--" + i); } }
3.复写run()
run() 中存在的是线程运行的代码,对于主线程中运行的代码存储在main()方法中。4.疑问解答
在主线程中,运行时候,也是按照顺序的。直到新的线程开启了,那么才会和存在线程抢夺CPU。public class MyThread extends Thread { public void run(){ for(int i=0;i<10;i++) System.out.println(Thread.currentThread().getName()+"--"+i); } } public class Text { /* * 主线程开始从上向下执行,直到执行到了tt线程,启动了tt线程, * 但是此时主线程已经运行结束了,所以运行结果都是一样的。 * */ public static void main(String[] agrs) { for (int i = 0; i < 10; i++) System.out.println(Thread.currentThread().getName() + "--" + i); MyThread tt = new MyThread(); tt.start(); } } public class Text1{ /* * 主线程开始从上向下执行,开始就创建了线程,并且启动了tt线程, * 然后tt线程会和主线程抢夺CPU,谁抢到就会执行谁,所以此时运行结果是不确定的 * */ public static void main(String[] agrs) { MyThread tt = new MyThread(); tt.start(); for (int i = 0; i < 10; i++) System.out.println(Thread.currentThread().getName() + "--" + i); } }
五.线程状态
线程状态如下图所示:六.线程名称
1.默认的线程
默认线程的名称是Thread-0,Thread-2…….。主线程的名称是:mainpublic class Text { public static void main(String[] agrs) { MyThread tt = new MyThread(); System.out.println("默认的线程名称:"+tt.getName()); //tt.start(); System.out.println( "主线程的默认的名字:"+Thread.currentThread().getName()); } }
2.自定义线程
自定义线程名称和获得线程名称名设置线程名称:可以用setName()方法,可以用构造方法设置线程名称
获得线程名称:用getName()方法获得线程名称
Thread.currentThread()获得当前运行的线程对象
public class MyThread extends Thread { public MyThread (String name){//通过构造方法设置线程名称 super(name); } public void run(){ } } public class Text { public static void main(String[] agrs) { MyThread tt = new MyThread("线程A"); System.out.println("线程名称:" + tt.getName()); // tt.start(); /* * 也可以通过setName()来设置线程名称 */ tt.setName("setName"); System.out.println("线程名称:" + tt.getName()); } } 结果: 线程名称:线程A 线程名称:setName
七.售票小例子
public class Tick extends Thread { private int tick = 10; public void run() { while (tick > 0) { System.out.println(Thread.currentThread().getName() + "卖了第" + (tick--) + "张票"); } } } public class Text { public static void main(String[] agrs) { new Tick().start(); new Tick().start();//这样相当于开了3个线程,就是3个窗口 new Tick().start();//但是这样相当于各个窗口买各自的票,而不是共享票 } }
这样的结果相当于买了40张票,但是本来就10张票,重复卖了
结果:
Thread-0卖了第10张票
Thread-2卖了第10张票
Thread-1卖了第10张票
Thread-1卖了第9张票
Thread-2卖了第9张票
Thread-0卖了第9张票
解决办法:
1. 把票(tick)定义成静态的,但是如果定义静态的话,但是静态变量声明周期长,不建议这样使用
public class Tick extends Thread { private static int tick = 10; public void run() { while (tick > 0) { System.out.println(Thread.currentThread().getName() + "卖了第" + (tick--) + "张票"); } } }
2.可以把值开启一个线程,结果没有错,但是和日常生活多个窗口共同卖票不符合
public class Text { public static void main(String[] agrs) { new Tick().start(); } }
3.使用实现Runnable接口线程处理此情况
步骤
(1).创建类(A)实现接口Runnable,复写run()方法(2).创建Thread对象,然后把类A的对象作为参数传给Thread的构造函数,这是把其关联起来。
(3).通过创建的线程对象,然后开启线程。
解答:复写run()方法,里面存的是线程要运行的代码
因为Runnble接口中只有一个run()方法,所以没有办法启动线程,所以还得使用Thread类来启动线程,所以要把Runnable的子类的对象作为参数传给Thread类构造函数。
售票例子(Runnable接口)
public class Tick implements Runnable { private int tick = 10; public void run(){ while (tick > 0) { System.out.println(Thread.currentThread().getName() + "卖了第" + (tick--) + "张票"); } } } 结果: Thread-0卖了第10张票 Thread-1卖了第10张票 Thread-0卖了第9张票 Thread-1卖了第8张票 Thread-0卖了第7张票 Thread-1卖了第6张票 Thread-0卖了第5张票 Thread-0卖了第4张票 Thread-0卖了第3张票 Thread-0卖了第2张票 Thread-0卖了第1张票 Thread-1卖了第0张票
从结果上可以看出,这是我们想要的那种结果,多个窗口共同买共有的票。
总结:Runnable接口和Thread类的区别与联系
(1).Runnable接口可以避免单继承的限制,要是继承Thread类的话,那么就不能继承其他的类了,因为只能单继承,如果实现了接口Runnale后,还可以继承其他的类,或是是实现其他的接口(2).Runnable实现了资源的共享,例如(售票程序的票)
(3).Runnable实增强了程序的健壮性,代码能够被多个程序共享,实现了数据 与代码是独立的。
(4).实现Runnable接口的线程的运行代码存在实现Runnable接口子类的run()方法中,继承Thread类的线程的代码存在Thread子类的run()方法中。
所以在以后的开发中,使用Runnble接口比较好,更多。
八.线程的安全问题
1.原因
当多条语句执行多个线程共享的资源,执行到一部分后,执行权被抢夺了,导致共享资源的不正正常修改,所以就产生了线程的安全问题。例如:售票小例子
public class Ticket implements Runnable { private int tick = 10; public void run() { while (true) { try { if (tick > 0) { Thread.sleep(10); System.out.println(Thread.currentThread().getName()+ "卖了第" + (tick) + "张票"); tick--; } } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Text { public static void main(String[] agrs) { Ticket tt=new Ticket(); new Thread(tt).start(); new Thread(tt).start(); } } 结果: Thread-1卖了第10张票 Thread-0卖了第10张票 Thread-1卖了第8张票 Thread-0卖了第8张票 Thread-0卖了第6张票 Thread-1卖了第5张票 Thread-1卖了第4张票 Thread-0卖了第3张票 Thread-0卖了第2张票 Thread-1卖了第2张票 Thread-0卖了第0张票
结果出现了:产生了重复的票,并且出现了0票,那么这是不正常的现象的。
2.同步代码块—解决办法
(1)利用的是关键子(synchronized)来处理线程同步问题,保证线程安全,锁就好比门上的锁一下,执行的顺序是:首先是判断锁是否是开着的,若是开着的,那么就可以进去(执行同步代码)然后把锁锁上,执行完后,把锁释放(把锁开开),判断锁是锁着的,那么就在外面等着,直到里面的人把锁开开,出来。(这样保证的是同步代码在某一段时间只有一个线程在执行。)(2).格式
synchronized(对象){执行共享资源的代码}
(3).同步规则
必须是多个线程执行(至少两个)才可以产生同步,必须是多个线程使用的是同一个锁,
优点:解决了多线程的安全问题
弊端:多线程需要判断,那么就会消耗时间,消耗资源
public class Ticket implements Runnable { private int tick = 10; public void run() { while (true) { synchronized (this) { try { if (tick > 0) { Thread.sleep(10); System.out.println(Thread.currentThread().getName() + "卖了第" + (tick) + "张票"); tick--; } } catch (InterruptedException e) { e.printStackTrace(); } } } } } 结果: Thread-0卖了第10张票 Thread-0卖了第9张票 Thread-0卖了第8张票 Thread-1卖了第7张票 Thread-1卖了第6张票 Thread-1卖了第5张票 Thread-1卖了第4张票 Thread-1卖了第3张票 Thread-1卖了第2张票 Thread-1卖了第1张票
这样结果就和我们正常想要的是一样的,没有异常现象
3.同步方法—解决办法
(1)概述
因为方法和代码块都是用来封装代码的,那么代码块可以使用锁来解决同步问题,那么函数也可以使用锁,使方法也可以操作同步问题。(2)格式
public synchronized 方法返回类型 方法名(参数){方法体}(3)使用
怎眼确定锁的位置,寿面判断那些是线程运行的代码,那些是共享资源,那些同步代码是执行的是同步资源,那么就把那些操作同步资源的语句使用锁来进行括起来。例如:客户向银行存钱,每次存三次,每次存100,分两个地方存户存
思路:共享资源:总价钱,银行,有两个线程(模拟两个地方存)
** * 银行类 */ public class Bank { private int sum;// 表示的是当前账户的总额 public synchronized void add(int mon) {// 存方法,加锁的话,那么里面的语句在一段时间内必须是一个线程在执行 sum += mon; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sum=" + sum); } } public class Cus implements Runnable { private Bank bank=new Bank(); public void run(){//线程执行的代码(线程) for(int i=0;i<3;i++){ bank.add(100); } } } public class Text { public static void main(String[] agrs) { Cus tt=new Cus();//客户 new Thread(tt).start();//开始存 new Thread(tt).start();//开始存 } } 结果: sum=100 sum=200 sum=300 sum=400 sum=500 sum=600
(4)同步代码块和同步函数的选择
假如连续的代码是同步代码,并且都是执行的都是共享资源,那么就可以把其抽取成函数,并且定义为同步函数假如一个函数中是全是同步代码,并且执行的全是共享资源,那么就把其定义为同步函数
(5)扩展
回顾:一共分为四种代码块普通代码块:就是定义在方法中的代码块。
构造块:定义在类中的代码块,优先于构造方法,重复调用。
静态块:使用static关键字声明的,只执行一次,优先于构造块。
同步代码块:使用synchronized关键字声明的代码块,称为同步代码块。格式:synchronized(同步对象){}
4.锁对象的确定
(1)同步函数的锁是this验证思路:还是使用的卖票程序,使用两个线程,他们执行的代码不同,一个是同步代码块(锁的对象是this synchronized(this){}),另一个线程执行的同步函数,如果结果没有异常,那么他们就完全符合线程同步的规则,
多个线程执行的是同步代码,并且他们的是锁是同一个锁,因为同步代码块的锁对象是this,那么此时同步函数锁的对象也是this
/*售票机*/ public class Ticket implements Runnable { private int tick = 100; public boolean flag = true; public void run() { if (flag) { while (true) { synchronized (this) { try { if (tick > 0) { Thread.sleep(10); System.out.println(Thread.currentThread().getName() + "卖了第" + (tick--) + "张票"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } else while (true) show(); } public synchronized void show() {// 同步函数 try { if (tick > 0) { Thread.sleep(10); System.out.println(Thread.currentThread().getName()+ "卖了第" + (tick) + "张票"); tick--; } } catch (InterruptedException e) { e.printStackTrace(); } } } public class Text { public static void main(String[] agrs) { Ticket tt=new Ticket(); new Thread(tt).start(); /*这里是主线程睡眠一下,让线程1先运行,防止直接改变标志位*/ try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } tt.flag=false;//更改标志位 new Thread(tt).start();//开始存 } } 部分结果: Thread-0卖了第100张票 Thread-0卖了第99张票 Thread-0卖了第98张票 Thread-0卖了第97张票 Thread-0卖了第96张票 Thread-0卖了第95张票 Thread-1卖了第94张票 Thread-1卖了第93张票 Thread-1卖了第92张票
从结果上额可以看出,结果是正常的,所以结论是正确的,同步函数的锁对象是this
(2)静态同步函数锁对象
静态同步函数的锁是:该方法所在类的字节码文件对象:类名.class验证方法:和上面的一样,把同步代码块的锁对象改为:Ticket.class
/*售票机*/ public class Ticket implements Runnable { private static int tick = 100; public boolean flag = true; public void run() { if (flag) { while (true) { synchronized (Ticket.class) { try { if (tick > 0) { Thread.sleep(10); System.out.println(Thread.currentThread().getName() + "卖了第" + (tick--) + "张票"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } else while (true) show(); } public static synchronized void show() {// 同步函数 try { if (tick > 0) { Thread.sleep(10); System.out.println(Thread.currentThread().getName()+ "卖了第" + (tick) + "张票"); tick--; } } catch (InterruptedException e) { e.printStackTrace(); } } }
结果:和上面的一样,正常。
我们可以这样思考,静态方法是随着类的加载而加载,此时没有类的对象,但是还是要找个对象,那么就是类的字节码文件的对象。
九.死锁
1.死锁产生的原因
死锁产生的原因是:一个所中还有另外一个锁,但是这两个锁对象是不相同的,其中一个锁(A)需要另外一个锁(B),而锁(B)也需要锁(A),这样有时候她们都不会妥协,那么就会产生死锁。2.举例死锁
class MyLock { static Object lockA = new Object(); static Object lockB = new Object(); } public class DeadLock implements Runnable { public boolean flag = true; public DeadLock(boolean f) { this.flag = f; } public void run() { if (flag) { // while (true) { synchronized (MyLock.lockA) { System.out.println("ifLockA"); synchronized (MyLock.lockB) { System.out.println("ifLockB"); } // } } } else { // while (true) { synchronized (MyLock.lockB) { System.out.println("elseLockB"); synchronized (MyLock.lockA) { System.out.println("elseLockA"); } // } } } } } public class Text { public static void main(String[] agrs) { DeadLock l1 = new DeadLock(true);// 第一个线程 DeadLock l2 = new DeadLock(false);// 第二个线程 new Thread(l1).start();// 开启线程 new Thread(l2).start();// 开启线程 } } 结果:可能会死锁 例如: if LockA else LockB
这就产生了死锁
(1). 资源共享的时候需要进行同步操作(2). 程序中过多了同步操作会产生死锁
死锁就是程序中互相的等待。
明白了死锁的原理以后在编写程序中就要尽量避免死锁。
十、停止线程及线程的其它方法
如何停止线程?
1、 定义循环结束标记,只有让run方法结束,开启多线程运行,运行代码通常是循环结构。只要控制住循环就可以让run方法结束,也就是线程结束。特殊情况:
当线程处于冻结状态,就不会读到标记,线程也就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。Thread类中提供来interrupt方法来强制清除冻结状态。2、 使用interrupt(中断)方法该方法是结束线程的冻结状态,是线程回到运行状态中来。
线程的其它方法
1、守护线程
setDaemon(): 当剩下的线程都为守护线程是时,java虚拟机退出;代码运行结束。该方法在线程启动前调用。
2、Join方法
当A线程执行到B线程的.join()方法时,A就等待,等B线程都执行完,A才会执行。Join可以临时加入线程执行。Join方法会抛出异常。结语:单单掌握这些线程的基本知识是不够的,实际用到的线程知识却很灵活,本以为自己掌握的特别好,今天编写关于线程的程序时却感觉力不从心,无从下手,所以一定要多理解,勤动脑思考。
——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-相关文章推荐
- 黑马程序员_Java基础(4)--多线程
- 黑马程序员_java基础加强7_多线程加强
- 黑马程序员——Java基础---多线程(1)
- 黑马程序员---------笔记整理(java基础八-----多线程)
- 黑马程序员——java基础---多线程
- 黑马程序员——Java基础——多线程
- 黑马程序员--Java基础学习笔记【序列化、多线程】
- 黑马程序员-------Java笔记--------多线程基础练习2题
- 黑马程序员--java基础--多线程
- 黑马程序员--java基础--多线程
- 黑马程序员——java基础——多线程
- 黑马程序员-19-java基础-多线程(2)-死锁与线程间通信(synchronized与Lock的区别及各自用法)
- 黑马程序员--Java基础之多线程
- 黑马程序员——Java零基础学习——初步涉及多线程
- 黑马程序员—10—java基础:有关多线程安全的学习笔记和学习心得体会
- 黑马程序员——java基础之多线程
- 黑马程序员_Java基础_多线程
- 黑马程序员_java基础加强8_多线程加强
- 黑马程序员——Java基础--多线程(1)
- 黑马程序员 java 基础 毕向东 面向对象 多线程