黑马程序员--Java多线程
2015-08-14 13:46
267 查看
——- android培训、java培训、期待与您交流! ———-
多线程:
如果程序只有一条执行路径,那么该程序就是单线程程序。
如果程序有多条执行路径,该程序就是多线程程序。
进程:
正在运行的程序就是进程
进程是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。
多进程意义:
单进程的计算机同一时间只能做一件事情,现在的计算机可以做很多事情。
也就是说现在的计算机支持多进程,可以在同一时间段内执行多个任务。并且提高CPU的使用率。
一边打游戏,一边听音乐,不是同时进行的,单CPU在某一时间点上只能做一件事。
CPU在做程序间的高效切换让我们感觉打游戏和听音乐是同时进行的。
一个程序只有一条执行路径,称为单线程程序。
一个程序有多条执行路径,称为多线程程序。
什么是线程:
同一进程内又可以执行多个任务,而这每一个任务就可以看成一个线程。
线程是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
多线程的意义:
多线程的存在,不是为了提高程序的执行速读,是为了提高应用程序的使用率。
程序的执行其实就是在抢CPU的资源,CPU的执行权。
多个进程在抢资源的时候,其中某一个进行的执行路径比较多,就会有更高的几率抢到CPU的执行权。
但是不能保证的是哪一个线程在哪个时刻抢到执行权,所以线程的执行有随机性。
并发和并行的区别:
并发是逻辑上同时发生,指在某一时间内同时运行多个程序。
并发是物理上同时发生,指在某一时间点同时运行多个程序。
JAVA程序运行原理:
java命令会启动java虚拟机,启动JVM就等于启动了一个应用程序,也就是一个进程,
这个进行自动启动一个“主进程”,然后主进程调用某个类的main方法。所以main方法运行在主线程中。
JAVA虚拟机的启动是多线程的,除了主线程,至少还需要垃圾回收线程,否则内存很快就会溢出。
如何实现多线程?
线程是依赖进程存在的,应该先创建一个进程出来。
进程是由系统创建的,应该调用系统功能创建一个进程。
java不能直接调用系统功能,所以没有办法直接实现多线程程序
但是java可以调用C/C++写好的程序实现多线程程序。
由C/C++去调用系统能创建进程,然后由java去调用。
java再提供一些类供我们使用,这就可以实现多线程了。
创建线程对象并启动
run()方法封装被线程执行的代码,直接调用是普通方法。
start()方法先启动了线程,然后再由JVM调用该线程的run()方法。
获取和设置线程名称:
设置线程优先级:
设置线程休眠时间:
加入线程:
礼让线程:执行效果就是两个线程你一次,我一次的执行
守护线程:
m1,m2设置称为守护线程,当前运行主线程结束的时候,m1,m2都会随之结束。但已经抢到了执行权,不可能立马结束,会稍微运行一下。
中断线程:
线程生命周期:
方式2好处:
1.可以避免由于java单继承带来的局限性
2.适合多个相同程序的代码去处理同一个资源的情况,
把线程和程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
判断线程是否有问题的标准
1.是否是多线程环境
2.是否有共享数据
3.是否有多条语句操作共享数据
线程的状态转换图及常见执行情况
测试类:
此时运行程序就会出现相同票卖多次和出现负数票的情况
1.相同的票卖了多次
CPU的一次操作必须是原子性的
2.出现负数票
线程的随机性和延迟导致的
解决方式1:同步代码块
把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行
方式2:同步方法
同步特点:
前提:多个线程
解决问题注意:多个线程使用的是同一个锁对象
同步好处:
同步解决了多线程的安全问题
同步的弊端:
当线程很多时,因为每个线程都会去判断同步上的锁,很消耗资源,降低程序的运行效率
JDK5以后提供了一个新的锁对象Lock,Lock是一个接口,ReentrantLock是Lock的实现类
例如:
中国人,外国人吃饭
正常:
中国人:筷子两支
外国人:刀和叉
死锁:
中国人:筷子一支,刀一把
外国人:筷子一支,叉一把
两个人出现死锁以后,都不能正常用餐,都等着对方
if中的语句抢到执行权,输入”if objA”。输出”if objB”之前被else中的语句抢到了执行权
else语句输出”else objB”,当往下执行的时候需要获取objA锁,但是objA锁在if语句线程中没有释放。
只好等着if语句中释放。此时if语句抢到执行权,往下执行的时候发现需要objB锁,但objB锁在else语句线程中没有释放。
这样就形成了死锁。
例如:卖票,需要有卖票的进程,还需要有生产票的进程
通过对学生信息的设置和获取,写一个简单的线程间通信案例
测试类:
资源类:
设置类:
获取类:
如果是生产多个,和消费多个的话,两个线程需要加上同一把锁才行。
但是这样做依然还存在着问题:
1.如果消费者先抢到CPU执行权,消费数据,这时数据如果是空,就没有意义。
应该等着数据生产出来,再去消费,这样才具有意义。
2.如果生产者先抢到CPU执行权,生产数据,但是生产完一定数量的数据以后,还继续持有执行权,
它还会继续生产数据,这还现实情况不符,需要等着消费者把数据消费以后,再生产。
正常思路:
1.生产者
先看是否有数据,有就等待,没有就生产,生产完通知消费者消费
2.消费者
先看是否有数据,有就消费,没有就等待,消费完通知生产者生产
java提供了一个等待唤醒机制来解决这个问题。
等待唤醒:
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
为什么等待唤醒方法定义在Object类中:
这些方法都是通过锁对象进行调用的,锁对象可以是任意的
所以,这些方法必须定义在Object类中。
测试类:
资源类:
生产者类:
消费者类:
多线程:
如果程序只有一条执行路径,那么该程序就是单线程程序。
如果程序有多条执行路径,该程序就是多线程程序。
进程:
正在运行的程序就是进程
进程是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。
多进程意义:
单进程的计算机同一时间只能做一件事情,现在的计算机可以做很多事情。
也就是说现在的计算机支持多进程,可以在同一时间段内执行多个任务。并且提高CPU的使用率。
一边打游戏,一边听音乐,不是同时进行的,单CPU在某一时间点上只能做一件事。
CPU在做程序间的高效切换让我们感觉打游戏和听音乐是同时进行的。
一个程序只有一条执行路径,称为单线程程序。
一个程序有多条执行路径,称为多线程程序。
什么是线程:
同一进程内又可以执行多个任务,而这每一个任务就可以看成一个线程。
线程是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
多线程的意义:
多线程的存在,不是为了提高程序的执行速读,是为了提高应用程序的使用率。
程序的执行其实就是在抢CPU的资源,CPU的执行权。
多个进程在抢资源的时候,其中某一个进行的执行路径比较多,就会有更高的几率抢到CPU的执行权。
但是不能保证的是哪一个线程在哪个时刻抢到执行权,所以线程的执行有随机性。
并发和并行的区别:
并发是逻辑上同时发生,指在某一时间内同时运行多个程序。
并发是物理上同时发生,指在某一时间点同时运行多个程序。
JAVA程序运行原理:
java命令会启动java虚拟机,启动JVM就等于启动了一个应用程序,也就是一个进程,
这个进行自动启动一个“主进程”,然后主进程调用某个类的main方法。所以main方法运行在主线程中。
JAVA虚拟机的启动是多线程的,除了主线程,至少还需要垃圾回收线程,否则内存很快就会溢出。
如何实现多线程?
线程是依赖进程存在的,应该先创建一个进程出来。
进程是由系统创建的,应该调用系统功能创建一个进程。
java不能直接调用系统功能,所以没有办法直接实现多线程程序
但是java可以调用C/C++写好的程序实现多线程程序。
由C/C++去调用系统能创建进程,然后由java去调用。
java再提供一些类供我们使用,这就可以实现多线程了。
方式1:继承Thread类
自定义MyThread()方法package com.kxg_01; public class MyThread extends Thread { @Override // 重写run()方法,不是类中的所有代码都需要被线程执行,这个时候,为了区分哪些代码能够被线程执行, // java提供了Thread类中的run()方法用来包含那些被线程执行的代码。 public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i);//getName()方法是Thread类中获取线程名称 } } }
创建线程对象并启动
package com.kxg_01; /* * 需求:继承Thread实现多线程的程序 * * 步骤: * 1.自定义MyThread类继承Threa类 * 2.重写Thread类中的run()方法 * 3.创建对象 * 4.启动线程 */ public class TreadDemo { public static void main(String[] args) { MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); // m1.run(); // m2.run(); // 调用run()方法不是开始线程,这样调用相当于调用了一个普通的run()方法 // 用start()方法启动线程 m1.start(); m2.start(); } }
run()方法封装被线程执行的代码,直接调用是普通方法。
start()方法先启动了线程,然后再由JVM调用该线程的run()方法。
获取和设置线程名称:
package com.kxg_01; public class MyThread extends Thread { public MyThread() { } // 定义带参构造方法,Thread类里面有这个带参构造,把参数传递给Thread public MyThread(String name) { super(name); } public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); } } }
package com.kxg_01; public class TreadDemo { public static void main(String[] args) { MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); // 通过带参构造定义线程名称 // MyThread m3 = new MyThread("广州"); // MyThread m4 = new MyThread("深圳"); // 通过Thread类中的setName()方法定义线程名称 m1.setName("北京"); m2.setName("上海"); m1.start(); m2.start(); } }
设置线程优先级:
package com.kxg_02; /* * 设置线程优先级 * public final void setPriority(int newPriority) * * 注意: * 线程默认优先级是5 * 线程优先级范围为1-10 * 优先级高仅仅代表该线程获取的CPU时间片的几率高一些,多次运行才能有好的效果 */ public class TreadDemo { public static void main(String[] args) { // 创建线程 MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); // 设置线程名称 m1.setName("北京"); m2.setName("上海"); // 设置线程优先级 m1.setPriority(1); m1.setPriority(10); // 启动线程 m1.start(); m2.start(); } }
设置线程休眠时间:
package com.kxg_03; public class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); // public static void sleep(long millis):设置休眠时间,单位为毫秒 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
加入线程:
package com.kxg_04; /* * public final void join():等待该线程终止。 * 别的线程需要等待这个加入的线程执行结束才能执行。 */ public class TreadDemo { public static void main(String[] args) { // 创建线程 MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); MyThread m3 = new MyThread(); // 设置线程名称 m1.setName("北京"); m2.setName("上海"); m3.setName("广州"); // 启动线程 m1.start(); // 加入线程 try { m1.join(); } catch (InterruptedException e) { e.printStackTrace(); } m2.start(); m3.start(); } }
礼让线程:执行效果就是两个线程你一次,我一次的执行
package com.kxg_05; public class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":" + i); // public static void yield():暂停当前正在执行的线程对象,并执行其他线程。也称为礼让线程 Thread.yield(); } } }
守护线程:
package com.kxg_06; /* * public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。 * 当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。 */ public class TreadDemo { public static void main(String[] args) { // 创建线程 MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); // 设置守护线程 m1.setDaemon(true); m2.setDaemon(true); // 设置线程名称 m1.setName("深圳"); m2.setName("上海"); // 启动线程 m1.start(); m2.start(); // 设置当前运行线程名称 Thread.currentThread().setName("北京"); for (int i = 0; i < 10; i++) { // Thread.currentThread().getName():当前执行的线程的名称 System.out.println(Thread.currentThread().getName() + ":" + i); } } }
m1,m2设置称为守护线程,当前运行主线程结束的时候,m1,m2都会随之结束。但已经抢到了执行权,不可能立马结束,会稍微运行一下。
中断线程:
package com.kxg_06; /* * 中断线程: * public void interrupt() */ public class TreadDemo { public static void main(String[] args) { // 创建线程 MyThread m1 = new MyThread(); MyThread m2 = new MyThread(); // 设置线程名称 m1.setName("深圳"); m2.setName("上海"); // 中断线程,后面的线程还可以继续运行 m1.interrupt(); // 启动线程 m2.start(); } }
线程生命周期:
方式2:通过实现Runnable接口的实现类
package com.kxg_07; /* * 实现Runnable接口 */ public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } }
package com.kxg_07; /* * 实现Runnable接口 * 步骤: * 1.自定义类MyRunnable实现Runnabe接口 * 3.创建MyRunnable对象 * 4.创建Thread类的对象,把MyRunnable对象作为构造参数传递 */ public class RunnableDemo { public static void main(String[] args) { // 创建对象 MyRunnable mr = new MyRunnable(); // 设置线程 Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); // 设置线程名称 t1.setName("深圳"); t2.setName("上海"); // 启动线程 t1.start(); t2.start(); } }
方式2好处:
1.可以避免由于java单继承带来的局限性
2.适合多个相同程序的代码去处理同一个资源的情况,
把线程和程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
判断线程是否有问题的标准
1.是否是多线程环境
2.是否有共享数据
3.是否有多条语句操作共享数据
线程的状态转换图及常见执行情况
方式3:实现Callable接口,需要和线程池结合使用
实现类:package com.kxg_06; import java.util.concurrent.Callable; /* * Callable<V>:带泛型的接口 * 接口中只有一个方法:V call() * 接口中的泛型是call()方法的返回值类型 * */ public class MyCallable implements Callable { @Override public Object call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } return null; } }
测试类:
package com.kxg_06; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class MyCallableDemo { public static void main(String[] args) { // 创建线程池对象 ExecutorService pool = Executors.newFixedThreadPool(2); // 添加Callable实现类 pool.submit(new MyCallable()); pool.submit(new MyCallable()); // 结束线程池 pool.shutdown(); } }
同步问题
先看一个模拟电影院卖票案例,为了模拟真实卖票环境,代码加入延时。package com.kxg_09; public class SellTicekt implements Runnable { private int ticket = 100; @Override public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票"); } } } }
package com.kxg_09; /* * 实现Runnable接口 */ public class SellTicketDemo { public static void main(String[] args) { // 创建自定义类对象 SellTicekt st = new SellTicekt(); // 创建线程 Thread t1 = new Thread(st); Thread t2 = new Thread(st); // 设置线程名称 t1.setName("窗口1"); t2.setName("窗口2"); // 启动线程 t1.start(); t2.start(); } }
此时运行程序就会出现相同票卖多次和出现负数票的情况
1.相同的票卖了多次
CPU的一次操作必须是原子性的
2.出现负数票
线程的随机性和延迟导致的
解决方式1:同步代码块
把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行
package com.kxg_09; /* * synchronized(对象){ * 代码; * } * * 注意:同步代码块可以解决安全问题的根本原因在对象上,该对象如果锁一样的功能,别的线程不能进入。 * 这个对象可以是任意对象,最好是用本身this作为这个对象。 * */ public class SellTicekt implements Runnable { private int ticket = 100; @Override public void run() { while (true) { synchronized (this) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票"); } } } } }
方式2:同步方法
package com.kxg_09; /* * synchronized关键字修饰方法 * 锁对象是this */ public class SellTicekt implements Runnable { private int ticket = 100; @Override public synchronized void run() { while (true) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票"); } } } }
同步特点:
前提:多个线程
解决问题注意:多个线程使用的是同一个锁对象
同步好处:
同步解决了多线程的安全问题
同步的弊端:
当线程很多时,因为每个线程都会去判断同步上的锁,很消耗资源,降低程序的运行效率
JDK5以后提供了一个新的锁对象Lock,Lock是一个接口,ReentrantLock是Lock的实现类
package com.kxg_11; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /* * Lock: * lock():添加锁 * unlock():释放锁 */ public class SellTicekt implements Runnable { private int ticket = 100; // 创建Lock接口的实现类ReentrantLock private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票"); } } finally { lock.unlock(); } } } }
死锁
两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象
例如:
中国人,外国人吃饭
正常:
中国人:筷子两支
外国人:刀和叉
死锁:
中国人:筷子一支,刀一把
外国人:筷子一支,叉一把
两个人出现死锁以后,都不能正常用餐,都等着对方
package com.kxg_12; public class DieLock extends Thread { // 创建两个锁对象 public static Object objA = new Object(); public static Object objB = new Object(); // 定义变量 private boolean flag; // 定义构造方法 public DieLock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { // 线程A中嵌套线程B synchronized (objA) { System.out.println("if ObjA"); synchronized (objB) { System.out.println("if objB"); } } } else { // 线程B中嵌套线程A synchronized (objB) { System.out.println("else objB"); synchronized (objA) { System.out.println("else objA"); } } } } }
package com.kxg_12; public class DieLockDemo { public static void main(String[] args) { // 创建两个线程 DieLock dl1 = new DieLock(true); DieLock dl2 = new DieLock(false); // 启动线程 dl1.start(); dl2.start(); } }
if中的语句抢到执行权,输入”if objA”。输出”if objB”之前被else中的语句抢到了执行权
else语句输出”else objB”,当往下执行的时候需要获取objA锁,但是objA锁在if语句线程中没有释放。
只好等着if语句中释放。此时if语句抢到执行权,往下执行的时候发现需要objB锁,但objB锁在else语句线程中没有释放。
这样就形成了死锁。
线程间通信
针对同一资源有不同的线程进行操作例如:卖票,需要有卖票的进程,还需要有生产票的进程
通过对学生信息的设置和获取,写一个简单的线程间通信案例
测试类:
package com.kxg_03; public class StudentDemo { public static void main(String[] args) { // 创建资源 Student s = new Student(); // 创建SetThread和GetThread对象 SetThread st = new SetThread(s); GetThread gt = new GetThread(s); // 创建线程 Thread t1 = new Thread(st); Thread t2 = new Thread(gt); // 开启线程 t1.start(); t2.start(); } }
资源类:
package com.kxg_03; /* * 定义学生类 */ public class Student { String name; int age; boolean flag;// 用来判断是否存在资源,默认是flash,没有资源 public synchronized void set(String name, int age) { // 生产者,如果有数据就等待 if (!this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 设置数据 this.name = name; this.age = age; // 修改标记 this.flag = false; // 唤醒线程 this.notify(); } public synchronized void get() { if (this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(this.name + ":" + this.age); // 修改标记 this.flag = true; // 唤醒线程 this.notify(); } }
设置类:
package com.kxg_03; /* * 设置学生信息的线程 */ public class SetThread implements Runnable { private Student s; private int i; public SetThread(Student s) { this.s = s; } @Override public void run() { while (true) { if (i % 2 == 0) { s.set("小明", 5); } else { s.set("汪汪", 2); } i++; } } }
获取类:
package com.kxg_03; /* * 设置获取学生信息的线程 */ public class GetThread implements Runnable { private Student s; public GetThread(Student s) { this.s = s; } @Override public void run() { while (true) { s.get(); } } }
如果是生产多个,和消费多个的话,两个线程需要加上同一把锁才行。
但是这样做依然还存在着问题:
1.如果消费者先抢到CPU执行权,消费数据,这时数据如果是空,就没有意义。
应该等着数据生产出来,再去消费,这样才具有意义。
2.如果生产者先抢到CPU执行权,生产数据,但是生产完一定数量的数据以后,还继续持有执行权,
它还会继续生产数据,这还现实情况不符,需要等着消费者把数据消费以后,再生产。
正常思路:
1.生产者
先看是否有数据,有就等待,没有就生产,生产完通知消费者消费
2.消费者
先看是否有数据,有就消费,没有就等待,消费完通知生产者生产
java提供了一个等待唤醒机制来解决这个问题。
等待唤醒:
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
为什么等待唤醒方法定义在Object类中:
这些方法都是通过锁对象进行调用的,锁对象可以是任意的
所以,这些方法必须定义在Object类中。
测试类:
package com.kxg_03; public class StudentDemo { public static void main(String[] args) { // 创建资源 Student s = new Student(); // 创建SetThread和GetThread对象 SetThread st = new SetThread(s); GetThread gt = new GetThread(s); // 创建线程 Thread t1 = new Thread(st); Thread t2 = new Thread(gt); // 开启线程 t1.start(); t2.start(); } }
资源类:
package com.kxg_03; /* * 定义学生类 */ public class Student { String name; int age; boolean flag;// 用来判断是否存在资源,默认是flash,没有资源 }
生产者类:
package com.kxg_03; /* * 设置学生信息的线程 */ public class SetThread implements Runnable { private Student s; private int i; public SetThread(Student s) { this.s = s; } @Override public void run() { while (true) { // 设置同步锁 synchronized (s) { // 如果有数据,生产者等待 if (!s.flag) { try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (i % 2 == 0) { s.name = "小明"; s.age = 5; } else { s.name = "汪汪"; s.age = 2; } i++; // 生产完成,把变量改为有数据 s.flag = false; // 唤醒线程 s.notify(); } } } }
消费者类:
package com.kxg_03; /* * 设置获取学生信息的线程 */ public class GetThread implements Runnable { private Student s; public GetThread(Student s) { this.s = s; } @Override public void run() { while (true) { // 设置同步锁 synchronized (s) { // 如果没有数据,消费者就等待 if (s.flag) { try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(s.name + ":" + s.age); // 修改标记 s.flag = true; // 唤醒线程 s.notify(); } } } }
总结
多线程中应用最多的实现方式是第二种实现方式,应该熟练掌握这种实现方式。多线程同步问题也是一个重点难点,需要掌握每种同步方法的实现方式。熟练掌握线程的生命周期,等待唤醒机制。相关文章推荐
- 我想问面试官的话(更新中)
- 静态链接的一点小总结(二) 《程序员的自我修养》·笔记
- 多线程经典面试题
- 面试需要注意的十二个得分细节
- 设计模式面试大集锦
- 黑马程序员——java复习总结——泛型和Map
- 【黑马程序员-学习笔记】OC-协议与分类
- 黑马程序员——26,基本数据操作流,字节数组操作流,转换流,编码表
- java面试题总结
- 【面试真题】华为2013至2015最全-嵌入式软件(附答案)
- 黑马程序员-Java基础:IO流
- 千锋扣丁学堂 教学时代-在线教育
- 程序员面试十大误区
- 程序员该如何合理安排时间呢?
- 线程面试题
- 三道经典的逻辑推理面试题:病狗、三盏灯、买鸡
- 黑马程序员-----Java基础-----抽象类
- 技术面试五步曲
- 黑马程序员 Java基础 反射
- 黑马程序员 Java基础 多线程2