前面我们讲了java的异常处理机制,介绍了在java开发中是如何定位错误,即显见bug的,而今天我们要探究的是一个老司机开车的问题–多线程,关于这个问题,整个体系很是复杂,同时也是面试中必考的一个考点,最重要的是,如果没有掌握到这个知识点,那么你在接下来的学习中,会觉得非常的痛苦。所以,在这里将额外花费一些事件进行介绍。(敲黑板,笔记做好咯)

多线程概念

在介绍什么叫多进程前,请允许先介绍一下进程和线程的关系。所谓进程,就是我们调用程序的这么一个过程,通俗来说,就是从我们打开应用,到最后关闭应用的这么一个过程,在这个过程里,计算机会经历从代码加载到内存,代码执行和执行完毕,程序退出,内存空间被销毁。这个整体的状态,称之为程序的进程。而其中,代码按照顺序不断执行的过程,就是一个线程。而何为多进程呢?我们知道,程序运行的时候,是会按照代码顺序(循环,选择,顺序)一行一行去实现的是吧,那么,在这里就牵涉到了一个问题:“如果我们要实现从网上加载一张比较大的图片到手机上并且显示,会是怎么样的呢?”如果从单线程的角度来说,当我们要加载图片的时候,肯定是要先等它加载完了才可以执行下一步是吧。那么如果这张图要加载一个小时呢?在这个过程里我们的用户可不能再执行其他的操作,就只能眼巴巴地盼着时间快点过去了是吧,但这是不存在的,一般来说,如果我们的软件要客户登上一个小时什么都不能动,客户只会默默卸载而不是等待。所以,我们必须想一个办法,既要让用户最终加载到这张图,同时也不会说只能等待而不能执行其他的操作。怎么办呢?没错,这就要用到我们的多线程技术了。所谓多线程技术,你可以理解为同时进程开挂,创造出了好几个工人(线程),让这些工人各自负责一些指定的内容,而不是只有一个工人来干完一件又一件。也就是说,所谓多线程,就是指可以同时运行多个程序块,使得程序运行的速率得更高。

实现多线程

要实现多线程,从整体上而言,主要分为两种方式,分别是继承Thread类或者实现Runnable接口,下面让我们一起看看详细的介绍

继承Thread类

Thread类存放在java.lang包中,当一个子类继承了Thread类时,必须实现其自带的run()方法,在其中编写需要实现的功能代码,然后创建出该对象,调用start方法执行。格式如下:

class ClassName extends Thread{public void run(){//code}
}ClassName test = new ClassName();
test.start();

我们来看一下具体实例:

package testabstractclass;public class Test1 {public static void main(String[] args){ThreadTest1 test1 = new ThreadTest1();ThreadTest2 test2 = new ThreadTest2();test1.start();test2.start();}
}
/***测试线程1*/
class ThreadTest1 extends Thread{int i=0;@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("测试线程1-->" + i);}}}
/***测试线程2*/
class ThreadTest2 extends Thread{int i=0;@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("测试线程2-->" + i);}}}

运行,结果如图:

在这里,你会发现,尽管test1和test2打印的代码的顺讯并不是连续的,但是也还是一个一个地打印出来,按道理来说,那也还是顺序执行是吧?是的,在我们的多线程中,程序的执行也是有顺序的,那么什么才是决定顺序的标志呢?就是CPU资源,我们的CPU在程序运行的时候会产生空闲的CPU资源,一旦哪个线程抢先获得了这个资源,就可以执行它的代码。所以,在多线程中,抢资源是非常关键的,这也说明了为什么运行结果会有线程1和线程2交互执行。当然,如果我们再次执行的时候,就会发现,其实他的结果也是会变得。因为每一次执行的时候,我们都不知道哪个线程会先取得CPU资源,所以每一次的运行结果都是未知的。但不可否认的是,这样的话,真的实现了我们一边加载图片,一边执行其他操作的愿景。当然,问题也会产生,聪明的我们即将一步一步探究怎么解决这些问题。慢慢来,不要急。
注意:一个线程对象只能调用一次的start方法,如果重复调用会出现IllegalThreadStateException异常,切记切记

实现Runnable接口

如果我们查看过Thread类的源码(没有查看的现在可以去查),你会发现,其实Thread是直接继承自一个Runnable的接口来实现多线程的。也就是可以理解为:Thread是在Runnable接口的基础上进行封装的一个类。我们可以使用Thread来实现多线程,自然也可以使用Runnale来实现多线程。怎么实现呢?我们看一下基本格式:

class className implements Runnable{public void run(){//执行多线程的代码}
}
具体的实例如下:
class ThreadTest1 implements Runnable{@Overridepublic void run() {for (int i = 0;i<10;i++) {System.out.println("thread1->"+i);}}
}

看到这里,大家可能就觉得和Thread没什么区别了,但是该怎么启动这个多线程呢?我们上面是用了Thread.start()方法来调用的是吧。那么我们的Runnable又是怎么调用的呢?熟悉我的套路的朋友们可能就想到说:我们来看一下源码,是吧。但实际上,在这里,是没有源码可以给你参考的,为什么呢?我们来看一下源码(哈哈哈,绕回来了),但是如果你们真的去看了Runnable的源码,你会发现,它就只有一个run方法,就这么简单粗暴了。连个启动的方法都没有,所以,我们要怎么才能启动Runnable呢?先看一段示例:

眼尖的朋友们可能发现了其中的关键所在,这里有两句代码:

new Thread(new ThreadTest1()).start();
new Thread(new ThreadTest2()).start();

是不是看起来特别无离头?如果是的话,说明匿内部类还没学到家,需要回去补补知识哦,在这里,我们是通过匿名内部类来实现了一个Thread对象,并且调用了这个对象的start方法。但是这个对象呢,和我们在上面看到的Thread又有点区别,就是他不需要重写run方法,而把这个run方法的实现交给了runnable。就好比Thread是一把枪,runnable是炮弹。即便我们是在Thread内部重写的run方法,本质上也还是runnable,因为Thread实现了Runnable接口。所以,姑且可以理解为:Runnable是基础,Thread是拓展。而由此便引发了这两者的一个区别所在:
如果一个类继承自Thread,则不适合多个线程共享资源,而如果一个类实现了Runnable接口,则可以在多个线程中去使用,从而实现了共享资源
就好比你有一支可以自动产生炮弹的枪,你可以随时打出这一发子弹,但是你不能做到把这发子弹放在其他的枪上使用。而我用工厂制作出来的标准子弹,因为没有限制在你的枪上,所以可以随便给其他的枪使用。所以,实现Runnable接口的好处就在于:

1.适合多个执行相同代码的线程去处理统一资源
2.避免由于java的单继承特性带来的局限
3.代码与数据独立分开,增强程序健壮性

如果想要亲自看一下效果的同学,可以尝试自己写一个售票程序。模拟3台机器同时出售50张票。分别用Thread和Runnable的方式来实现一次,或许你对这两种方式的区别就会有一个更为深刻的理解了。但是具体的操作步骤,这里便不去说,最后的学习办法除了跟着敲,还要想着敲。自己思考一下执行步骤应该是怎样的,再去写自己的代码,更容易帮助你理解和记忆。

拓展:解密多线程背后的启动方式

我们知道,我们前面是用了Thread.start()方法来实现多线程,但细心的我们应该可以发现,我们调用了这个start方法之后,执行的却是run方法,为什么呢?这是因为我们虽然是调用了start来启动了一个多线程,但这时的多线程是并没有执行,而是出于一种就绪的状态,在这个状态,一旦系统获得了cpu资源,便开始执行run方法。注意,这里的run方法也有一个坑,他执行的并不是Thread里面的run方法,而是Runnable里面的run方法,什么意思呢?我们看看源码:

 /* What will be run. */private Runnable target;/*** If this thread was constructed using a separate* <code>Runnable</code> run object, then that* <code>Runnable</code> object's <code>run</code> method is called;* otherwise, this method does nothing and returns.* <p>* Subclasses of <code>Thread</code> should override this method.** @see     #start()* @see     #stop()* @see     #Thread(ThreadGroup, Runnable, String)*/@Overridepublic void run() {if (target != null) {target.run();}}

我们从源码发现,尽管我们最终调用了Thread的run方法,但实际上,调用的却是target中的run方法。也就是说,我们看起来是调用了Thread的start方法,但实际上最终执行的却是Runnable中的run方法。是不是很晕?但晕也得记住,这也是一道面试题来的。

线程的状态

我们前面说了,当我们调用start方法后,线程会进入一个就绪的状态,那么由此便牵涉到了下面的内容,线程有什么状态呢?我们通过一张图来看看:

虽然丑了点,但大致长这样,接下来我们针对这张图做一个说明:

创建状态:当我们构造出一个线程对象的时候,此时这个对象便处于创建状态,拥有着相应的内存空间以及资源,但是却无法运行就绪状态:如上所说,当我们进调用了start方法时,便进入这个状态,但此时同样不能运行,因为它缺少了cpu时间片运行状态:当现场对象获得了cpu时间片时,便开始进入执行状态(执行run方法)。一旦执行完毕,便终止当前线程。一旦还没有执行完毕,便失去了cpu时间片(其他阻塞时间发生时),会进入阻塞状态,或者返回就绪状态,等待下一次获得cpu时间片时,继续运行阻塞状态:一个正在执行的线程在某种特殊的情况下,比如人为的挂起(调用sleep()、wait()、suspend()等方法)或者需要执行耗时的操作时,线程会让出cpu并暂时停止执行,进入阻塞状态。在这个状态下,线程不能进入排队队列,只有当引起阻塞的原因消除后,才会重新转入就绪状态结束状态:线程调用stop方法或者run方法全部执行完毕后,便处于结束状态,这个状态下的线程不具有重新运行的能力,等待被回收。

线程安全的问题

谈到多线程,必不可少的一个问题就是线程安全的问题。在开发过程中,如果我们通过多线程操作统一组数组,那么因为多个线程是同时执行的,所以在这个过程中便极为出现数据丢失或者数据不准确的问题。什么意思呢?比如我们通过两个线程分别操作放东西和拿东西,一个线程负责把货物放到车上,另一个线程便负责把货物拉走。并且他们是同时工作的。假设刚开始的时候,放东西的线程还跟得上拿东西的线程的速度,但后来,体力不支,跟不上拿东西的线程的速度时,便存在了这么一个问题,我们的货物还没放到车上,拿东西的线程就已经把车给拉走了。如此便造成了数据丢失。再比如售票的问题,我们假设当票大于0的时候,就继续把票卖出,而小于或者等于0的时候,就不卖了是吧。但是有可能存在这么一种情况,我们的线程一刚监测到票额还有1张,准备买下把票数减1,代表卖到了这一张票的时候,突然失去了cpu时间片,这时候,线程二也监测到了这张票的信息,因为线程一还没有减1,所以此时票数仍然显示是1,所以这个时候线程二觉得,恩,还有票,然后准备执行见一操作时,坑爹的cpu时间片又没了,此时线程三一路杀入,看到还有一张票,二话不说,买下了,此时票数显示为0了是吧。这一切看起来无非就是线程三运气足够好是吧,但是如果对线程状态还有印象的同学可能会醒悟过来,这里面有坑啊!设想,当线程一在此获得时间片的时候,它会执行什么样的步骤呢?再监测有没有票?太天真了,它说,我之前已经监测过了,肯定还有票的,不怕,于是刷刷刷地把票额减一,就走了,走了。。。看都不看票数还剩多少。再然后,线程二又醒了,同样说我之前监测到了还有1张票,不怕,于是再把票数减一,又走了。但是坑爹的是,最后一张票明明已经被线程三拿走了啊!所以这里线程一和线程二拿到的又是什么鬼呢?所以这里就是数据不准确的情况了。那么,就买票而言,如果12306没有处理好这种线程不安全的问题,一个春节课后,可能就要被买到票又做不了车的人给砸了吧,毕竟车位真的有限呀,那么没怎么处理这个问题呢?这就又涉及到接下来要介绍的内容了。那就是——

同步与死锁

先说同步,同步是一种用来处理线程不安全的技术,是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待这个线程结束后才可以继续执行。什么意思呢?我们把数据空间当做一间房子,当线程一走进这个房子里开始操作数据后,便把门给锁住,不让其他的线程进来。不管线程一在里面睡了多久才拿到cpu时间片,只要它不全部执行完毕,离开这个房子,其他的线程就不能进来。当然,也有一种情况是,线程一处理完这个数据后,第二次进来的也还是它,因为它可以再次获得cpu时间片,进而在此进行数据操作。但不管怎么样都好,我们都避免了上面说的被线程三捷足先登的问题了。因此也就保证了线程的安全。那么,怎么实现同步呢?有两种方式:

使用同步代码块

使用同步代码块的时候需要了解一个关键字:synchronize,如果我们在常见的代码块上加上这个关键字,就表明它是一个同步代码块,格式如下:

synchronize (同步对象){需要同步的代码
}

就以买票为例,请看:

public class ThreadDemo1 {public static void main(String[] args) {new Thread(new ThreadTest1("线程一")).start();new Thread(new ThreadTest1("线程二")).start();new Thread(new ThreadTest1("线程三")).start();}
}class ThreadTest1 implements Runnable{private String name;private static int ticket = 5;public ThreadTest1(String name) {this.name = name;}@Overridepublic void run() {for (int i = 0;i<10;i++) {synchronized (ThreadDemo1.class) {if (ticket >0) {try {Thread.sleep(300);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println(name+"卖出一张票,当前票额:"+ticket--);}}}}}

运行这段代码,结果如下:

在这里,如果你多运行几遍,你就会发现,前面的线程或许会变,但最后的票数肯定就是按照这样的顺序来执行,这就是同步带来的好处,避免了线程不安全的问题发生。
那么,在这里,或许也有人奇怪,说在这里:

synchronize (同步对象){需要同步的代码
}

这里的同步对象应该是什么?关于这个问题,一般而言,我们是把当前能够获得该对象中需要同步的数据的对象。什么意思呢?就如上面示例而言,我们锁住的对象是:

synchronized (ThreadDemo1.class) {if (ticket >0) {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(name+"卖出一张票,当前票额:"+ticket--);}}

可以看到,我们锁住的是ThreadDemo1而不是实现了Runnable的ThreadTest1对象呢,这是因为,我们在ThreadDemo创建出了三个不同的线程,每个线程都继承了Runnable接口,但是每个线程对象的地址
是否就是一致的呢?不是的。他们是分别独立的三个对象,所以当我们锁住的是ThreadTest1对象时,那就代表我们最终只是分别锁住了三个不同的对象,这样的话我们不是在同一个对象里面进行操作。就好比我们在卖票的地方开了三个窗口,但是只有一个窗口是只能同步的,那么剩下的窗口自然就是谁有空就谁去执行操作,这样的话还能实现同步吗?不能,所以我们要把范围扩大,把这个卖票的地方锁起来,每次只允许一个线程进入,不管他选择哪个窗口都没关系,只要保证是每次一个线程在操作就好。

同步方法

同步方法的使用比同步块看起来简单,它只需要在方法中添加synchronized声明即可。这里不多做介绍,直接上示例代码:

public class ThreadDemo1 {public static void main(String[] args) {ThreadTest1 threadTest1 = new ThreadTest1("售票机");new Thread(threadTest1).start();new Thread(threadTest1).start();new Thread(threadTest1).start();}
}class ThreadTest1 implements Runnable{private String name;private int ticket = 5;public ThreadTest1(String name) {this.name = name;}@Overridepublic void run() {for (int i = 0;i<10;i++) {this.sale();}}private synchronized void sale() {if (ticket > 0) {try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(name+"卖出一张票,当前票额:"+ticket--);}}
}

这个示例代码看起来和上面那一个差不多,实际上却存在着极大的差别。不仅体现在使用方法上,还体现在使用方式,数据处理,对象选择等方面都有差别。因此希望朋友们多研究一下这两个代码有何不同之处,当你能找出这两段代码之间的差别时,就表明你对同步有一个比较深入的理解了。但是在这里,并不会对此进行讲解吗,而希望是你们自己去领悟,如此才能提升你们的思考。当然,如果你思考出来的话,也可以在下方评论贴出,让大家伙参考参考,看看你的理解有没有跑偏了,毕竟及时的学习反馈才是最重要的嘛。
你们以为学习就要结束了吗?非也。我们只讲了同步,还没开始讲死锁呢,怎么可以结束了呢?接下来再看我们的另一个知识点——“死锁
什么叫死锁呢?所谓的死锁就是指两个线程都在等待对方先完成,造成了程序之间的停滞状态,这是由于同步过多引起的。什么意思呢?假设我们目前有两个数据需要同步,线程一的名字叫张三,它需要接收线程二(李四)手中的画之后,才可以把自己的书传送给李四。这看起来没什么问题,当李四把画给了张三之后,张三就把书交给李四,很符合逻辑对不对?那我们接下来再加个条件,李四说:张三要先把书给李四,李四才可以把画交给张三。那么,问题就发生了。双方都需要对方的东西,双方有需要对象先提交同样的东西才可以做出响应。而且张三和李四知识程序,他不会自主协调说,我先给你一半,你也先给我一半吧,所以问题就发生了,张三不断请求李四给他画,李四不断请求张三给他书。就好像先有鸡后有蛋,还有先有蛋,后有鸡的问题一样,不断重复,因为便造成了死锁。那么要怎么解决这个问题呢?其一,就是要编码前确定好逻辑顺序,先给谁,再给谁。其二,就是尽量减少同步了。

多线程综合案例:生产者与消费者的瓜葛

关于多线程,存在的忧虑无非就是数据丢失或者数据精度缺失的问题(至少我目前遇到的是这些,如果还有其他,也欢迎补充,不要让我做井底之蛙呀,拜谢~),我们在前面就说了当多个线程操作同一数据时,线程安全问题就必须解决。就好比生产者与消费者的关系,生产者生产产品,消费者消费产品,两者似乎没什么瓜葛是吧,但是需要注意的是,如果生产者没有生产产品,消费者又何来的消费呢?因此,这里便产生了一个模式“生产者-消费者模式”。它的思路大致如下:当生产者生产出产品后,通知消费者去拿。当生产者在生产产品时,消费者进入等待状态,知道生产者叫它之后,再去消费产品。实例代码如下:

public class ThreadDemo1 {private List<Integer> number = new ArrayList<Integer>(10);public static void main(String[] args) {ThreadDemo1 marKet = new ThreadDemo1();Consumer consumer = marKet.new Consumer();Producer producer = marKet.new Producer();new Thread(producer).start();new Thread(consumer).start();}/*** 生产者* @author dml**/class Producer implements Runnable{@Overridepublic void run() {while(true){synchronized(number){while(number.size() == 10){try {System.out.println("生产空间已满,通知消费");number.wait();} catch (InterruptedException e) {e.printStackTrace();number.notify();}}number.add(1);System.out.println("生产一个商品,当前还能生产"+(10-number.size())+"个商品");}}}}/*** 消费者* @author dml**/class Consumer implements Runnable{@Overridepublic void run() {while(true){synchronized (number) {while(number.size() ==0){try {System.out.println("没有商品可消费,等待中");number.wait();} catch (InterruptedException e) {e.printStackTrace();number.notify();}}number.remove(0);number.notify();System.out.println("消费一个,剩余"+number.size()+"个商品");}}}}
}

运行结果如下:

好了,关于生产者,消费者的实现,不做讲解,让大家去思考。但是接下来要普及的几个东西,是要记住(可能会被面试),也是辅助你理解的,不要错过了哦

关于等待与唤醒那些事

在线程中使线程进入等待(阻塞状态),有几个方法,其中常用的是sleep()和wait();其中sleep方法是线程类的方法,作用是暂停该线程的执行时间,把执行机会让给其他线程,到制定时间后便会自动回复。调用sleep方法不会释放对象锁。因此不能用于同步
wait方法是Object类的方法,即所有对象原则上都能调用这个方法。当该对象调用wait方法时,会导致本线程放弃对象锁,而进入等待对象池中,只有针对这个对象发出的notify或者notifyall方法后,本线程才进入对象锁定池准备获得对象锁,进而进入运行状态。

线程池那些事

关于线程,还有一个重要的概念,叫做线程池。为什么会有这个东西呢?我们来分析一下,当我们创建出一个线程对象时,是不是就已经为其分配好了一定的内存空间,当线程执行结束后,便进入结束状态呢?这点毋庸置疑,我们在前面已经说到,那么,如果我们要创建多个线程,是不是要给每个线程都分配一定的内存空间呢?是的,这就有可能导致这样一个问题,我们的内存空间不断分配,而失去作用的线程对象又还没被及时回收,如此便容易造成内存溢出(OOM)而导致程序崩溃。因此,我们再使用线程的时候,一定不能多开线程,要限制他的数量。可是我又可要这么多线程,怎么办呢?线程池就出现了,它的作用是封装几个线程在里面,当我们调用一个线程池时,会把里面的线程取出,执行线程任务,执行完毕后,回到线程池中休眠,直到下一次的线程任务调用。如此,便解决了需要创建大量的线程对象的问题,用于多线程下载中是非常试用的。那么,如何创建线程池呢?这个不用你担心,一般而言,很多框架都会为我们内置好线程池,我们不用手动去创建、但有时候遇到面试的时候,有的面试官会问你这个问题,如果你能写得出的话,无疑又是一项加分项,关于这点,因为篇幅有限(眼睛受不住了~~),所以这里不作示例,贴出几个干货连接,希望可以帮助到您~
传送门出现!
深入理解java之线程池
海子的这篇文章,真的是非常详细地介绍了关于线程池的知识。如果能看一遍下来,或许你就大概晓得怎么实现线程池了。
最后,关于多线程的一些知识点就要结束了。距离我们正式进入Android方面的介绍也不远了。但同样的问题在于,本人的期末考试准备周也到来了。因此,总结起来就是6月上旬忙着“挑战杯”,下旬忙着复习考试。所以博客这里又要冷落一段时间了,估计下旬还会发一篇关于单例模式的面试知识点,其他的内容可就要等七月份啦。预估在七月份,我们就可以正式踏入Android开发的过程啦,加油吧,骚年们。
官方声明:
如果对文章有表示疑问之处,请在下方评论指出,共同进步,谢谢~

java - 深入篇 --Java的多线程实现相关推荐

  1. Java提高篇 —— Java三大特性之继承

    一.前言 在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事 ...

  2. Java基础篇--Java 数组

    Java基础篇--Java 数组 Java 数组 声明数组变量 创建数组 处理数组 For-Each 循环 数组作为函数的参数 数组作为函数的返回值 多维数组 多维数组的动态初始化(以二维数组为例) ...

  3. Java提高篇——Java实现多重继承

    多重继承指的是一个类可以同时从多于一个的父类那里继承行为和特征,然而我们知道Java为了保证数据安全,它只允许单继承.有些时候我们会认为如果系统中需要使用多重继承往往都是糟糕的设计,这个时候我们往往需 ...

  4. Java IO篇 Java IO编程

    Java IO 一.java io 概述 1.1 相关概念 二.Java IO类库的框架 2.1 Java IO的类型 2.2 IO 类库 三.Java IO的基本用法 3.1 Java IO :字节 ...

  5. Java提高篇——Java中的异常处理

    对于运行时异常.错误和检查异常,Java技术所要求的异常处理方式有所不同. 由于运行时异常及其子类的不可查性,为了更合理.更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出, ...

  6. java gul_[java实战篇]--java的GUI(1)

    给出一个实例即可: package mymenu; import java.awt.*; import java.awt.event.*; import java.io.*; public class ...

  7. JAVA工具篇--java.awt.Robot模拟微信批量添加好友

    前言:java.awt.Robot可以控制鼠标和键盘,本文基于此通过模拟认为添加微信好友的过程实现批量添加微信好友,并最终输出微信号/手机号是否有好友及好友的基本信息,本文代码示例禁用学习交流使用: ...

  8. 【java学习之路】(java SE篇)010.多线程

    多线程 线程的概念 程序 是一个指令的集合 进程 (正在执行中的程序)是一个静态的概念 进程是程序的一次静态执行过程,占用特定的地址空间 每个进程都是独立的,由三部分组成 cpu data code ...

  9. java第二篇Java基础

    Java分为三个版本:Java SE(标准版).Java EE(企业版).Java ME(微型版).其中JavaSE就是大家学JavaEE和JavaME的基础,换而言之学Java先从JavaSE开始, ...

最新文章

  1. ping 代理_Happy专访:Ping太高不是问题 换我不会像120一样比赛
  2. find、sed、awk、grep命令总结
  3. 10个有毒的设计神器
  4. 函数式编程与REST的思考
  5. 第二部分面向对像基础第五章
  6. MFC添加自定义消息及处理函数
  7. android串口工具apk_【APK】一个强大的Android开发工具!
  8. 【AI面试题】什么是数据不平衡,如何解决
  9. 第四届中国软件工程大会征文通知
  10. Linux网络子系统中协议栈的入口处理
  11. ubuntu 18.04 增加新磁盘、挂载、格式化
  12. android 模拟点击menu键,android编程之menu按键功能实现方法
  13. X在苍茫大地 闻一达(闻大嘴) 闻明远
  14. VideoMAE 论文阅读
  15. 【Alpha阶段】第三次scrum meeting
  16. 新版mysql的下载教程_Mysql最新版8.0.21下载安装配置教程详解
  17. VIVO NEX3高层预热,差0.4到100%屏占比,这得让多少人心动
  18. 远程访问及控制SSH
  19. 百度地图level对应距离(比例尺级别对应的多少米)
  20. HDU 4417 Super Mario(线段树||树状数组+离线操作 之线段树篇)

热门文章

  1. 什么是RabbitMQ RabbitMQ详解
  2. Java实现在线打开编辑保存PPT
  3. 0003-动态环境绿色公益环保宣传PPT模板免费下载
  4. 【机器学习】层次聚类算法 CURE算法
  5. vue admin template 侧边栏及顶部栏演示字体样式修改
  6. 给入职三五年程序员的建议,看蚂蚁金服离职人员的个人经历及总结!
  7. Vue+element-ui上传logo图片到后端生成二维码展示到页面
  8. 【已解决】如何做excel表的下拉框多选
  9. Word输出PDF公式丢失(特别是mathtype输出的)一步解决
  10. svn客户端,重新输入用户名密码