Java高级--->多线程的学习
1.进程
1.进程是正在运行的程序,也就是执行程序的一次执行过程,是系统进行资源分配的基本单位
2.目前操作系统都是支持多进程,可以进行执行多个进程,通过进程ID区分
3.单核cpu在同一时刻,只能有一个进程,宏观并行,微观串行。
2. 线程
线程,又称轻量级进程,进程中的一条执行路径,也是cpu的基本调度单位。一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程。
例如:迅雷是一个进程,当中的多个下载任务即为线程。
Java虚拟机是一个继承,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行
3.进程和线程的区别
1.进程是操作系统资源分配的基本单位,而线程是cpu的基本调度单位。
2.一个程序运行后至少有一个进程。
3.一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义。
4.进程间不能共享数据段地址,但是同进程的线程之间可以。
4.线程的组成
任何一个线程都具有基本的组成部分
CPU时间片: 操作系统(OS)会为每个线程分配执行时间 运行数据:堆空间: 存储线程需要的对象,多个线程可以共享堆中的数据。栈空间: 存储线程需使用的局部变量,每个线程都拥有独立的栈。线程的逻辑代码.
5.线程的特点
1. 线程抢占式执行效率高 可防止单一线程长时间独占CPU.
2. 在单核CPU中,宏观上同时执行,微观上顺序执行
6.线程的创建方式(三种)
6.1 第一种方式: 继承Thread类 重写run方法
//1.继承线程
public class MyThread extends Thread{//2。重写run方法@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName()+"==="+i);}}
}public class Test {public static void main(String[] args) {//3.创建线程MyThread myThread = new MyThread();myThread.setName("aaa");//4.开启线程myThread.start();}
}
相关方法:
一、获取线程ID和线程名称1.在Thread的子类中调用this.getId()或this.getName()2.使用Thread.currentThread().getId()和Thread.currentThread().getName()
二、修改线程名称1. 调用线程对象的setName()方法2. 使用线程子类的构造方法赋值
举例:使用线程Thread类实现4个窗口各卖100张票
public class MyThread extends Thread{int ticket=100;@Overridepublic void run() {for(;ticket>0;ticket--){System.out.println(Thread.currentThread().getName()+"剩余:"+ticket+"张票");}}
}
public class Test {public static void main(String[] args) {MyThread m1 = new MyThread();m1.setName("A窗口");m1.start();MyThread m2 = new MyThread();m2.setName("B窗口");m2.start();MyThread m3 = new MyThread();m3.setName("C窗口");m3.start();MyThread m4 = new MyThread();m4.setName("D窗口");m4.start();}
}
若想让四个窗口共卖这100张票可在ticket前加 static 即static int ticket=100;
6.2 第二种方式: 实现Runnable接口
实例:实现四个窗口共卖100张票
public class MyRunnable implements Runnable{int ticket=100;@Overridepublic void run() {for(;ticket>0;ticket--){System.out.println(Thread.currentThread().getName()+"剩余:"+ticket+"张票");}}
}
public class TestDemo {public static void main(String[] args) {//可以将MyRunnable理解为一个任务由四个窗口执行,则共享票数 从而实现四个窗口//共卖100张票MyRunnable r1 = new MyRunnable();Thread t1 = new Thread(r1,"A窗口");Thread t2 = new Thread(r1,"B窗口");Thread t3 = new Thread(r1,"C窗口");Thread t4 = new Thread(r1,"D窗口");t1.start();t2.start();t3.start();t4.start();}
}
6.3 第二种方式: 实现Callable接口
实现Callable接口,它和实现Runnable接口差不多,只是该接口种的方法有返回值和异常抛出。
public class Test {public static void main(String[] args) throws Exception {My1 task1=new My1();My2 task2=new My2();//第一种方式建创建线程对象并提交Callable类型的任务//但是这种方式是比较麻烦的,需要封装到FutureTask类种,//因此建议使用线程池来提交任务FutureTask futureTask=new FutureTask(task1);Thread t1=new Thread(futureTask);t1.start();System.out.println(futureTask.get());//第二种方式 使用线程池来提交任务 应用场景: 适合大文件上传。ExecutorService executorService = Executors.newFixedThreadPool(5);Future<Integer> future = executorService.submit(task1);Integer sum1 = future.get();//需要等线程执行完毕后,才会把结果返回给该变量//实现1-100的和Future<Integer> future1 = executorService.submit(task2);Integer sum2 = future1.get();System.out.println(sum1+sum2);}
}
class My1 implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum=0;for (int i=1;i<=50;i++){sum+=i;}return sum;}
}class My2 implements Callable<Integer>{@Overridepublic Integer call() throws Exception {int sum=0;for (int i=51;i<=100;i++){sum+=i;}return sum;}
}
7.线程的生命周期以及状态转换
1.新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new Thread();
2.就绪状态(Runnable):当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3.运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4.阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:1.等待阻塞--运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的 sleep() 或 join() 或发出了I/O请求时,线程会进入到阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期;
8.线程的常用方法
休眠:
public static void sleep(long millis)当前线程主动休眠millis毫秒。
public class ThreadSleep {public static void main(String[] args) {ThreadSleepDemo t = new ThreadSleepDemo();t.start();for(int i=1;i<10;i++){System.out.println("main线程====循环"+i+"次");}}
}class ThreadSleepDemo extends Thread{@Overridepublic void run() {for(int i=1;i<10;i++){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");}}
}
放弃:
public static void yield()当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片 yield()只让有相同执行权的线程获得cup时间片,但是yield()不能控制cup交出的时间,yeild()只是让线程恢复到就绪状态,那么可能在执行yeild()后进入就绪状态,然后马上又进入运行状态。
public class ThreadYield {public static void main(String[] args) {TY t1 = new TY();t1.start();for(int i=1;i<10;i++){Thread.yield();System.out.println("main线程===循环"+i+"次");}}
}
class TY extends Thread{@Overridepublic void run() {for(int i=1;i<10;i++){Thread.yield();System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");}}
}
3.加入:
public final void join()允许其他线程加入到当前线程中在main函数线程中调用线程tj.join()方法,此时main函数线程就进入阻塞状态,直到线程tj完全执行完以后,线程main才结束阻塞状态
public class ThreadJoin {public static void main(String[] args) {TJ tj = new TJ();tj.start();try {//tj执行完后才会执行main函数tj.join();} catch (InterruptedException e) {e.printStackTrace();}for(int i=1;i<10;i++){System.out.println("main线程===循环"+i+"次");}}
}
class TJ extends Thread{@Overridepublic void run() {for(int i=1;i<10;i++){System.out.println(Thread.currentThread().getName()+"线程===循环"+i+"次");}}
}
4.优先级:
线程对象.setPriority()线程优先级1-10,默认为5,优先级越高,表示获取CPU的概率越高
5.守护线程:
线程对象.setDaemon(true);设置为守护线程。线程有两类:用户线程(前台线程)和守护线程(后台线程)如果程序中所有前台线程都执行完毕了,后台线程也会自动结束。垃圾回收线程属于守护线程。
public class ThreadDeamon {public static void main(String[] args) {TP tp1 = new TP();tp1.setName("A线程");//当main线程执行完毕后,后台TP线程自动结束tp1.setDaemon(true);tp1.start();for(int i=1;i<20;i++){System.out.println("main线程===循环"+i+"次");}}
}
class TP extends Thread{@Overridepublic void run() {for(int i=1;i<50;i++){System.out.println(Thread.currentThread().getName()+"===循环"+i+"次");}}
}
9.线程的安全问题
public class TestSafe {//静态资源 共享private static String [] arr=new String[5];private static int index=0;public static void main(String[] args) throws Exception {//匿名对象--Runnable hello=new Runnable() {@Overridepublic void run() {if (arr[index] == null) {arr[index] = "hello";index++;}}};Runnable world=new Runnable() {@Overridepublic void run() {if (arr[index] == null) {arr[index] = "world";index++;}}};Thread t1=new Thread(hello);Thread t2=new Thread(world);t1.start();t2.start();t1.join();t2.join();System.out.println(Arrays.asList(arr));//可能出现的情况,所以出现的情况会丢失 即数据不安全 // (1)hello world null null null// (2)world hello null null null// (3)hello null null null null// (4)world null null null null}
}
多线程安全问题:
1.当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
2.临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
3.原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
使用synchronized可以解决线程不安全问题
synchronized语法:
synchronized(临界资源对象){//对临界资源对象加锁//代码 原子操作
}
public class TestSafe {//静态资源 共享private static String [] arr=new String[5];private static int index=0;public static void main(String[] args) throws Exception {//匿名对象--Runnable hello=new Runnable() {@Overridepublic void run() {//加锁 可以解决安全问题synchronized (arr) {//共享资源 原子操作if (arr[index] == null) {arr[index] = "hello";index++;}}}};Runnable world=new Runnable() {@Overridepublic void run() {synchronized (arr) {if (arr[index] == null) {arr[index] = "world";index++;}}}};Thread t1=new Thread(hello);Thread t2=new Thread(world);t1.start();t2.start();t1.join();t2.join();System.out.println(Arrays.asList(arr));}
运行一直是这个
注意:
1.每个对象都有一个互斥锁标记,用来分配给线程
2.只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步代码块
3.线程退出同步代码块时,会释放相应的互斥锁标记
10.线程死锁
当A线程拥有锁资源a时,这时A线程需要锁资源b, 而B线程拥有锁资源b,这时B线程需要锁资源a, 这样会导致A等待B线程释放资源b, B线程等待A线程释放锁资源a。 从而二个处于永久等待。从而操作死锁。
例子: 两人去餐厅吃饭,A有一根筷子,B有另一个筷子。A要等B的那根筷子,B要等A的筷子,两个人都在循环等待,则会陷入死锁状态。
解决办法:使用synchronized可以解决该问题
public class Boy extends Thread{@Overridepublic void run() {synchronized (LockObject.b){System.out.println(Thread.currentThread().getName()+"获得筷子b");synchronized (LockObject.a){System.out.println(Thread.currentThread().getName()+"获得筷子a");}}}
}
public class Gril extends Thread{@Overridepublic void run() {synchronized (LockObject.a){System.out.println(Thread.currentThread().getName()+"获得筷子a");synchronized (LockObject.b){System.out.println(Thread.currentThread().getName()+"获得筷子b");}}}
}
public class LockObject {/*筷子a*/public static final Object a = new Object();/*筷子b*/public static final Object b = new Object();
}
public class Test {public static void main(String[] args) {Boy boy = new Boy();boy.setName("aaa");Gril gril = new Gril();gril.setName("bbb");boy.start();gril.start();}
}
1.得到死锁的原因:锁与锁之间有嵌套导致。
2.解决死锁的方法:
1. 尽量减少锁得嵌套。
2. 可以使用一些安全类。
3. 可以使用Lock中得枷锁,设置枷锁时间。
11.线程通信
所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。
因为我们无法对哪个线程先获得cpu,也无法确定哪个线程先执行,但是我们想指定某个线程先执行,哪些线程后执行,这样的话我们就需要线程通信技术。
线程通信中的方法:
1.等待 -- 释放锁,进入等待队列public final void wait()public finnal void wait(long timeout)必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有的锁标记。同时此线程阻塞让其在等待队列中属于Object类中的方法2.通知 -- 唤醒等待队列中的线程,进入就绪队列中,参与cpu的竞争public final void notify()public final void notifyAll()
实例:存钱和取钱
规定-- 先存钱在取钱 且存一次钱 取一次钱
public class BankCard {private double balance;private boolean flag=false;public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//存钱public synchronized void save(double money){//若还有取钱则让该线程等待 进入等待队列if(flag==true){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//否则 如下操作this.balance += money;System.out.println(Thread.currentThread().getName()+"存了"+money+", 余额为:"+this.balance);//存钱后将flag置于true 进入取钱操作flag=true;//唤醒当前等待队列中线程this.notify();}//取钱 加入同步代码 使得线程安全public synchronized void withdraw(double money){//若还没有存钱 则让该线程等待if(flag==false){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//否则 如下操作this.balance -= money;System.out.println(Thread.currentThread().getName()+"取了"+money+", 余额为:"+this.balance);//取钱后将flag置于false 进入存钱操作flag=false;//唤醒当前等待队列中线程this.notify();}
}
public class BoyCard implements Runnable{private BankCard bankCard;public BoyCard(BankCard bankCard) {this.bankCard = bankCard;}@Overridepublic void run() {for (int i=0;i<10;i++){bankCard.save(1000);}}
}
public class GrilCard implements Runnable{private BankCard bankCard;public GrilCard(BankCard bankCard) {this.bankCard = bankCard;}@Overridepublic void run() {for (int i=0;i<10;i++){bankCard.withdraw(1000);}}
}
public class Test {public static void main(String[] args) {BankCard bankCard = new BankCard();BoyCard boyCard = new BoyCard(bankCard);GrilCard grilCard = new GrilCard(bankCard);Thread t1 = new Thread(boyCard,"AA");Thread t2 = new Thread(grilCard,"BB");t1.start();t2.start();}
}
sleep和wait的区别:1.所在的类不同:sleep属于Thread类,wait属于Object类。2.使用的地方: sleep可以在任何代码块中使用。wait只能在同步代码块中使用。3.锁资源的释放: sleep不释放锁资源,wait会释放锁资源。4.sleep时间片到了自动唤醒,wait必须需要使用notify或notifyAll唤醒notify()和 notifyAll()有什么区别?1.notifyl()会唤醒所有的线程,notify()会唤醒一个线程。2.notifyAll() 会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
12.线程池
该池子中预先存储若干个线程对象。整个池子就是线程池
存在问题:1.线程是宝贵的内存资源,单个线程约占1MB的空间,过多分配易造成内存溢出。2.频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。线程池的作用:1.线程容器,可设定线程分配的数量上限2.将预先创建的线程对象存入池子中,并重用线程池中的线程对象3.避免频繁的创建和销毁。
线程池的创建方式:
所有的线程池—封装了一个父接口—java.util.concurrent.Executor.
它的实现接口: ExecutorService.
工具类Executors可以创建相应的线程池:
[1] 创建单一线程池 newSingleThreadExecutor()
[2] 创建定长线程池。newFixedThreadPool(n);
[3] 创建可变线程池. newCachedThreadPool()
[4] 创建延迟线程池 .newScheduledThreadPool(n);
方法:Executor:线程池的根类.它中的方法execute()执行线程任务的方法Runnable类型的任务ExecutorService: 线程池的子接口shutdown(); 关闭线程池。需要等待线程池中任务执行完毕后才会关闭。shutdownNow(): 立即关闭线程池。isTerminated():判断线程池是否终止了。submit(): 提交任务给线程池中线程对象、Runnable和Callable类型的任务。
- 创建单一线程池 newSingleThreadExecutor()
适应场景:队列要求线程有序执行。
ExecutorService executorService = Executors.newSingleThreadExecutor();for (int i = 1; i <= 5; i++) {executorService.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "====");}});executorService.shutdown();}
不管存在几个线程 永远都是一个线程在执行五次
- 创建定长线程池。newFixedThreadPool(n);
ExecutorService executorService = Executors.newFixedThreadPool(2);for(int i=1;i<=5;i++) {executorService.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "====" );}});executorService.shutdown();}
- 创建可变线程池. newCachedThreadPool()
ExecutorService executorService = Executors.newCachedThreadPool();for(int i=1;i<=5;i++) {executorService.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "====" );}});executorService.shutdown();}
- 创建延迟线程池 .newScheduledThreadPool(n);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);scheduledExecutorService.schedule(new Runnable() {@Overridepublic void run() {for(int i=1;i<=5;i++){System.out.println(Thread.currentThread().getName()+"===="+i);}}},10, TimeUnit.SECONDS);scheduledExecutorService.shutdown();}
//long delay:延迟时间 TimeUnit unit:时间单位,Runnable command:runnable的创建线程public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
推荐使用原始的创建线程池方式
上面使用Executors创建线程池的四种放方式,都是基于底层ThreadPoolExecutor实现,而阿里开发手册,建议使用最原始的方式。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险
/*原始*//*int corePoolSize, 核心线程数
* int maximumPoolSize, 最大线程数
* long keepAliveTime, 空闲时间
* TimeUnit unit, 时间单位
* BlockingQueue<Runnable> workQueue: 堵塞队列,
* LinkedBlockingDeque:可以设置等待的个数,如果不设置默认为Integer的最大值。*/LinkedBlockingDeque blockingDeque = new LinkedBlockingDeque(3);ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 10, TimeUnit.SECONDS, blockingDeque);/*这里注意 要保证循环的次数-阻塞队列线程数<= 最大线程数 否则存在异常*/for (int i = 0; i < 8; i++) {executor.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"===");}});}//关闭线程executor.shutdown();}
13.手动锁
Lock是手动锁的父接口,它下面有很多实现类。
lock() :获取锁,如锁被占用,则等待
unlock()释放锁资源,放在finally中
boolean tryLock():尝试获取锁(成功返回true,失败返回false,不阻塞)
使用重入锁解决卖票
public class demo04 {public static void main(String[] args) {Ticket task=new Ticket();Thread t1=new Thread(task,"窗口A");Thread t2=new Thread(task,"窗口B");Thread t3=new Thread(task,"窗口C");Thread t4=new Thread(task,"窗口D");t1.start();t2.start();t3.start();t4.start();}
}class Ticket implements Runnable{private int ticket=100;Lock s=new ReentrantLock();@Overridepublic void run() {while(true) {try {s.lock();//查看释放获取锁资源if (ticket > 0) {--ticket;System.out.println(Thread.currentThread().getName() + "卖了一张,剩余:" + ticket + "张");} else {break;}}finally {s.unlock();//释放锁}}}
}
Synchronized和Lock区别:
1.synchronized可以给类,方法,代码块加锁,而lock只能给代码块加锁
2.synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而lock需要自己加锁和释放锁,如果使用不当没有unlock()去释放锁就会造成死锁
3.通过lock可以知道有没有成功的获取锁,而synchronized却无法办法
Java高级--->多线程的学习相关推荐
- 用10086客服热线理解Java高级多线程之线程池
Java高级多线程之线程池 客服热线案例 引入线程池 1.线程的概念 2.线程池的作用: 获取线程池 1.常用的线程池接口和类 2.代码案例 Callable接口 1.概念简述 2.应用场景 3.方法 ...
- Java高级框架——Spring学习
北京尚学堂--基于此教育机构的视频所学习 目录 一.Spring 框架简介及官方压缩包目录介绍 二.IOC 三.Spring环境的搭建 四.Spring的三种创建对象方法 五.如何给bean的属性赋值 ...
- Java 高级 --- 多线程快速入门
这世上有三样东西是别人抢不走的:一是吃进胃里的食物,二是藏在心中的梦想,三是读进大脑的书 多线程快速入门 1.线程与进程区别 每个正在系统上运行的程序都是一个进程.每个进程包含一到多个线程.线程是一组 ...
- java高级-多线程编程
2019独角兽企业重金招聘Python工程师标准>>> 一.进程和线程 在java语言中最大的特点就是支持多线程的开发(也是为数不多支持多线程开发的语言),如果对多线程没有一个全面而 ...
- Java8日期时间API,Java高级多线程面试
plusNanos(int offset):增加指定纳秒 减少相关的方法 minusYears(int offset):减少指定年 minusMonths(int offset):减少指定月 minu ...
- Java个人技术知识点总结(业务场景篇,java高级多线程面试
Kafka宕机引发的高可用问题 问题要从一次Kafka的宕机开始说起. 笔者所在的是一家金融科技公司,但公司内部并没有采用在金融支付领域更为流行的RabbitMQ,而是采用了设计之初就为日志处理而生的 ...
- JAVA基础 多线程技术学习笔记(V1.0)
目录 一.多线程介绍 1.1 多线程中的基本概念 1.1.1多线程与进程 1.1.2 进程.线程的区别和联系 1.1.3 并发和并行的区别 1.1.4 线程的执行特点 1.1.5 主线程与子线程 ...
- Java高级----多线程
一.进程和线程 1.概念 进程,是计算机中正在运行的程序,是系统进行资源分配的基本单位.目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分.以前的单核CPU在同一时刻,只能有一个进程, ...
- 带你了解Java高级编程-----多线程
带你了解Java高级编程-----多线程 对于Java的学习,基本的步骤是Java基础编程,掌握了Java语言的基本语法.数组.面向对象编程.异常处理这四部分之后,就要开始对Java高级编程进一步学习 ...
- Java高级特性增强-多线程
请戳GitHub原文: https://github.com/wangzhiwub... 大数据成神之路系列: 请戳GitHub原文: https://github.com/wangzhiwub... ...
最新文章
- python 判断字符串是否包含另一个字符串_强烈推荐:Python字符串(string)方法整理(一)...
- 禅道8.2.4 腾讯云迁移至VM
- android中实现view的更新有两组方法
- java操作storm,Storm集群常用批量操作命令
- vpython 贞测碰撞_7、Pygame碰撞检测
- 全国计算机等级考试题库二级C操作题100套(第54套)
- 大二下学期软件工程概论总结
- 牛客14350 苦逼的单身狗
- Ubuntu 12.04 eclipse 安装 svn插件
- 自创一种新的方法建立 平衡二叉树(AVL)
- Geohash距离估算
- 增加网站的档次!网页设计师可在网站中加入暗色调
- Openwrt Web gui LUCI 流程浅析
- Mybatis generator mapper文件重新生成不会覆盖原文件
- arm服务器测评_某ARM服务器与X86服务器简单性能对比
- cublas_学习笔记2
- 【MySQL 12】MySQL 8.0.18 重新初始化
- 京东校招java面试题_2018京东校招Java笔试题
- 如何破解网络密码?(2种方法)
- 第一台生物计算机,世界上第一台DNA计算机问世