部分容易被问到的面试题总结(二——线程部分)

今天和大家继续分享“Java并发”相关面试题。
咱们长话短说:

一、说一说对线程的理解:

(一)线程是进程的子集,一个进程可以有很多线程。每个进程都有自己的内存空间、可执行代码、唯一进程标识符—PID;

(二)每条线程各司其职。不同的进程使用不同的内存空间(线程自己的堆、栈),而所有的线程共享一片相同的内存空间(即进程主内存)。这与栈内存不同,每个线程都拥有各自的栈来存储本地数据。
【追问:线程自己的堆栈中堆区域是否和进程的主内存是何关系?】

二、如何实现多线程?

常用的有五种:
(一)继承Thread类:Java单继承,此处不推荐;
(二)实现Runnable接口:Thread类也是继承Runnable接口,推荐;
(三)使用匿名内部类:又分两种方式,1:基于继承Thread类以子类的方式实现;
2:基于实现Runnable接口,将实现这个接口的类作为参数传递给Thread()方法即可。
(四)实现Callable接口:实现Callable接口,配合FutureTask使用(FutureTask封装Callable实现类的实现并将其作为Thread对象的target创建并启动线程。),有返回值(返回值是通过调用FutureTask对象的get()方法来获取子线程的执行结果的返回值);
(五)使用线程池:提高线程复用,节约稀缺的线程资源;
1、何为线程池?如何使用?
线程池就是事先将多个线程对象放到容器中,当使用的时候就不用new线程而是直接在池中拿就是(原理相似于“令牌桶”或“IOC”),既节省了开辟子线程的时间,也提高了代码的执行效率。

在JDK的java.util,concurrent.Executors中提供了生成多种线程池的静态方法:
①ExecutorService new CachedThreadPool=Executors.new CachedThreadPool();这种方法是创建一个可缓存的线程池,此线程池不会对大小做限制,线程池的大小完全依赖于JVM的性能;
②ExecutorService new Fixed Thread Pool=Executors.newFixedThreadPool(4-----最大线程的数量为4);创建一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大限制4,数字可改;
③Scheduled ExecutorService new Scheduled Thread Pool=Executors.new ScheduledThreadPool(4);创建一个大小无限的线程池,支持定时以及周期性执行任务的需求;
④Executor Service new Single Thread Executor=Executors.new SingleThreadExecutor();创建一个单线程的线程池,保证所有任务的执行顺序按任务提交的顺序执行(先到先得);

关闭线程池:
其实对于关闭线程池的问题被问到的频率很高,目前来说,关闭线程池问题一直难以定论。从一开始的stop(停止)到interrupt(打断),再到现在的shutdown/shutdownnow,都没有一个标准,不过可以确定的是stop已经过时,因为他总是引起并发症。最常用的是shutdown,即拒绝接受新的命令的同时执行完已接收的命令(善始善终);而shotdownnow是拒绝接受新命令也直接废掉正在执行的线程。
所以要关闭线程池,其实是优雅的关闭线程。

用线程池的意义:
在开发过程中,合理的利用线程池有助于
①降低资源消耗;
②提高响应速度;
③提高线程的可管理性,进一步统一分配、调优和监控。

最好再补充一下“线程同步(有五种)”方面的知识点:
1:同步方法(synchronized);
2:同步代码块(synchronized);
3:使用特殊变量(volatile);
4:是用重入锁(ReetrantLock类,重入即可重复共用);
5:使用局部变量(ThreadLocal,即副本并行);

三、讲一讲死锁,如何处理死锁?

(一)死锁:
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。但是该资源又被其他线程拥有无法释放,则形成无期阻塞,因此程序不可能正常终止。导致死锁现象。
最经典的案例见代码:

public class DeadLockTest {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {//箭头函数指定范围
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出结果出现:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

由代码可见:
线程A通过synchronized(资源1)获得资源1的监视器锁,然后通过sleep(1000)让线程A休眠,为了让线程B得以执行并获取资源2的监视器锁。线程A和B休眠结束后,都开始请求对方的资源,二者将会陷入互相等待状态,即死锁。上述代码符合死锁的四个必要条件:
1、互斥:该资源任意一个时刻只有一个线程拥有;
2、不剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有使用完后才能释放资源;
3、请求与保持:一个进程因请求资源而阻塞,对已获得的资源会保持不释放。
4、循环等待:若干进程之间形成一种无限等待(谁不让着谁,看谁耗得起谁)。

出现死锁的话,只能说明前期准备不充足;那么我们应该在编程时便考虑:
(二)如何避免死锁?
很简单,上面已经说了,死锁的产生需要四个必要条件,缺一不可。那么我们只需要设法破坏这些条件即可:
1、破坏互斥条件:无法破坏,因为锁的目的就是让线程互斥(临界资源需要互斥访问)。
2、破坏请求与保持条件:一次性申请所有资源;
3、破坏不剥夺条件:占用部分资源的线程再申请其他资源时,若申请不到,可以主动释放自己占有的资源(车让人,人快走,文明又畅通!)。
4、破坏循环等待条件:靠按序申请资源来预防。按照某一顺序申请资源,释放资源则反序释放(先到后出,类似于栈。和大家一起温故一下堆、栈、静态内存区域的区别:
【堆和栈的区别
  一、堆栈空间分配区别:
  1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
  2、堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
  二、堆栈缓存方式区别:
  1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
  2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
  三、堆栈数据结构区别:
  堆(数据结构):堆可以被看成是一棵树,如:堆排序;
  栈(数据结构):一种先进后出的数据结构。】)。即可破坏循环等待条件。
  针对于上述的代码,我们进行修改使其避免死锁:

new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();

输出结果将变为:

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0

这样的话,线程A先获得资源1的监视器锁,同时线程B获取不到了。之后线程A再去获取资源2 的监视器锁,便可以拿到。随后A用完后解除了对资源1、资源2的监视器锁的占用,线程B就可以快乐的“退一步海阔天空”拿到想要的资源进行对应操作了。即可避免死锁。

四、如何在Java中获取线程堆栈?

不同的JVM,有多种方法来获得Java进程的线程堆栈。获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。
Windows可以使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。你也可以用jstack这个工具来获取,它对线程id进行操作,用jps这个工具找到id。

五、为什么wait, notify 和 notifyAll这些方法不在Thread类里面?

JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁,那么调用对象中的wait()方法。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中,因为锁属于对象。

(感谢大家耐心阅读,若有更好的经验,期待分享。)

六、多线程交叉打印的问题:

思路:主线程打印完毕后,唤醒子线程打印,同时自身进入等待状态;子线程打印后,唤醒等待的主线程,同时自身进入等待状态,如此便可以进行交替打印,为了打印过程中不被中断,必须处理同步问题。
一般常见的让线程按照顺序执行,是使用join()方法或者直接用单线程模式的线程池(记得关闭线程池)。但此处的交叉打印将这个顺序打印提高了一个难度,咱们来看一下:

这里主要介绍三种方案来解决:一是wait和notify机制;二是使用Lock;三是使用await()和signalAll()方法

一.采用wait和notify

public class Test {
    
    /**
     * 主线程打印20次,然后子线程打印10次,如此交替打印30次 
     * @throws Exception 
     */
    public static void main(String[] args) throws Exception {
        
        final Service service=new Service();
        
        new Thread(){
            
            public void run() {
                for (int i = 0; i < 30; i++) {
                    try {
                        service.subPrint(i);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            
        }.start();
        
        for (int i = 0; i < 30; i++) {
            service.mainPrint(i);
        }
    }
    
    public static class Service{
        private boolean shouldBeMain=true;
    
        /**
         * 主线程打印,注意同步
         */
        public synchronized void mainPrint(int loopth) throws Exception{
            
            while(!shouldBeMain){//不该主线程
                System.out.println("主线程等待:"+loopth);
                this.wait();
            }
            
            Thread.sleep(1000);
            for (int i = 0; i < 20; i++) {
                System.out.println("主线程打印:"+i+",第"+loopth+"次循环");
            }
            
            shouldBeMain=false;
            
            this.notify();
            
        }
        
        
       public synchronized void subPrint(int loopth) throws Exception{
           while(shouldBeMain){
               System.out.println("子线程等待:"+loopth);
               this.wait();
           }
           
           Thread.sleep(1000);
           
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程打印:"+i+",第"+loopth+"次循环");
            }
            
            shouldBeMain=true;
            
            this.notify();
            
        }
        
    }

}

二.采用ReentrantLock

public class Test2 {

    /**
     * 主线程打印20次,然后子线程打印10次,如此交替打印30次 
     * @throws Exception 
     */
    public static void main(String[] args) throws Exception {
        
        final Service service=new Service();
        
        //子线程打印
        new Thread(){
            
            public void run() {
                for (int i = 0; i < 30; i++) {
                    try {
                        service.subPrint(i);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            
        }.start();
        
        //主线程打印
        for (int i = 0; i < 30; i++) {
            service.mainPrint(i);
        }
    }
    
    public static class Service{
        private ReentrantLock lock=new ReentrantLock();//创建重入锁
        private Condition condition=lock.newCondition();
        
        private boolean shouldBeMain=true;
    
        public  void mainPrint(int loopth) throws Exception{
            //上锁
            lock.lock();
            
            while(!shouldBeMain){//不该主线程
                System.out.println("主线程等待:"+loopth);
                condition.await();
            }
            
            Thread.sleep(1000);
            for (int i = 0; i < 20; i++) {
                System.out.println("主线程打印:"+i+",第"+loopth+"次循环");
            }
            
            shouldBeMain=false;
            
            condition.signal();
            //解锁
            lock.unlock();
            
        }
        
       public  void subPrint(int loopth) throws Exception{
           lock.lock(); 
           while(shouldBeMain){
               System.out.println("子线程等待:"+loopth);
               condition.await();
           }
           
           Thread.sleep(1000);
           
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程打印:"+i+",第"+loopth+"次循环");
            }
            
            shouldBeMain=true;
            
            condition.signal();
            
            lock.unlock();
            
        }
        
    }
    
}

三、使用await()和signalAll()方法

public class MyService {
 private Lock lock = new ReentrantLock();
 private Condition condition = lock.newCondition();
 private boolean flag = false;
 public void printA() {
 try {
  lock.lock();
  while (flag) {
  condition.await();
  }
  for (int i = 0; i < 5; i++) {
  System.out.println("printA...");
  TimeUnit.SECONDS.sleep(1);
  }
  flag = true;
  condition.signalAll();
 } catch (InterruptedException e) {
  e.printStackTrace();
 } finally {
  lock.unlock();
 }
 }
 public void printB() {
 try {
  lock.lock();
  while (!flag) {
  condition.await();
  }
  for (int i = 0; i < 5; i++) {
  System.out.println("printB...");
  TimeUnit.SECONDS.sleep(1);
  }
  flag = false;
  condition.signalAll();
 } catch (InterruptedException e) {
  e.printStackTrace();
 } finally {
  lock.unlock();
 }
 }
}

public class ThreadA implements Runnable {
 private MyService myService;
 public ThreadA(MyService myService) {
 super();
 this.myService = myService;
 }
 @Override
 public void run() {
 myService.printA();
 }
}

public class ThreadA implements Runnable {
 private MyService myService;
 public ThreadA(MyService myService) {
 super();
 this.myService = myService;
 }
 @Override
 public void run() {
 myService.printA();
 }
}

public class ThreadA implements Runnable {
 private MyService myService;
 public ThreadA(MyService myService) {
 super();
 this.myService = myService;
 }
 @Override
 public void run() {
 myService.printA();
 }
}

再复杂点,如果对一个字符串’adasdfsafwfvgs’两个线程交替输出字符,最后要求A线程执行完任务输出:“A线程打印完了”,B线程执行完任务输入:“B线程打印完了”,最后有主线程输出一句话“我打印完了”!

@Data
public class TestThread {
    volatile static boolean open=false;
    volatile static int index=0;
    static String s="adasdfsafwfvgs";

    private ThreadA threadA;
    private ThreadB threadB;
    public TestThread(){
        this.threadA=new ThreadA();
        this.threadB=new ThreadB();
    }

    public  class ThreadB extends Thread {
        @Override
        public void run(){
            while(index<s.length()){
                if(open){
                    System.out.println(Thread.currentThread().getName()+"-----"+s.charAt(index++));
                    open=false;
                }
            }
            System.out.println("b线程打印完了");
        }
    }

    public class ThreadA extends Thread {
        @Override
        public void run(){
            while(index< s.length()){
                if(!open){
                    System.out.println(Thread.currentThread().getName()+"-----"+s.charAt(index++));
                    open=true;
                }
            }
            System.out.println("a线程打印完了");
        }
    }

}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = KafkaClientApplication.class)
public class TestThreadTest {

    /**
     * 测试使用两个线程对字符串交替输出字符的代码
     */
    @Test
    public void test(){
        TestThread testThread=new TestThread();
        TestThread.ThreadA threadA = testThread.getThreadA();
        TestThread.ThreadB threadB = testThread.getThreadB();
        threadA.setName("threadA");
        threadB.setName("threadB");
        threadA.start();
        threadB.start();
        try {
            threadA.join();
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我打印完了");
    }
}

更多详细的解法,如利用 AtomicInteger 和 volatile的方法,请参考线程交替打印方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值