java基础对于学习安卓是很重要的,比如说线程,多线程。我们做安卓开发可能不太需要去研究高并发这些高深的问题,但是基础的知识要掌握,特别是要理解为什么会这样?以及它的使用场景。本篇文章主要是结合常规面试题去讲解基础。现在来看看一些非常基础的面试题。
以上问题是在网上搜的,也许还可以问得更细,比如多线程开启时,它们是同一时间运行的吗?再比如,是不是多线程就一定会发生线程安全问题?只要理解了多线程,无论面试官怎么样问,都能回答上。
应用场景有很多,比如打游戏和售票。打游戏时,如果对方打你,要等他打完你,你才能出招,这种事情你能忍?分分钟会爆粗口。这个时候就得用到多线程,同时对打才刺激。还有我们平时春节多个窗口售票,开售时候上千人抢几百张票,这也要用多线程才能实现。
实现线程的方式通常有2种
public class Thread1 extends Thread { @Override public void run() { super.run(); for (int i = 0 ; i< 1000 ; i++){ Log.i("thread","i======" +i); } } } 复制代码
public class Thread2 implements Runnable{ @Override public void run() { for (int j = 0 ; j< 1000 ; j++){ Log.i("thread","j--------------------------" +j); } } } 复制代码
现在调用 start()
开启上面两个线程
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(); } } 复制代码
通过查看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()
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(); } } 复制代码
运行后打印结果如下
我只截了部分,实际情况是打印完i以后,才开始打印j,这就说明,是执行完 thread1.run();
以后,才开始执行 thread2.run();
,这只是单纯的按顺序执行相应run方法里面的内容。
run
方法并不是开启线程,是执行 run
里面的内容,而 start()
是开启线程。
以多窗口售票为例子,假设有3个窗口售200张票,每个窗口排队的人都有1000人。
先写一个简易的售票系统。
//火车售票系统 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+"张票"); } } } } 复制代码
上面的代码很好理解,不多作解释,上面看不懂的注释可以先忽略,下面会介绍。开启3个窗口去抢票
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(); }} 复制代码
运行后看下打印结果,截取了部分打印结果。
出现了两次都剩一张票,还有剩下负数的票的情况,这就是多线程有可能导致的并发问题。三个窗口就是三个线程,三个线程同时开启。上面部分有提到,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个人同时一起上厕所,所以在设计厕所的时候会加锁,只要有一个人进去,把门锁住,不管外面的人有多着急,也必须等里面的人开锁出来,下一个人才能进去。程序也是来源于生活 ,解决线程安全问题,我们可以在公共的核心部分加一把锁。代码如下:
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+"张票"); }} } } } 复制代码
再运行就没有问题了。是不是感觉很简单?因为java语言提供了这个解决办法,不用我们自己实现。 简单的问题要力求做到最好,上了锁就会影响运行效率,所以我们只给核心部分上锁,核心部分越细越好,节省时间。
文章写到这里,开篇问的几个问题也有了答案,现在来简短的答一下。
通常有两种方式,继承Thread,实现Runnable接口
run()
和 start()
的区别。 调用 start()
。执行 run()
是执行方法里面的内容, start()
才是开启线程。
当多个线程操作同一共享数据的时候。
加锁,给公共核心部分加锁。
以上只是给出很简短的答案,真正面试的时候还是要加上自己的理解。任何面试都一样,只有理解了知识,才能正确的去回答问题,死记硬背答案是不可行的。
关于多线程就写到这里了。最近疫情还在持续,大家一起加油,坚持到可以脱口罩敲码那天。