JAVA面试汇总-2.多线程

汤太咸

多线程

1. 开启线程的三种方式?

(1)继承Thread类,重写该类的run方法,调用线程对象的start()方法来启动
(2)实现Runnable接口,重写该类的run方法,调用线程对象的start()方法来启动
(3)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
通过new Thread传入FutureTask,执行Thread的start方法开始执行Callable线程。执行完成后通过FutureTask的get方法获得返回值,Callable具体代码如下:

public class TestCallableThread implements Callable<String> {
    public static void main(String[] args) throws Exception{
        TestCallableThread tct = new TestCallableThread();
        FutureTask<String> ft = new FutureTask<>(tct);
        new Thread(ft).start();
        while(true){
            if(ft.isDone()){
                System.out.println(ft.get());
                break;
            }else{
                System.out.println("waiting");
                Thread.sleep(1000);
            }
        }
    }
    @Override
    public String call() throws Exception {
        Thread.sleep(10000);
        return "Very Good";
    }
}

2. 说说进程,线程,协程之间的区别

(1)进程是系统进行资源分配和调度的独立单位
(2)线程是进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位
(3)协程是一种用户态的轻量级线程,协程的调度完全由用户控制
(4)进程与线程的区别
线程是指进程内的一个执行单元,也是进程内的可调度实体。其主要区别:
<1>地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,他们共享进程的地址空间,而进程有自己独立的地址空间。
<2>资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
<3>线程是CPU处理器调度的基本单位,但是进程不是。
<4>二者均可并发执行(共同点)
<5>每一个独立的线程有一个程序运行的入口,顺序执行序列和程序出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程控制。
(5)协程与线程的区别
<1>一个线程可以有多个协程,一个进程也可以单独拥有多个协程。
<2>进程线程都是同步机制,而协程则是异步
<3>协程能够保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用状态
(6)协程的优点
<1>协程执行的效率非常高。
<2>协程不需要多线程的锁机制

3. 线程之间是如何通信的?

(1)volatile:线程会将内存中的数据,拷贝到各自的本地内存中,当某个变量被 volatile 修饰并且发生改变时,volatile 变量底层会通过lock前缀的指令,将该变量写会主存,同时使其他线程的本地变量的数据无效,从而再次直接从主存读取数据。
(2)等待/通知机制:先来看看wait的原理图
微信截图_20211119164401.png

public class WaitDemo {
    private static Object lock = new Object();
    private static  boolean flag = true;
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    while (flag){
                        try {
                            System.out.println("wait start .......");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("wait end ....... ");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag){
                    synchronized (lock){
                        if (flag){
                            lock.notify();
                            System.out.println("notify .......");
                            flag = false;
                        }
                    }
                }
            }
        }).start();
    }
}

(3)join方式:这个比较特殊,下面代码的意思是当前的DemoThread的线程要等待上一个线程执行完毕后才能继续执行,这样就涉及了等待,通知的操作。

public class TestJoin {
    public static void main(String[] args) {
        Thread previorThread = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new DemoThread(previorThread));
            thread.start();
            previorThread = thread;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main terminal.");
    }
    static class DemoThread implements Runnable {
        private Thread thread;
        public DemoThread(Thread thread){
            this.thread = thread;
        }
        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " DemoThread terminal.");
        }
    }
}
//输出,很显然,join之后,后边的需要等待前面的线程完事
main main terminal.
Thread-0 DemoThread terminal.
Thread-1 DemoThread terminal.
Thread-2 DemoThread terminal.
Thread-3 DemoThread terminal.
Thread-4 DemoThread terminal.
Thread-5 DemoThread terminal.
Thread-6 DemoThread terminal.
Thread-7 DemoThread terminal.
Thread-8 DemoThread terminal.
Thread-9 DemoThread terminal.

(4)threadLocal方式:实际上线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。

4. 什么是Daemon线程?它有什么意义?

Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。
守护线程指在程序运行的时候在后台提供一种通用服务的线程,垃圾回收就是一种守护线程。
当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
守护线程,通过Thread.setDaemon(true)设置,需要在start之前设置。

public class TestJoin {
    public static void main(String[] args) {
        Thread previorThread = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new DemoThread(previorThread));
            thread.start();
            previorThread = thread;
        }
        for (int i = 0; i < 10; i++) {
            Thread daemonDemoThread = new Thread(new DaemonDemoThread(previorThread));
            daemonDemoThread.setDaemon(true);
            daemonDemoThread.start();
            previorThread = daemonDemoThread;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main terminal.");
    }
    static class DemoThread implements Runnable {
        private Thread thread;
        public DemoThread(Thread thread){
            this.thread = thread;
        }
        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " DemoThread terminal.");
        }
    }
    static class DaemonDemoThread implements Runnable {
        private Thread thread;
        public DaemonDemoThread(Thread thread){
            this.thread = thread;
        }
        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " DaemonDemoThread terminal.");
        }
    }
}
//输出,很显然,守护线程没完事就被干掉了
main main terminal.
Thread-0 DemoThread terminal.
Thread-1 DemoThread terminal.
Thread-2 DemoThread terminal.
Thread-3 DemoThread terminal.
Thread-4 DemoThread terminal.
Thread-5 DemoThread terminal.
Thread-6 DemoThread terminal.
Thread-7 DemoThread terminal.
Thread-8 DemoThread terminal.
Thread-9 DemoThread terminal.
Thread-10 DaemonDemoThread terminal.
Thread-11 DaemonDemoThread terminal.
Thread-12 DaemonDemoThread terminal.
Thread-13 DaemonDemoThread terminal.
Thread-14 DaemonDemoThread terminal.

5. 在java中守护线程和本地线程区别?

上面似乎已经讲了,守护线程在所有本地线程执行完成就会被干掉。另外也有人说是守护线程是jvm创建的,本地线程是程序创建的,但是感觉似乎不准确。其实就是守护线程实际上是辅助本地线程进程操作的,帮助回收内存,一些监听输入输出啊之类的。

6. 为什么要有线程,而不是仅仅用进程?

(1)进程只能在一个时间干一件事,如果想同时干两件事或多件事(其实开俩进程干?但是开销比较大),进程就无能为力了。
(2)进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
(3)每个进程需要单独的空间内存地址等很多用户资源,损耗大,但是多线程是虚拟内存共享,线程间切换容易,线程等待时,不占用cpu。

7. 什么是可重入锁(ReentrantLock)?

(1)ReentrantLock和synchronized都是独占锁
(2)synchronized加锁解锁的过程是隐式的,ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
(3)new ReentrantLock(true)可以实现公平锁(按照等待时间越长越优先获得锁权限),如果传入false表示非公平锁(性能更好)。
(4)当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,但是ReentrantLock可以通过lockInterruptibly()响应中断。tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。

8. 什么是线程组,为什么在Java中不推荐使用?

线程组(ThreadGroup)就是由线程组成的管理线程的类。
线程组ThreadGroup对象中的stop,resume,suspend会导致安全问题,主要是死锁问题,已经被官方废弃,多以价值已经大不如以前。
线程组ThreadGroup不是线程安全的,在使用过程中不能及时获取安全的信息。

9. 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

(1)悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制, 比如行锁,表锁等,读锁, 写锁等,都是在做操作之前先上锁。再比如Java 里面的同步原语synchronized 关键字的实现也是悲观锁。
(2)乐观锁:顾名思义,就是很乐观, 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition 机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS 实现的。
(3)读取频繁使用乐观锁,写入频繁使用悲观锁。

10. Java中用到的线程调度算法是什么?

(1)分时调度模型和抢占式调度模型。
(2)分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
(3)java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程(或者按照饥饿状态顺序判断),使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

11. 同步方法和同步块,哪个是更好的选择?

(1)同步方法:即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
(2)同步块:即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
(3)同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

12. run()和start()方法区别

(1)run()是实现Runable接口和继承Thread必须重写的,多线程实现的主方法,写的是具体的多线程执行的编码内容。
(2)start()方法是启动执行多线程的方法,表示真正开始运行多线程方法。

13. 如何控制某个方法允许并发访问线程的个数?

Semaphore创建时设定最多同时访问个数,acquire()申请新请求计数+1,达到设定数值后,内部会执行Thread的挂起操作,并把超过指定个数的线程放入到队列中等待唤醒。当release()后会唤醒队列里的前几个线程执行。

import java.util.concurrent.Semaphore;
public class SemaphoreTest {
    static Semaphore semaphore = new Semaphore(2, true);
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test();
                }
            }).start();
        }
    }
    public static void test() {
        try {
            //这块可以运行时可以看到实际时先打印出来,说明已经进入了方法了
            System.out.println("waiting"+Thread.currentThread().getName());
            //申请一个新的请求
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"进来了");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"走了");
        //释放一个请求
        semaphore.release();
    }
}

14. 在Java中wait和seelp方法的不同;

(1)wait()方法,是属于Object类,wait()出发后,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态
(2)sleep()方法,是属于Thread类的,sleep()方法的执行过程中,线程不会释放对象锁

15. Thread类中的yield方法有什么作用?

Thread.yield()可以暂停当前正在执行的线程对象,让其他有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

16. 什么是不可变对象,它对写并发应用有什么帮助?

对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变, 例如String、基本类型的包装类(Long,Double等)、BigInteger和BigDecimal等。主要是线程安全,在多线程情况下不可改变,天生线程安全。

17. 谈谈wait/notify关键字的理解

都是继承自Object的方法,wait()方法用来将当前线程置入休眠状态,notify()用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁。

18. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中,因为锁属于对象。
那么复杂的说呢?因为锁是在对象头中的markWorld的中标记的,wait和notify直接理解为get和set方法,实际上就是对锁的操作,因此理解来说,这个应该放到Object类中,而不是在Thread类中。

19. 什么导致线程阻塞?

(1)Thread.sleep方法休眠一段时间,线程放弃cpu,一段时间后在恢复运行
(2)线程执行wait()方法,进入阻塞状态,直到notify或者notifyAll方法唤醒
(3)等待相关资源:线程执行I/O操作或进行远程通信时,会因为等待相关的资源而进入阻塞状态。例如线程执行System.in.read()时,如果用户没有输入,则一直等待
(4)调用了yield方法后,同样会进入阻塞状态。
(5)suspend() 是让线程进入阻塞状态,没有resume()是不会恢复的。
(6)join()方法,被join的Thread要等待join的Thread执行完毕才能继续执行。

20. 讲一下java中的同步的方法

(1)当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,不被其他线程的调用,从而保证了该变量的唯一性和准确性。
(2)同步方法有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
(3)synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
(4)synchronized代码块同步关键代码
(5)使用特殊域变量(volatile)实现线程同步,但是不能完全避免同步问题,因为其不能保证原子操作,它只是在取得该变量值的时候是从内存中读取的而不是存缓存中读取。
(6)使用重入锁实现线程同步,通过lock和unlock来控制方法的执行锁。
(7)ThreadLocal的方式,由于每个线程都是独立的ThreadLocal,所以其实并不存在什么同步,以及冲突,每个线程都在自己的空间内执行。

public class Bank {
    //实际上这个是每个线程独立的ThreadLocal,并不共享,因此多个线程调用addMoney
    //或者subMoney的时候,都是在自己的独立线程内的,并不共享,不存在冲突
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    // 存钱
    public void addMoney(int money) {
        count.set(count.get() + money);
        System.out.println(System.currentTimeMillis() + "存进:" + money);

    }
    //取钱
    public void subMoney(int money) {
        if (count.get() - money < 0) {
            System.out.println("余额不足");
            return;
        }
        count.set(count.get() - money);
        System.out.println(+System.currentTimeMillis() + "取出:" + money);
    }
    //查询
    public void lookMoney() {
        System.out.println("账户余额:" + count.get());
    }
}

21. 谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解

(1)当synchronized作用于普通方法是,锁对象是this
(2)当synchronized作用于静态方法是,锁对象是当前类的Class对象
(3)当synchronized作用于代码块时,锁对象是synchronized(obj)中的这个obj
(4)类锁:synchronized修饰静态的方法或代码块
(5)方法锁:用synchronized修饰非静态方法
(6)重入锁:以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。synchronized和ReentrantLock 都是可重入锁。

//不可冲入锁,明显lock方法,锁了之后,别的都要等待,直到unlock才可以
public class Lock {
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}
//可重入锁,判断非该线程等待,如果仍然是该线程执行,则计数加一
public class Lock {
    boolean isLocked = false;
    Thread lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread();
        while (isLocked && lockedBy != thread) {
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock() {
        if (Thread.currentThread() == this.lockedBy) {
            lockedCount--;
            if (lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

22. static synchronized 方法的多线程访问和作用

(1)重点在static上面,也就是这个方法是静态的,所有的线程过来都是执行这个静态方法,因此控制所有的并发都要走这一个方法。
(2)网上有很多解释,基本上都是在,不带static的是只控制到这个类的同一个实例的访问,带static的情况下就包括了所有的实例(即使不是同一个实例的情况)都被控制了访问。

23. 同一个类里面两个synchronized方法,两个线程同时访问的问题

(1)两个方法都没有synchronized修饰,调用时都可进入,方法A和方法B都没有加synchronized关键字时,调用方法A的时候可进入方法B,方法A和方法B都可以并行执行
(2)一个方法有synchronized修饰,另一个方法没有,调用时都可进入,方法A加synchronized关键字而方法B没有加时,调用方法A的时候可以进入方法B,但是方法A只能一个一个执行,方法B可以并行执行
(3)两个方法都加了synchronized修饰,一个方法执行完才能执行另一个,方法A和方法B都加了synchronized关键字时,调用方法A之后,必须等A执行完成才能进入方法B,方法B执行完了才能执行方法A,因此实际上是顺序执行的
(4)两个方法都是静态方法且还加了synchronized修饰,一个方法执行完才能执行另一个,方法A和方法B都是static静态方法,且都加了synchronized关键字,则调用方法A之后,需要等A执行完成才能进入方法B,方法B执行完了才能执行方法A,因此实际上是顺序执行的,在如果是同一个实例执行的情况下和(3)是一致的,但是如果不是同一个实例下,(3)也会并行执行,但是(4)还是调用方法A之后,需要等A执行完成才能进入方法B,方法B执行完了才能执行方法A,实际上是顺序执行的。

//可以自己运行下,去掉两个synchronized,保留一个,都带着,再带着static,各种情况运行下
public class TwoSynchronized {
    public synchronized void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + " method1 started");
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + " method1 end");
        }catch (Exception e){

        }
    }
    public synchronized void method2() {
        try {
            System.out.println(Thread.currentThread().getName()+" method2 started");
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+" method2 end");
        }catch (Exception e){

        }
    }
    public static void main(String[] args) throws InterruptedException {
        TwoSynchronized two = new TwoSynchronized();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    two.method1();
                }
            }).start();
        }
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    two.method2();
                }
            }).start();
        }
        Thread.sleep(100000);
    }
}

24. 你如何确保main()方法所在的线程是Java程序最后结束的线程?

可以使用 Thread 类的 join()方法来确保所有程序创建的线程在 main()方法退出前结束。之前讲过了。

25. 谈谈volatile关键字的作用

(1)保证可见性,当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
(2)保证有序性,Java内存模型允许编译器和处理器对指令重排序以提高运行性能,只会对不存在数据依赖性的指令重排序。因此有在多线程情况下,会导致某些由于指令重排的问题,被声明为volatile可以禁止指令重排序。
(3)内存屏障,在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。在每个volatile读操作的前面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。

LoadLoadL,Load1;LoadLoad;Load2:保证load1的读取操作在在load2及后续读取操作之前执行
StoreStore,Store1;StoreStore;Store2:在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore,Load1;LoadStore;Store2:在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad,Store1;StoreLoad;Load2:保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

26. 谈谈ThreadLocal关键字的作用

(1)每个线程都会存在自己独立的ThreadLocal
(2)实际上是一个map,可以把一些在线程开始时候放入一些内容到ThreadLocal中,这样在线程的生命周期内,这些内容都可以直接从ThreadLocal中拿到(如果启动了多线程除外)。
(3)很多时候AOP的过程中需要从ThreadLocal中取出来一些东西

27. Java中Semaphore是什么?

Semaphore信号量,可以控制同时执行的线程数,创建时设定最多同时访问个数,acquire()申请新请求计数+1,达到设定数值后,内部会执行Thread的挂起操作,并把超过指定个数的线程放入到队列中等待唤醒。当release()后会唤醒队列里的前几个线程执行。

import java.util.concurrent.Semaphore;
public class SemaphoreTest {
    static Semaphore semaphore = new Semaphore(2, true);
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test();
                }
            }).start();
        }
    }
    public static void test() {
        try {
            //这块可以运行时可以看到实际时先打印出来,说明已经进入了方法了
            System.out.println("waiting"+Thread.currentThread().getName());
            //申请一个新的请求
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"进来了");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"走了");
        //释放一个请求
        semaphore.release();
    }
}

28. 什么是Callable和Future?

(1)Callable和Runnable是一类,但是Callable在线程执行完成,可以返回一些内容。
(2)Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable用于产生结果,Future用于获取结果。
(3)把Callable和Future结合起来有个FutureTask,可以执行Callable并且获取结果。下面来个样例:

public class TestCallableThread implements Callable<String> {
    public static void main(String[] args) throws Exception{
        TestCallableThread tct = new TestCallableThread();
        FutureTask<String> ft = new FutureTask<>(tct);
        new Thread(ft).start();
        while(true){
            if(ft.isDone()){
                System.out.println(ft.get());
                break;
            }else{
                System.out.println("waiting");
                Thread.sleep(1000);
            }
        }
    }
    @Override
    public String call() throws Exception {
        Thread.sleep(10000);
        return "Very Good";
    }
}

29. ThreadLocal、synchronized 和volatile 关键字的区别

(1)ThreadLocal是每个线程独立的一个Map,可以存储一些内容,和另外两个没啥关系
(2)synchronized用来修饰方法或者代码块的,是一种锁机制,增加了synchronized方法可以防止多线程同时执行该方法,如果是两个实例(同一个实例可以避免)的情况下的同一个synchronized方法被执行,实际上是还能够被同时执行的,如果需要控制就再增加上static就可以控制了
(3)volatile用来修饰变量的,在多线程中同步变量。表示如果要使用该内容,需要直接从内存中取值,而不能从缓存中取,避免了某些情况下的线程冲突

30. synchronized与Lock的区别

(1)synchronized是一个关键字,执行完毕释放,无法获得锁状态
(2)synchronized关键字是通过字节码指令来实现的
(3)synchronized关键字编译后会在同步块前后形成monitorenter和monitorexit两个字节码指令
(4)执行monitorenter指令时需要先获得对象的锁(每个对象有一个监视器锁monitor),如果这个对象没被锁或者当前线程已经获得此锁(也就是重入锁),那么锁的计数器+1。如果获取失败,那么当前线程阻塞,直到锁被对另一个线程释放
(5)执行monitorexit指令时,计数器减一,当为0的时候锁释放
(6)Lock是一个类,需要手动锁、解锁,否则就会死锁,可以通过trylock来知道有没有获取锁
(7)Lock是通过同步器AQS(AbstractQueuedSynchronized类)来实现的,AQS根本上是通过一个双向队列来实现的
(8)Lock的线程构造成一个节点,一个线程先尝试获得锁,如果获取锁失败,就将该线程加到队列尾部
(9)Lock:非公平锁的lock方法,调用的sync(NonfairSync和fairSync的父类)的lock方法

31. ReentrantLock 、synchronized和volatile比较

(1)ReentrantLock是一种锁,ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。new ReentrantLock(true)可以实现公平锁(按照等待时间越长越优先获得锁权限),如果传入false表示非公平锁(性能更好)。ReentrantLock可以通过lockInterruptibly()响应中断。tryLock()可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
(2)synchronized用来修饰方法或者代码块的,是一种锁机制,增加了synchronized方法可以防止多线程同时执行该方法,如果是两个实例(同一个实例可以避免)的情况下的同一个synchronized方法被执行,实际上是还能够被同时执行的,如果需要控制就再增加上static就可以控制了
(3)volatile用来修饰变量的,在多线程中同步变量。表示如果要使用该内容,需要直接从内存中取值,而不能从缓存中取,避免了某些情况下的线程冲突

32. 在Java中CycliBarriar和CountdownLatch有什么区别?

(1)CountdownLatch: 一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行。是并发包中提供的一个可用于控制多个线程同时开始某个动作的类,其采用的方法为减少计数的方式,当计数减至零时位于latch.Await()后的代码才会被执行,CountDownLatch是减计数方式,计数==0时释放所有等待的线程;CountDownLatch当计数到0时,计数无法被重置。
(2)CycliBarriar字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。 即:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。CyclicBarrier是当await的数量到达了设置的数量的时候,才会继续往下面执行,CyclicBarrier计数达到指定值时,计数置为0重新开始。
(3)对于CountDownLatch来说,重点是那个“一个线程”,是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
(4)CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch测试代码

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class TestCountDownLatch {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        Executor executor = Executors.newFixedThreadPool(5);
        //测试阻塞其他线程
        new Thread(new MyRunnable(countDownLatch)).start();
        //为了测试效果进行线程休眠
        Thread.sleep(1000);
        for (int i = 1; i <= 5; i++) {
            countDownLatch.countDown();
            System.out.println("第" + i + "调用countDown方法结束");
            //为了测试效果进行线程休眠
            Thread.sleep(1000);
        }
        /*
         *测试阻塞主线程
         */
        for (int i = 1; i <= 5; i++) {
            new Thread(new MyRunnable1(countDownLatch, i + "")).start();
            Thread.sleep(1000);
        }
        try {
            System.out.println("主线程阻塞");
            countDownLatch.await();
            System.out.println("主线程继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static class MyRunnable implements Runnable {
        CountDownLatch countDownLatch;
        public MyRunnable(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            try {
                System.out.println("进入线程,即将进入阻塞状态");
                //调用await进行线程阻塞
                countDownLatch.await();
                System.out.println("线程进行执行...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class MyRunnable1 implements Runnable {
        private CountDownLatch countDownLatch;
        private String mark;
        public MyRunnable1(CountDownLatch countDownLatch, String mark) {
            super();
            this.countDownLatch = countDownLatch;
            this.mark = mark;
        }
        @Override
        public void run() {
            System.out.println(mark + "号线程开始");
            try {
                //使线程休眠,看到更好的测试效果
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(mark + "号线程结束");
                //调用CountDownLatch的countDown方法进行次数减1
                countDownLatch.countDown();
            }
        }
        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }
        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
        public String getMark() {
            return mark;
        }
        public void setMark(String mark) {
            this.mark = mark;
        }
    }
}
//输出
进入线程,即将进入阻塞状态
第1调用countDown方法结束
第2调用countDown方法结束
第3调用countDown方法结束
第4调用countDown方法结束
第5调用countDown方法结束
线程进行执行...
1号线程开始
2号线程开始
3号线程开始
4号线程开始
5号线程开始
主线程阻塞
主线程继续执行

CyclicBarrier测试代码

import java.util.concurrent.*;

public class TestCyclicBarrier {
    static final Integer NUM = 5;
    public static void main(String[] args) throws InterruptedException {
        //实例CyclicBarrier对象
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM);//实例化一个固定大小线程池
        ExecutorService executor = Executors.newFixedThreadPool(NUM);
        for (int i = 1; i <= NUM; i++) {
            //执行线程
            executor.execute(new MyRunnale2(cyclicBarrier, i + "号"));
            //为了更好的效果,休眠一秒
            Thread.sleep(1000);
        }
        System.out.println("指令通知完成");
        //执行完毕需要关闭,否则主线程还在这卡着不关闭
        executor.shutdown();
    }
    static class MyRunnale2 implements Runnable {
        private CyclicBarrier cyclicBarrier;
        private String mark;
        public MyRunnale2(CyclicBarrier cyclicBarrier, String mark) {
            super();
            this.cyclicBarrier = cyclicBarrier;
            this.mark = mark;
        }
        @Override
        public void run() {
            System.out.println(mark + "进入线程,线程阻塞中...");
            try {
                //barrier的await方法,在所有参与者都已经在此barrier上调用await方法之前,将一直等待。
                cyclicBarrier.await();
                System.out.println(mark + "进入开始执行...");
                Thread.sleep(2000);//为了看到更好的效果,线程阻塞两秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(mark + "线程阻塞结束,继续执行...");
        }
        public CyclicBarrier getCyclicBarrier() {
            return cyclicBarrier;
        }
        public void setCyclicBarrier(CyclicBarrier cyclicBarrier){
            this.cyclicBarrier = cyclicBarrier;
        }
    }
}
//输出
1号进入线程,线程阻塞中...
2号进入线程,线程阻塞中...
3号进入线程,线程阻塞中...
4号进入线程,线程阻塞中...
5号进入线程,线程阻塞中...
5号进入开始执行...
1号进入开始执行...
2号进入开始执行...
3号进入开始执行...
4号进入开始执行...
指令通知完成
3号线程阻塞结束,继续执行...
2号线程阻塞结束,继续执行...
4号线程阻塞结束,继续执行...
5号线程阻塞结束,继续执行...
1号线程阻塞结束,继续执行...

33. CopyOnWriteArrayList可以用于什么应用场景?

(1)ArrayList在同时遍历和修改这个列表时,会抛出 ConcurrentModificationException。
(2)CopyOnWriteArrayList由于是copy出来的,因此不会出现这个错误。
(3)写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致gc。
(4)不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
(5)读写分离,读和写分开;最终一致性;使用另外开辟空间的思路,来解决并发冲突

34. Java中invokeAndWait 和 invokeLater有什么区别?

这个似乎没啥用,为啥问这个?
invokeAndWait()方法请求事件派发线程对组件进行相应更新,需要等待执行完成。
invokeLater()方法是异步调用更新组件的。

35. 多线程中的忙循环是什么?

忙循环就是用户循环让一个线程等待,不像传统方法使用wait()、sleep()或yield(),它们都放弃CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
按照自己的理解写了个忙循环

public class TestBusyWait {
    public static int wait = 3;
    public static void main(String[] args) throws Exception{
        new Thread(new MyRunnable()).start();
        for (int i = 0; i < 3; i++) {
            new Thread(new MyRunnable1()).start();
        }
        Thread.sleep(2000);
    }
    static class MyRunnable implements Runnable {
        public MyRunnable() {
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"空循环开始");
            while (wait != 0) {
            }
            System.out.println(Thread.currentThread().getName()+"空循环完成");
        }
    }
    static class MyRunnable1 implements Runnable {
        public MyRunnable1() {
        }
        @Override
        public void run() {
            wait--;
            try {
                System.out.println(Thread.currentThread().getName()+"开始了");
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+"完成了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

36. 怎么检测一个线程是否拥有锁?

在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。

public class TestHoldsLock {
    static Object o = new Object();
    public static synchronized void method1() {
        System.out.println("before method1 Thread.holdsLock(o)=="+Thread.holdsLock(o));
        synchronized (o) {
            System.out.println("runing method1 Thread.holdsLock(o)=="+Thread.holdsLock(o));
        }
        System.out.println("after method1 Thread.holdsLock(o)=="+Thread.holdsLock(o));
    }
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main Thread.holdsLock(o)=="+Thread.holdsLock(o));
        TestHoldsLock test = new TestHoldsLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();
    }
}
//输出
main Thread.holdsLock(o)==false
before method1 Thread.holdsLock(o)==false
runing method1 Thread.holdsLock(o)==true
after method1 Thread.holdsLock(o)==false

37. 死锁的四个必要条件?

(1)互斥条件:一个资源每次只能被一个进程使用;
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;

38. 什么是线程池,如何使用?

(1)什么是线程池:java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。实际上是对多个线程统一管理,避免了多次创建销毁操作,可以控制同时执行的最大线程数,防止执行过多式服务卡死。
(2)Executors工厂类可以创建多种不同的线程池
(3)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(4)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
(5)newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(6)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorTest {
    public static void main(String[] args) {
        //创建一个可重用固定线程数的线程池
        ExecutorService pool= Executors.newSingleThreadExecutor();
        //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口;
        Thread t1=new MyThread();
        Thread t2=new MyThread();
        Thread t3=new MyThread();
        Thread t4=new MyThread();
        Thread t5=new MyThread();
        //将线程放到池中执行;
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        //关闭线程池
        pool.shutdown();

    }
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" started");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end");
        }
    }
}
//输出
pool-1-thread-1 started
pool-1-thread-1 end
pool-1-thread-1 started
pool-1-thread-1 end
pool-1-thread-1 started
pool-1-thread-1 end
pool-1-thread-1 started
pool-1-thread-1 end
pool-1-thread-1 started
pool-1-thread-1 end
//如果修改为
ExecutorService pool= Executors.newFixedThreadPool(2);
//可以看到下面输出同时只有2个在执行
pool-1-thread-1 started
pool-1-thread-2 started
pool-1-thread-1 end
pool-1-thread-2 end
pool-1-thread-1 started
pool-1-thread-2 started
pool-1-thread-2 end
pool-1-thread-1 end
pool-1-thread-2 started
pool-1-thread-2 end
//ScheduledThreadPoolExecutor测试类,定时执行任务
public class ScheduledThreadExecutorTest{
    public static void main(String[] args) {
        //下面这样写也可以,也可以自己new
        //ScheduledExecutorService exec = Executors.newScheduledThreadPool(2);
        ScheduledThreadPoolExecutor exec =new ScheduledThreadPoolExecutor(2);
        exec.scheduleAtFixedRate(new Runnable(){//每隔一段时间执行
            @Override
            public void run() {
                System.out.println("===================");

            }}, 1000, 5000, TimeUnit.MILLISECONDS);
        exec.scheduleAtFixedRate(new Runnable(){//每隔一段时间打印系统时间,证明两者是互不影响的
            @Override
            public void run() {
                System.out.println(System.nanoTime());
            }}, 1000, 2000, TimeUnit.MILLISECONDS);
    }
}
//输出,一直循环下去
===================
18116537502800
18118526972600
18120536395200
===================
18122534792700
18124535284500
===================

39. Java中interrupted 和 isInterrupted方法的区别?

(1)interrupt方法用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。线程中断仅仅是置线程的中断状态位,不会停止线程。视线程的状态为并做处理。一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
(2)interrupted查询当前线程(一定注意是当前线程)的中断状态,并且清除原状态。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
(3)isInterrupted仅仅是查询当前线程的中断状态,并不清除原状态。

public class InterruptThreadTest extends Thread {
    @Override
    public  void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("执行中");
            if(this.isInterrupted()){
                System.out.println("first call outside thread.interrupted(): " + this.interrupted());
                System.out.println("first call outside thread.isInterrupted(): " + this.isInterrupted());
                System.out.println("second call outside thread.interrupted(): " + this.interrupted());
                System.out.println("second call outside thread.isInterrupted(): " + this.isInterrupted());
            }
        }

    }
    public static void main(String[] args) {
        InterruptThreadTest thread = new InterruptThreadTest();
        thread.start();
        thread.interrupt();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("status: " + thread.getState());
        System.out.println("thread is alive : " + thread.isAlive() );
    }
}
//输出
执行中
first call outside thread.interrupted(): true
first call outside thread.isInterrupted(): false
second call outside thread.interrupted(): false
second call outside thread.isInterrupted(): false
执行中
执行中
执行中
执行中
执行中
。。。重复
status: TERMINATED
thread is alive : false

40. Java线程池中submit() 和 execute()方法有什么区别?

(1)两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在Executor 接口中。
(2) submit()方法可以返回持有计算结果的 Future 对象,它定义在ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。
(3)excute方法会抛出异常。sumbit方法不会抛出异常,调用Future.get()如果有异常可以抛出。
(4)excute入参Runnable,submit入参可以为Callable(可以返回结果的),也可以为Runnable(不能返回结果)。

public class SingleThreadExecutorTest {
    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<?> runnableFuture = executorService.submit(() -> System.out.println("run"));// submit一个Runnable任务
        Future<?> callableFuture = executorService.submit(() -> "call");// submit一个Callable任务

        executorService.execute(() -> System.out.println("test"));

        System.out.println(runnableFuture.get());// Output null
        System.out.println(callableFuture.get());// Output call
    }
}
//输出
run
null
call
test

41. 对象锁和类锁是否会互相影响?

42. lock原理

43. ReentrantLock的内部实现

44. 谈谈NIO的理解

45. 用Java实现阻塞队列

46. BlockingQueue介绍:

47. 多线程有什么要注意的问题?

48. 如何保证多线程读写文件的安全?

49. 多线程断点续传原理

50. 断点续传的实现

51. 实现生产者消费者模式

52. Java中的ReadWriteLock是什么?

53. 用Java写一个会导致死锁的程序,你将怎么解决?

54. SimpleDateFormat是线程安全的吗?

55. Java中的同步集合与并发集合有什么区别?

56. Java中ConcurrentHashMap的并发度是什么?

57. 什么是Java Timer类?如何创建一个有特定时间间隔的任务?

阅读 184

程序员一枚,也爱读书,也爱理财,还喜欢把学到的读到的总结出来写点东西,写的不难,希望大家喜欢。

3 声望
0 粉丝
0 条评论

程序员一枚,也爱读书,也爱理财,还喜欢把学到的读到的总结出来写点东西,写的不难,希望大家喜欢。

3 声望
0 粉丝
文章目录
宣传栏