Java笔试题多线程部分简答

1,536 阅读8分钟

1.谈谈你对多线程的理解


一个类继承了Thread类之后,那么此类就具备了多线程的操作功能。

线程的三点必要因素:

class MyThread extends Thread{     //第一、要继承Thread
     private String name;
     public MyThread(String name) {
           this.name = name;
     }
     public void run(){     //第二、要实现run()方法
           for(int i=0; i<5; i++){
                System.out.println(name +"运行,i=" +i);
           }
     }
}
public class ThreadDemo {
     public static void main(String [] args){
           MyThread mt1 = new MyThread("线程1");
           MyThread mt2 = new MyThread("线程2");
           mt1.start();    //第三、要使用start()方法启动线程
           mt2.start();
     }
}


缺点:不能实现资源共享,所以在系统中通常使用Runnable来实现多线程

一个类实现了Runnable之后,那么此类就具备了多线程的操作功能。

class MyThread extends Runnable{     //第一、要继承Runnable
     private String name;
     public MyThread(String name) {
           this.name = name;
     }
     public void run(){    //第二、要实现run()方法
           for(int i=0; i<5; i++){
                System.out.println(name +"运行,i=" +i);
           }
     }
}
public class ThreadDemo {
     public static void main(String [] args){
           MyThread mt1 = new MyThread("线程1");
           MyThread mt2 = new MyThread("线程2");
           new Thread(mt1).start();  //第三、要使用start()方法启动线程
           new Thread(mt2).start();
     }
}

java中创建线程的方式:

  • 继承Thread并重写run方法

  • 实现Runnable并重写run方法,然后作为参数传入Thread

  • 实现Callable,并重写call(),call方法有返回值。使用FutureTask包装Callable实现类,其中FutureTask实现了Runnable和Future接口,最后将FutureTask作为参数传入Thread中

  • 由线程池创建并管理线程。

2.在线程里该如何返回值,其实就是callable runnable 区别。

Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。

其中前两种方式线程执行完后都没有返回值,后两种是带返回值的。Callable接口支持返回执行结果,需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取结果;当不调用此方法时,主线程不会阻塞!

Runnable和Callable的区别

  1. Callable规定的方法是call(),而Runnable规定的方法是run()。

  2. 实现Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回。

  3. 执行结果call方法可以抛出异常,run方法若有异常只能在内部消化。

  4. 运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。

  5. 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。

  6. 通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。


3.通过ThreadLocal或volatile关键字,来说明线程的内存模型。

ThreadLocal,线程本地变量也可以叫线程本地存储,其为每一个线程维护了一个独立的变量副本。将对象的可见范围限制在同一个线程内。原理是:在每个线程中都存有一个本地ThreadMap,相当于存了一个对象的副本,key为threadlocal对象本身,value为需要存储的对象值,这样各个线程之间对于某个成员变量都有自己的副本,不会冲突。

volatile保证了成员变量在多线程下的可见性和有序性,不保证原子性,使用volatile关键字的时候,该变量一旦被修改,会立即写入到主存中,同时会让其他线程的工作内存中的缓存失效,这样,其他线程在访问该变量的时候会重新从主存中读取可以获得该变量最新的数据,从而保证的变量的可见性。



4.wait和sleep,yield区别和联系

Sleep是线程类Thread 的方法,它是使当前线程暂时睡眠,可以放在任何位置,Sleep使用的时候,线程并不会放弃对象的使用权,所以在同步方法或同步块中使用sleep,一个线程访问时,其他的线程无法访问。sleep只是暂时休眠一定时间,时间到了之后,自动恢复运行,不需另外的线程唤醒.

wait是使当前线程暂时放弃对象的使用权进行等待,必须放在同步方法或同步块里,wait会释放当前线程,放弃对象的使用权,让其他的线程也可以访问,线程执行wait方法时,需要其他线程调用Monitor.Pulse()或者Monitor.PulseAll() 通过monitor监视器进行唤醒或者是通知等待的队列。

所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;其他线程无法访问
而wait()等待时,释放对象锁。其他线程可以访问


5.线程的基本状态以及状态之间的关系?

一个新的线程创建之后通过start()方法进入运行状态,在运行状态中可以使用yield()进行礼让,但是仍然可以进行,如果现在一个线程需要暂停的话,可以使用suspend(),sleep(),wait(),如果现在线程不需要再执行,则可以通过stop()结束(如果run()方法执行完毕也表示结束),或者一个新的线程直接调用stop()方法也可以进行结束。



6.两个线程可以对同一个ArrayList进行add操作吗?会出现什么结果?

public class A{
    static List<Integer> list = new ArrayList<>();
    static class BB implements Runable{
        @Override
        public void run(){
            for(int j=0; j<100;j++){
                list.add(j);
            }
        }
    }
    public static void mian(String [] args){
        BB b = new BB();
        Thread t1 = new Thread(b);
        Thread t2 = new Thread(b);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(list.size())
    }
}


比如上面的例子,打印的结果不一定是200.

因为ArrayList不是线程安全的,问题出在add方法上。

public boolean add(E e){
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    rerurn true;
}


上面的程序,可能有三种情况发生:

  • 数组下标越界,首先要检查容量,必要时进行扩容。每当在数组边界处,如果A线程和B线程同时进入并检查容量,也就是它们都执行完ensureCapacityInternal方法,因为还有一个空间,所以不进行扩容,此时如果A暂停下来,B成功自增;然后接着A从 elementData[size++]=e开始执行,由于A之前已经检查过没有扩容,而B成功自增使得现在没有空余空间了,此时A就会发生数组下标越界。

  • 小于200,size++可以看成是 size=size+1,这一行代码包括三个步骤,先读取size,然后将size加1,最后将这个新值写回到size。此时若A和B线程同时读取到size假设为10,B先自增成功size变11,然后回来A因为它读到的size也是10,所以自增后写入size被更新成11,也就是说两次自增,实际上size只增大了1。因此最后的size会小于200。

  • 200,运气很好,没有发生以上的情况。


7.volatile和synchronized讲一下?

synchronized保证了当有多个线程同时操作共享数据时,任何时刻只有一个线程能进入临界区操作共享数据,其他线程必须等待。因此它可以保证操作的原子性。synchronized通过同步锁保证线程安全,进入临界区前必须获得对象的锁,其他没有获得锁的线程不可进入。当临界区中的线程操作完毕后,它会释放锁,此时其他线程可以竞争锁,得到锁的那个线程便可以进入临界区。

synchronized还可以保证可见性。因为对一个变量的unlock操作之前,必须先把次变量同步回主内存中。它还可以保证有序性,因为一个变量在任何时刻只能有一个线程对其进行lock操作(也就是任何时刻只有一个线程可以获得该锁对象),这决定了持有同一把锁的两个同步块只能串行进入。

volatile是一个关键字,用于修饰变量。被其修饰的变量具有可见性和有序性。

可见性,当一条线程修改了这个变量的值,新值能被其他线程立刻观察到。具体来说,volatile的作用是:在本CPU对变量的修改直接写入主内存中,同时这个写操作使得其他CPU中对应变量的缓存行无效,这样其他线程在读取这个变量时候必须从主内存中读取,所以读取到的是最新的,这就是上面说得能被立即“看到”。

有序性,volatile可以禁止指令重排。volatile在其汇编代码中有一个lock操作,这个操作相当于一个内存屏障,指令重排不能越过内存屏障。具体来说在执行到volatile变量时,内存屏障之前的语句一定被执行过了且结果对后面是已知的,而内存屏障后面的语句一定还没执行到;在volatile变量之前的语句不能被重排后其之后,相反其后的语句也不能被重排到之前。

作者:junxia

如果您认为阅读这篇博客让您有些收获,欢迎扫描下方二维码关注我们,与我们交流互动。

本公众号专注Java面试题目的收集和解析以及【Web项目】源码的分享。 


以下为公众号部分截图。