面试官:java基础怎么样?多线程一定会引发多线程安全问题吗?说说你的理解

1,809 阅读7分钟

java基础对于学习安卓是很重要的,比如说线程,多线程。我们做安卓开发可能不太需要去研究高并发这些高深的问题,但是基础的知识要掌握,特别是要理解为什么会这样?以及它的使用场景。本篇文章主要是结合常规面试题去讲解基础。现在来看看一些非常基础的面试题。

  1. 实现线程有几种方式?
  2. 如何启动线程?执行run()和start()的区别。
  3. 什么情况下才会发生线程安全问题?
  4. 怎么样解决线程安全问题?

以上问题是在网上搜的,也许还可以问得更细,比如多线程开启时,它们是同一时间运行的吗?再比如,是不是多线程就一定会发生线程安全问题?只要理解了多线程,无论面试官怎么样问,都能回答上。

多线程使用场景

应用场景有很多,比如打游戏和售票。打游戏时,如果对方打你,要等他打完你,你才能出招,这种事情你能忍?分分钟会爆粗口。这个时候就得用到多线程,同时对打才刺激。还有我们平时春节多个窗口售票,开售时候上千人抢几百张票,这也要用多线程才能实现。

实现线程的方式

实现线程的方式通常有2种

第一种方式是继承Thread

public class Thread1 extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0 ; i< 1000 ; i++){
            Log.i("thread","i======" +i);
        }
    }
}

第二种方式是实现Runnable接口

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语言提供了这个解决办法,不用我们自己实现。简单的问题要力求做到最好,上了锁就会影响运行效率,所以我们只给核心部分上锁,核心部分越细越好,节省时间。

文章写到这里,开篇问的几个问题也有了答案,现在来简短的答一下。

  1. 实现线程的几种方式?

通常有两种方式,继承Thread,实现Runnable接口

  1. 如何启动线程?执行run()start()的区别。

调用start()。执行run()是执行方法里面的内容,start()才是开启线程。

  1. 什么情况下才会发生线程安全问题?

当多个线程操作同一共享数据的时候。

  1. 怎么样解决线程安全问题?

加锁,给公共核心部分加锁。

以上只是给出很简短的答案,真正面试的时候还是要加上自己的理解。任何面试都一样,只有理解了知识,才能正确的去回答问题,死记硬背答案是不可行的。

关于多线程就写到这里了。最近疫情还在持续,大家一起加油,坚持到可以脱口罩敲码那天。