java基础对于学习安卓是很重要的,比如说线程,多线程。我们做安卓开发可能不太需要去研究高并发这些高深的问题,但是基础的知识要掌握,特别是要理解为什么会这样?以及它的使用场景。本篇文章主要是结合常规面试题去讲解基础。现在来看看一些非常基础的面试题。
- 实现线程有几种方式?
- 如何启动线程?执行run()和start()的区别。
- 什么情况下才会发生线程安全问题?
- 怎么样解决线程安全问题?
以上问题是在网上搜的,也许还可以问得更细,比如多线程开启时,它们是同一时间运行的吗?再比如,是不是多线程就一定会发生线程安全问题?只要理解了多线程,无论面试官怎么样问,都能回答上。
多线程使用场景
应用场景有很多,比如打游戏和售票。打游戏时,如果对方打你,要等他打完你,你才能出招,这种事情你能忍?分分钟会爆粗口。这个时候就得用到多线程,同时对打才刺激。还有我们平时春节多个窗口售票,开售时候上千人抢几百张票,这也要用多线程才能实现。
实现线程的方式
实现线程的方式通常有2种
第一种方式是继承Thread
<code>public class Thread1 extends Thread {
@Override
public void run() {
super.run();
for (int i = 0 ; i< 1000 ; i++){
Log.i("thread","i======" +i);
}
}
}/<code>
第二种方式是实现Runnable接口
<code>public class Thread2 implements Runnable{
@Override
public void run() {
for (int j = 0 ; j< 1000 ; j++){
Log.i("thread","j--------------------------" +j);
}
}
} /<code>
启动线程
现在调用start()开启上面两个线程
<code>public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread1 thread1 = new Thread1();
thread1.start();
Thread thread2 = new Thread(new Thread2());
thread2.start();
}
}/<code>
通过查看Api文档,知道start()方法是启动线程。运行以后,看看打印的内容。
考虑到图太长,我只截取一部分,真实情况是一开始打印的全是i,直到i = 130的时候才开始打印j,j打印一会又开始打印i。同时开启,按道理应该i和j轮流打印?结果证明两个线程实际上并不是同一时间同时执行的。这就涉及到CPU对于时间的调度了,Thread1和Tread2就是两个任务,以单核cpu为例,我把这个过程简单归结为下图。
cpu可能先分配1ms给Task1执行,再到分配2ms给Task2执行,然后再分配10ms给Task1执行,以此类推。所以cpu并不是同时处理两个线程,而是同一时间段交替运行,但是由于处理的时候非常的快,以ms计算甚至更快,所以感觉两个任务是同时执行的。(cpu分配时间我们预估不了,这只是我随意取的时间)
将start()改为调用run()
<code>public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread1 thread1 = new Thread1();
thread1.run();
Thread thread2 = new Thread(new Thread2());
thread2.run();
}
}/<code>
运行后打印结果如下
我只截了部分,实际情况是打印完i以后,才开始打印j,这就说明,是执行完thread1.run();以后,才开始执行thread2.run();,这只是单纯的按顺序执行相应run方法里面的内容。调用run方法并不是开启线程,是执行run里面的内容,而start()是开启线程。
多线程会有可能发生什么问题
以多窗口售票为例子,假设有3个窗口售200张票,每个窗口排队的人都有1000人。
先写一个简易的售票系统。
<code>//火车售票系统
public class TicketSystem implements Runnable {
public static int ticketNum = 200;
@Override
public void run() {
for (int i = 1; i <= 1000; i++){//步骤1 1ms
if (ticketNum > 0){//步骤2 2ms
try {
Thread.sleep(50);//需要输入相关信息之类,需要时间,而且只是假设,没有这么快可以买到票的。 步骤3 50ms
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNum --;
System.out.println("恭喜您,成功抢到票,还剩下:"+ticketNum+"张票");
}
}
}
}/<code>
上面的代码很好理解,不多作解释,上面看不懂的注释可以先忽略,下面会介绍。开启3个窗口去抢票
<code>public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TicketSystem ts = new TicketSystem();
//创建3个窗口
Thread thread1 = new Thread(ts);
Thread thread2 = new Thread(ts);
Thread thread3 = new Thread(ts);
thread1.start();
thread2.start();
thread3.start();
}}/<code>
运行后看下打印结果,截取了部分打印结果。
出现了两次都剩一张票,还有剩下负数的票的情况,这就是多线程有可能导致的并发问题。三个窗口就是三个线程,三个线程同时开启。上面部分有提到,cpu是通过时间调度去交替执行这些任务。假设步骤1的for循环需要执行1ms,步骤2中的if条件判断语句需要执行2ms,步骤3的购买操作需要50ms。票还剩下最后一张的时候,线程thread1分配到的时间是2ms,刚执行完if语句,这个时候ticketNum还是为1,然后切换到thread2,分配的时间是54ms,刚好执行完买票操作,这个时候ticketNum已经为0,但是当thread1再执行的时候,它之前已经进入了if语句,会把剩下的代码执行完,ticketNum就为-1了,其它的情况也是同理。cpu分配的时间是我们不能掌控的,而三个线程同时操作的是同一数据ticketNum,这样引发了不正常的结果。
在文章最开始打印i和j的时候,也是开启了多线程,没有出现问题。在多窗口售票开启多线程,出现了问题,这两个例子的区别在哪里?区别在于多窗口售票,几个线程访问的是同一个共享数据,就是200张票,而i和j的例子,两个线程访问的数据是互不相关的。从这里就知道,并不能说多线程就一定会发生线程安全问题,当多个线程操作同一共享数据的时候,才会引发线程安全问题。
解决线程安全问题
上述的多线程共享了同一数据,出现了线程安全问题。我们不妨把这个问题想成火车上的乘客上厕所的问题,这是一个有点味道的例子,哈哈。整条车厢有20个人同时想使用厕所,而厕所只有一个可以使用,大家是不是得要共享这个厕所?不可能让20个人同时一起上厕所,所以在设计厕所的时候会加锁,只要有一个人进去,把门锁住,不管外面的人有多着急,也必须等里面的人开锁出来,下一个人才能进去。程序也是来源于生活 ,解决线程安全问题,我们可以在公共的核心部分加一把锁。代码如下:
<code>public class TicketSystem implements Runnable {
public static int ticketNum = 200;
@Override
public void run() {
for (int i = 1; i <= 1000; i++){//步骤1 1ms
synchronized (TicketSystem.class){
if (ticketNum > 0){//步骤2 2ms
try {
Thread.sleep(50);//需要输入相关信息之类,需要时间,而且只是假设,没有这么快可以买到票的。 步骤3 50ms
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNum --;
System.out.println("恭喜您,成功抢到票,还剩下:"+ticketNum+"张票");
}}
}
}
}/<code>
再运行就没有问题了。是不是感觉很简单?因为java语言提供了这个解决办法,不用我们自己实现。简单的问题要力求做到最好,上了锁就会影响运行效率,所以我们只给核心部分上锁,核心部分越细越好,节省时间。
文章写到这里,开篇问的几个问题也有了答案,现在来简短的答一下。
1. 实现线程的几种方式?
通常有两种方式,继承Thread,实现Runnable接口
2. 如何启动线程?执行run()和start()的区别。
调用start()。执行run()是执行方法里面的内容,start()才是开启线程。
3. 什么情况下才会发生线程安全问题?
当多个线程操作同一共享数据的时候。
4. 怎么样解决线程安全问题?
加锁,给公共核心部分加锁。
以上只是给出很简短的答案,真正面试的时候还是要加上自己的理解。任何面试都一样,只有理解了知识,才能正确的去回答问题,死记硬背答案是不可行的。
关于多线程就写到这里了。最近疫情还在持续,大家一起加油,坚持到可以脱口罩敲码那天。
最后
最后我想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
相信它会给大家带来很多收获:
【Android进阶学习视频】、【全套Android面试秘籍PDF】、【Android开发核心知识点笔记】可以 私信我【安卓】免费获取!
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
閱讀更多 Android高級架構師 的文章