首页 > 编程语言 >【Java】多线程面试题总结

【Java】多线程面试题总结

时间:2023-08-02 23:44:07浏览次数:55  
标签:面试题 Java Thread System 线程 println 多线程 public out

最近在看面试题,所以想用自己的理解总结一下,便于加深印象。

为什么使用多线程

  1. 使用多线程可以充分利用CPU,提高CPU的使用率。
  2. 提高系统的运行效率,对于一些复杂或者耗时的功能,可以对其进行拆分,比如将某个任务拆分了A、B、C三个子任务,如果子任务之间没有依赖关系,那么就可以使用多线程同时运行A、B、C三个子任务,来提高效率。
  3. 可以通过多线程处理一些异步任务。

创线程的方式有哪些

(1)继承Thread方式创建

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("run task by Thread");
    }
}

启动线程:

public class Test {
    public static void main(String[] args) {
        // 创建线程
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();
    }
}

(2)通过实现Runnable的方式创建

class Task implements Runnable{
    @Override
    public void run() {
         System.out.println("run task by Runnable");
    }
}

启动线程:

public class Test {
    public static void main(String[] args) {
         // 创建线程
        Thread myTask = new Thread(new Task());
         // 启动线程
        myTask.start();
    }
}

(3)如果需要返回值,还可以使用Callable+FutureTask的方式(也可以通过Callable+Future配合线程池使用):

class Task implements Callable<Integer>{

    /**
     * 相当于run方法
     */
    @Override
    public Integer call() throws Exception {
        Thread.sleep(5000);
        int sum = 0;
        for(int i=0; i<10; i++) {
            sum += i;
        }
        return sum;
    }
}

运行线程:

public class FutureTest {
    public static void main(String[] args) {
        // 创建FutureTask
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Task());
        // 创建线程
        Thread thread = new Thread(futureTask);
        // 启动线程
        thread.start();
        try {
            // get()方法可以获取返回结果,它会阻塞到任务执行完毕
            System.out.println("运行结果:" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Runnable和Callable的区别
相同点

(1)创建线程通常使用实现接口的方式,面向接口编程,这样在开发过程中可以提高系统的扩展性和灵活性,Runnable和Callable都是接口;
(2)Runnable和Callable都需要通过Thread.start()启动线程;

不同点
(1)Runnable的run方法没有返回值只能返回void,并且不能向上抛出异常,而Callable的call方法既可以有返回值,也可以向上抛出异常。

如何解决线程安全问题

  1. 提供了syncronized关键字来加锁;
  2. 提供了Lock接口来加锁;
  3. volatile关键字,可以保证可见性,需要注意它保证的只是可见性,并不能保证原子性,如果是非原子性的操作,单纯使用volatile并不能保证线程的安全;
  4. java.util.concurrent包下提供了一系列类,比如一些并发容器ConcurrentHashMap等;

syncronized和Lock的区别

  1. syncronized是Java中的关键字,而Lock是一个接口;
  2. syncronized在发生异常时,可以自动释放线程持有的锁,Lock在发生异常时,如果没有通过unLock方法释放锁,则不会自动释放锁,所以在使用Lock的时候注意在finally中调用unLock去释放锁,否则有可能导致死锁的发生;
  3. Lock调用lockInterruptibly在等待抢占某个锁的过程中,可以响应中断,而synchronized不能,会一直等待下去,不能响应中断;
  4. Lock通过tryLock方法可以设置等待时间,在设定的时间内如果未能成功获取锁,将放弃抢占锁,并返回false,如果成功获取锁返回true,通过返回值可以判断是否获取到锁,而syncronized没有这个功能;

syncronized实现原理

syncronized可以加在方法上,对整个方法实现同步访问,也可以使用在代码块中,对某块代码进行同步访问。

(1)普通方法

对于普通方法,锁住的是当前实例对象:

public class SyncTest {

    private int i = 0;

    public synchronized void add() {
        System.out.println("【synchronized】");
        i++;
    }
}

在编译的字节码中,可以看到方法的flag中有ACC_SYNCHRONIZED标记:

(2)静态方法

对于静态方法,锁住的是当前类的class对象:

public class SyncTest {

    private static int j = 0;

    // 静态方法,锁住的是SyncTest.class
    public static synchronized void add() {
        System.out.println("【static synchronized】");
        j++;
    }
}

同样在编译的字节码中,可以看到ACC_SYNCHRONIZED标记:

(3)同步代码块

对于同步代码块,锁住的是括号中的对象:

public class Test {

    private int i = 0;

    public void add() {
        // 锁住的是括号内的对象,这里锁住的是Test.class
        synchronized (Test.class) {
            i++;
        }
    }
}

在编译后的字节码中,可以看到添加了monitorenter和monitorexit指令:

每个实例对象和类的Class对象都会关联一个Monitor,Monitor字面翻译为监视器,也可以称之为管程,在Java中就是通过它来实现多线程下的同步访问的。

对于同步代码块,在编译后的字节码中看到添加了monitorenter和monitorexit指令,当执行到monitorenter时,就会去获取锁住对象的Monitor锁,它可以保证在同一时刻只能有一个线程获取到锁,获取成功之后才可以往下进行,monitorexit指令执行时会释放掉Monitor锁。

对于同步方法,执行时会检查方法的flag中是否有ACC_SYNCHRONIZED标记,如果有同样会获取对应的Monitor锁,并在方法执行完毕之后释放锁。

Thread中的一些常用方法

sleep方法

sleep方法用于使线程进入睡眠状态,它可以指定睡眠的时间,并且会抛出InterruptedException异常,因为sleep方法会进入阻塞状态,处于阻塞状态的线程如果被中断,会抛出抛出InterruptedException异常。

sleep方法如果持有某个锁之后进入阻塞状态,此时并不会释放锁。

        @Override
        public void run() {
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

interrupt方法

interrupt方法可以中断处于阻塞状态的线程,处于阻塞状态的线程被中断时会抛出一个InterruptedException异常。

public class IntteruptTest {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("开始sleep");
                    // 进入睡眠
                    Thread.currentThread().sleep(10000);
                    System.out.println("结束sleep");
                } catch (InterruptedException e) { // 如果被中断
                    System.out.println("sleep被中断");
                    e.printStackTrace();
                }
                System.out.println("执行完毕");
            }
        };
        // 启动线程
        thread.start();
        // 中断线程
        thread.interrupt();
    }
}

输出:

开始sleep
sleep被中断
执行完毕
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at Thread.IntteruptTest$1.run(IntteruptTest.java:25)
interrupted()方法和isInterrupted()方法

相同点

interrupted()方法和isInterrupted()方法都可以用来获取线程的中断状态,中断状态表示线程是否被中断,初始化及未被中断过的时候,中断状态值为false,当线程被中断后,中断状态为true。

不同点

(1)调用方式不一样:

  • interrupted()是Thread类中的方法,可以直接通过Thread.interrupted()进行调用;
  • isInterrupted()方法需要获取到当前的线程再继续调用Thread.currentThread().isInterrupted()

(2)interrupted()方法调用的时候会判断中断状态的值,如果已经被中断过(值为true),会将其恢复为false状态。

public class Test {
    public static void main(String[] args) {
        // 初始状态,当前线程未被中断过,所以返回false
        System.out.println("1. " + Thread.currentThread().isInterrupted());
        // 当前线程未被中断过,同样返回false
        System.out.println("2. " + Thread.interrupted());
        // 中断当前线程
        Thread.currentThread().interrupt();
        // 线程被中断,此时isInterrupted()返回true
        System.out.println("3. " + Thread.currentThread().isInterrupted());
        // interrupted同样返回true,表示线程被中断过,处于此状态时,它会将中断标识的状态由true置为false,恢复未中断的状态
        System.out.println("4. " + Thread.interrupted());
        // 由于中断状态被上一步的Thread.interrupted()恢复为了false,所以这里返回false
        System.out.println("5. " + Thread.currentThread().isInterrupted());
        // 中断状态已经处于false状态,所以同样返回false
        System.out.println("6. " + Thread.interrupted());
    }
}

输出:

1. false
2. false
3. true
4. true
5. false
6. false

从源码中可以看到 isInterrupted调用的是navtive的isInterrupted方法来实现的:

public class Thread implements Runnable {
   public boolean isInterrupted() {
       // 调用原生的isInterrupted
        return isInterrupted(false);
   }

    // native方法
    private native boolean isInterrupted(boolean ClearInterrupted);
}

interrupted()方法,底层也是调用了navtive的isInterrupted方法,只不过在参数中传入了true,表示恢复中断标识:

public class Thread implements Runnable {
    public static boolean interrupted() {
        // isInterrupted是native方法
        return currentThread().isInterrupted(true);
    }
}

join方法

如果调用某个Thread对象的join方法时,会将调用者所处的线程进入阻塞状态,直到Thread对象中的任务运行完毕,再恢复调用者的线程继续往下运行。

来看个例子:

public class Test {
    public static void main(String[] args) {
        Thread taskOne = new Thread(new TaskOne());
        taskOne.start();
        System.out.println("Main线程");
    }
}

class TaskOne implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            System.out.println("任务一运行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

Main线程
任务一运行完毕

从输出结果中可以看到Main线程打印之后,任务一才运行完毕,假如需要等任务一结束之后才执行Main线程,就可以使用到join方法:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread taskOne = new Thread(new TaskOne());
        taskOne.start();
        // 当前线程是Main线程,调用taskOne的join方法时,会使Main线程进入阻塞状态,直到taskOne的run方法中的任务执行完毕
        taskOne.join();
        // taskOne任务结束之后,恢复Main线程的运行
        System.out.println("Main线程");
    }
}

输出:

任务一运行完毕
Main线程

CountDownLatch和CyclicBarrier的区别

CountDownLatch

CountDownLatch用于某个线程等待其他线程的任务执行完毕之后再执行。
在创建CountDownLatch的时候可以指定值,调用countDown()方法会使值减1,调用await()方法会使调用线程进入阻塞状态,直到CountDownLatch的值变为0再继续向下执行。

public class CountDownLatchTest {
    public static void main(String[] args) {
        // 创建CountDownLatch,可以指定值,这里设为2
        CountDownLatch countDownLatch = new CountDownLatch(2);

        // 创建任务1
        new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("任务一正在运行");
                    Thread.sleep(3000);
                    // 值减1
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();

        // 创建任务2
        new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("任务二正在运行");
                    Thread.sleep(1000);
                    // 值减1
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();

        try {
            System.out.println("等待任务执行完毕");
            // await()方法会使线程进入阻塞状态,countDownLatch的值变为0再继续执行
            countDownLatch.await();
            System.out.println("所有任务执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

任务一正在运行
等待任务执行完毕
任务二正在运行
所有任务执行完毕

从输出结果中可以看到,任务一执行完毕之后,会等待任务二执行完毕之后,才会执行await()方法后面的代码。

CyclicBarrier

CyclicBarrier用于一组线程,等待组内其他线程进入某个状态时,组内的所有线程再同时往下进行。

CyclicBarrier待所有的线程资源释放后,可以重新使用,这也是被称之为回环的原因。

public class CyclicBarrierTest {

    public static void main(String[] args) {
        // 创建CyclicBarrier,数量设为2
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
        // 创建任务1
        new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("任务一正在运行");
                    Thread.sleep(3000);
                    // 这里等待其他线程
                    // 所有线程都进入等待状态后,所有线程才可以进行往下进行
                    System.out.println("任务一进入等待");
                    cyclicBarrier.await();
                    System.out.println("任务一继续执行");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();

        // 创建任务2
        new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("任务二正在运行");
                    Thread.sleep(1000);
                    System.out.println("任务二进入等待");
                     // 这里等待其他线程
                    cyclicBarrier.await();
                    System.out.println("任务二继续执行");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();
    }
}

输出:

任务一正在运行
任务二正在运行
任务二进入等待
任务一进入等待
任务一继续执行
任务二继续执行

从输出结果中可以看到,任务二先执行到await()方法,此时任务一还在运行中,所以任务二等待任务一,当任务一也执行到await()方法时,再同时执行await()方法后面的代码。

Semaphore信号量

Semaphore可以用来控制访问受保护资源的线程个数。

在创建Semaphore的时候可以指定个数,表示有多少个线程可以获取到锁,acquire()方法用于获取一个锁,release()方法用于释放一个锁,当指定数量的锁获取完毕之后,如果有新的线程调用acquire方法获取锁只能等待其他线程释放。

public class SemaphoreTest {
    public static void main(String[] args) {
        // 创建Semaphore,值设置为3,表示可以有3个线程获取到锁
        Semaphore semaphore = new Semaphore(3);
        // 创建5个线程
        for(int i=1; i<=5; i++) {
            int j = i;
            new Thread(){
                @Override
                public void run() {
                    try {
                        // 获取锁
                        semaphore.acquire();
                        System.out.println("第"+j+"个线程获取到锁,开始执行");
                        Thread.sleep(new Random().nextInt(5000));
                        System.out.println("第"+j+"个线程执行完毕,并释放锁");
                        // 释放锁
                        semaphore.release();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                }
            }.start();
        }
    }
}

输出:

第1个线程获取到锁,开始执行
第2个线程获取到锁,开始执行
第3个线程获取到锁,开始执行
第1个线程执行完毕,并释放锁
第4个线程获取到锁,开始执行
第4个线程执行完毕,并释放锁
第5个线程获取到锁,开始执行
第5个线程执行完毕,并释放锁
第3个线程执行完毕,并释放锁
第2个线程执行完毕,并释放锁

由于设置了锁的数量,从输出结果中可以看到,前三个线程获取到锁之后,其他线程只能等待,当线程1执行完毕释放锁之后,线程4才能申请到锁,执行任务。

标签:面试题,Java,Thread,System,线程,println,多线程,public,out
From: https://www.cnblogs.com/shanml/p/17594571.html

相关文章

  • java-房屋出租系统实现
    房屋出租系统项目需求能够实现对房屋信息的添加、修改和删除(用数组实现),并能够打印房屋明细表项目界面主菜单新增房源查找房源删除房源修改房源房屋列表退出系统项目设计项目实现显示主菜单在HouseView.java中,编写一个方法mainMenu,显示菜单点击查看代码......
  • JavaScript中的 "return await promise" 与 "return promise"
    原文地址:'returnawaitpromise'vs'returnpromise'inJavaScript原文作者:DmitriPavlutin译文出自:翻译计划当从异步功能中返回时,您可以等待该承诺得到解决,或者您可以直接返回它:returnawaitpromisereturnpromise:jsasyncfunctionfunc1(){constpromise=asyncOperat......
  • 我需要 把 目标数据源中的表 获取到表的字段和字段的类型等信息,然后在目标数据源中创
    当涉及到将Oracle数据库字段类型映射为MySQL数据库字段类型时,考虑到不同数据库的差异和复杂性,以下是一个更全面的映射示例,涵盖了更多的Oracle字段类型及其可能的MySQL对应类型。importjava.util.HashMap;importjava.util.Map;publicclassOracleToMySQLTypeConverter{......
  • Java面试题 P42:框架篇:Spring-Spring框架中的单例bean是线程安全的吗?Spring框架中的bea
        ......
  • 金九银十你做好准备了吗?Android超全面求职攻略+面试题合集,助你拿offer
    有着“金九银十”之称的招聘旺季快要开启,求职高峰期也就此来临。而今年就业形势严峻,“金三银四”被大家笑称为“铜三铁四”,可能还有很多人目前还没有找到工作,那我们一定要抓住“金九银十”的机会,这求职攻略,希望能帮助到大家。1、简历准备简历时每个行业面试前都需要准备的,一份适合......
  • Java学习Day06
    第四章流程控制语句一、概述1.1、说明在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。也就是说,程序的流程对运行结果有直接的影响。所以,我们必须清楚每条语句的执行流程。而且,很多时候我们要通过控制语句的执行顺序来实现我们要完成的功能。简单来说......
  • Java学习Day07
    第六章方法一、方法1.1、概述在我们的日常生活中,方法可以理解为要做某件事情,而采取的解决办法。如:小明同学在路边准备坐车来学校学习。这就面临着一件事情(坐车到学校这件事情)需要解决,解决办法呢?可采用坐公交车或坐出租车的方式来学校,那么,这种解决某件事情的办法,我们就称为方......
  • java 十六进制字符串转换为有符号整数
    StringhexString="FEF7";//十六进制字符串intintValue=Integer.parseInt(hexString,16);//将十六进制字符串转换为整数shortsignedValue=(short)intValue;//转换为短整型(16位有符号整数)intintValue=(bytes[1]&0xFF)<<8|(bytes[0]&0xFF);//合并......
  • 3 Linux多线程开发
    3Linux多线程开发3.1线程概述3.1.1线程概述与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(......
  • Java内部类
    一、内部类的定义:定义在类中的类二、内部类的作用内部类可以访问外部类的所有数据,包括被【private修饰的私有数据】(1)为什么内部类可以访问外部类的所有成员变量内部类编译后会单独生成一份class文件,编译器会自动为内部类添加一个【外部类类型】的实例,内部类通过【外部类......