Java多线程面经整理

  1. 什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对 运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。

  1. 线程与进程的区别

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。

线程:是进程的一个执行单元,是进程内调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

一个程序至少一个进程,一个进程至少一个线程。

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

参考博客https://blog.csdn.net/feiBlog/article/details/85397287

  1. 如何在Java中实现线程

在语言层面有四种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承 java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。第三种 实现Callable<>接口并重写call方法;使用线程池(有返回值)
参考博客https://www.cnblogs.com/duanjiapingjy/p/9434244.html
实现Runnable接口

class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我在看佐匹克---"+i);

        }

    }

    public static void main(String[] args) {
        //创建runnbale接口的实现类对象
        MyThread testThread3 = new MyThread();
        new Thread(testThread3).start();
    }
}

继承Thread类

@Test
public void solution() {
    Thread t = new Thread() {
        @Override
        public void run() {
            System.out.println("123");
        }
    };
    t.start();
}

实现Callable接口

public class test {

    class MyThread implements Callable<String> {

        @Override
        public String call() throws Exception {
            return "Hello World!";
        }
    }

    @Test
    public void solution() throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Future<String> submit = threadPool.submit(new MyThread());
        System.out.println(submit.get());
    }
}

使用线程池

@Test
public void solution(){
    Executor executor = Executors.newSingleThreadExecutor();
    executor.execute(new Runnable(){
        public void run(){
            //执行的任务
            System.out.println("123");
        }
    });
}
  1. 使用Thread还是Runnable

java不支持类的多继承,但允许实现多个接口。

  1. Thread类中的start()和run()方法有什么区别

通过调用线程类的start()方法来启动一个线程,使线程处于就绪状态,即可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止;如果直接调用线程类的run()方法,会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程。即start()方法能够异步的调用run()方法,但是直接调用run()方法却是同步的,无法达到多线程的目的;因此,只用通过调用线程类的start()方法才能达到多线程的目的

  1. java中Runnable和Callable有什么不同

Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在 JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

  1. Java中的volatile变量是什么

volatile是一个特殊的修饰符,只有成员变量才能使用它。保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  1. 什么是线程安全?vector可以保证线程安全吗

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。Vector 和 ArrayList 实现了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 关键修饰。但对于复合操作,Vector 仍然需要进行同步处理

if (!vector.contains(element)) 
    vector.add(element); 
    ...
}

虽然条件判断 if (!vector.contains(element))与方法调用 vector.add(element); 都是原子性的操作 (atomic),但在 if 条件判断为真后,那个用来访问vector.contains 方法的锁已经释放,在即将的 vector.add 方法调用 之间有间隙,在多线程环境中,完全有可能被其他线程获得 vector的 lock 并改变其状态

参考博客https://blog.csdn.net/xdonx/article/details/9465489

  1. Java中notify和notifyAll有什么区别

因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行

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

这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在 Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通 过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

  1. 什么是ThreadLocal变量

ThreadLocal使用场合主要解决多线程中数据由并发产生不一致问题。ThreadLocal为每个线程中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

  • 每个线程Thread内部都维护一个 ThreadLocalMap(ThreadLocal的静态内部类ThreadLocalMap)
  • ThreadLocalMap创建了一个长度为16的Entry数组table。即ThreadLocalMap为每个线程维护了一个table
  • ThreadLocal调用set插入数据时,会通过Thread.currentThread() 获取当前线程,通过当前线程获取线程的ThreadLocalMap对象,然后调用ThreadLocalMap的set(key,value)方法,其中key为ThreadLocal对象,value为需要保存的值
  • ThreadLocalMap会将key和value创建成一个Entry对象(Entry中的key是一个指向ThreadLocal对象的弱引用),并将这个Entry对象赋值给table[i](i为通过key的hashCode与length位运算确定出一个索引值
static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                // 这里调用父类WeakReference方法key进入
                super(k);
                value = v;
            }
        }

总结:

  • 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。

  • 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
    参考博客https://www.jianshu.com/p/3c5d7f09dfbd

    关于Entry中的key使用弱引用:
    弱引用:当Java系统回收内存时,弱引用就会被回收
    在这里插入图片描述
    通过图片可以看出ThreadLocal对象被一个强引用tl和一个弱引用key同时引用。如果key使用强引用,那么当tl = null要释放ThreadLocal时,key依然强应用TreadLocal会导致内存泄漏。所以key需要使用弱引用,当tl= null时,下一次内存回收,ThreadLocal就会被回收。

  1. 什么是FutureTask?
    FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。
    参考博客https://blog.csdn.net/linchunquan/article/details/22382487

  2. Java中interrupted 和 isInterrupted方法的区别?
    interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来 检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。
    许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false

  3. 为什么wait和notify方法要在同步块中调用?
    (1)尝试在未获取对象锁时调用这三个方法,那么你将得一个"java.lang.IllegalMonitorStateException:current thread not owner"
    (2)避免wait和notify之间产生竞态条件
    只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。当一个线程正在某一个对象的同步方法中运行时调用了这个对象的wait()方法,那么这个线程将释放该对象的独占锁并被放入这个对象的等待队列。wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法
    某线程调用某对象的notify()或notifyAll()方法时,该对象的等待线程中的线程,将被转移到该对象的入口队列中。这些线程将竞争该对象的锁,最终获得锁的线程继续执行。如果没有线程在该对象的等待队列中等待获得锁,那么notify()和notifyAll()将不起任何作用。在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
    假设wait(),notify(),notifyAll()方法不需要加锁就能够被调用。此时消费者线程调用wait()正在进入状态变量的等待队列(译者注:可能还未进入)。在同一时刻,生产者线程调用notify()方法打算向消费者线程通知状态改变。那么此时消费者线程将错过这个通知并一直阻塞。因此,对象的wait(),notify(),notifyAll()方法必须在该对象的同步方法或同步代码块中被互斥地调用
    参考博客https://blog.csdn.net/haluoluo211/article/details/49558155
    https://blog.csdn.net/lsgqjh/article/details/61915074
    https://www.cnblogs.com/wxw7blog/p/7396906.html

  4. 为什么wait()需要放到循环中?
    处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来 时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方 法效果更好的原因。通过案例来说明:
    正确的使用

synchronized (monitor) {
    //  判断条件谓词是否得到满足
    while(!locked) {
        //  等待唤醒
        monitor.wait();
    }
    //  处理其他的业务逻辑
}

需要说明,当 执行monitor.wait();语句时,当前线程被强制释放对象锁,开始等待被唤醒后再次从该处往下执行。如果使用if判断如下:

synchronized (monitor) {
    //  判断条件谓词是否得到满足
    if(!locked) {
        //  等待唤醒
        monitor.wait();
    }
    //  处理其他的业务逻辑
}

线程从wait中醒来,就会直接执行处理其他的业务逻辑。而使用while循环,则会再次判断 !locked 。如果非要使用if判断,可以修改代码如下:

synchronized (monitor) {
    //  判断条件谓词是否得到满足
    if(!locked) {
        //  等待唤醒
        monitor.wait();
        if(locked) {
            //  处理其他的业务逻辑
        } else {
            //  跳转到monitor.wait(); 
        }
    }
}

参考博客https://blog.csdn.net/yiifaa/article/details/76341707

  1. java中的同步集合与并发集合又什么区别?
    同步集合可以简单地理解为通过synchronized来实现同步的集合。如果有多个线程调用同步集合的方法,它们将会串行执行:
    (1) Vector是线程安全的,ArrayList不是; ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;Stack是继承于Vector,基于动态数组实现的一个线程安全的栈; ArrayList、vector、Stack的共性特点:随机访问速度快,插入和移除性能较差(这是数组的特点,三者的底层均为数组实现
    (2) HashMap是非synchronized的,而Hashtable是synchronized的;HashMap可以存在null的键值(key)和值(value),但是Hashtable是不可以的
    @Test
    public void fun() {
        Map<String,String> map = new HashMap<>();
        map.put("key1",null);
        map.put("key2",null);
        map.put(null,"null");
        map.put(null,"null1"); // null会被覆盖
        for (Map.Entry<String,String> entry: map.entrySet()) {
            System.out.println(entry.getKey() + entry.getValue());
        }
        Map<String,String> table = new Hashtable<>();
        table.put("key1","null");
//        table.put("key2",null); // 报错
//        table.put(null,null); // 报错
        for (Map.Entry<String,String> entry: table.entrySet()) {
            System.out.println(entry.getKey() + entry.getValue());
        }
    }

(3) Collections:Collections是为集合提供各种方便操作的工具类,通过它,可以实现集合排序、查找、替换、同步控制、设置不可变集合

Collections.synchronizedCollection(Collection<T>t)
Collections.synchronizedList(List<T>list)
Collections.synchronizedMap(Map<K, V>map)
Collections.synchronizedSet(Set<T> t)

并发集合 是jdk5.0重要的特性,增加了并发包java.util.concurrent.*。Java内存模型、volatile变量及AbstractQueuedSynchronizer(简称AQS同步器),是并发包众多实现的基础
(1) 常见的并发集合:
ConcurrentHashMap:线程安全的HashMap的实现
CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue
ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。
CopyOnWrite集合即写时复制的集合。
通俗的理解是当我们往一个集合添加元素的时候,不直接往当前集合添加,而是先将当前集合进行Copy,复制出一个新的集合,然后新的集合里添加元素,添加完元素之后,再将原集合的引用指向新的集合。这样做的好处是我们可以对CopyOnWrite集合进行并发的读,而不需要加锁,因为当前集合不会添加任何元素。所以CopyOnWrite集合也是一种读写分离的思想,读和写不同的集合
参考博客https://blog.csdn.net/yuruixin_china/article/details/82082195

  1. 如何避免死锁?
    多线程中的死锁
    死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:

互斥条件:一个资源每次只能被一个进程使用。

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

  1. 怎么检测一个线程是否拥有锁?
    java.lang.Thread中有一个方法叫holdsLock(),如果当且仅当当前线程拥有某个具体对象的锁返回true

  2. Java中活锁和死锁有什么区别
    活锁:一个线程通常会有会响应其他线程的活动。如果其他线程也会响应另一个线程的活动,那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:甲向他自己的左边靠想让乙过去,而乙向他的右边靠想让甲过去。可见他们阻塞了对方。甲向他的右边靠,而乙向他的左边靠,他们还是阻塞了对方。
    死锁:两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候,死锁会让你的程序挂起无法完成任务。

  3. Java中synchronized 和 ReentrantLock 有什么不同

    • 相同点
      • 都是加锁方式同步
      • 都是可重入锁
      • 阻塞式同步
    • 不同点
      • s是原生语法层面的互斥, 需要jvm实现; r是jdk提供的api层面的互斥锁
      • s代码块执行完之后系统会自动释放锁, r必须用户手动释放锁
      • s范围是整个方法或代码块, r是方法调用, 可跨方法
      • s非公平锁, r两者皆可(默认公平锁)

ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
(1)等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
(2)公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
(3)锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。

/**
 * ReentrantLock绑定多个Condition(代码实现)
 * 多线程按顺序调用 , A->B->C
 *
 * A打印5次,B打印10次,C打印15次 ABC都循环10次
 */
public class ShareResource {
    private int number = 1; // 为1是A打印,为2时B打印,为3时C打印
    //A :1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition c1 =lock.newCondition();
    private Condition c2 =lock.newCondition();
    private Condition c3 =lock.newCondition();
    public void prints5(){
        lock.lock();
        try
        {
            // 1 判断
            while(number!=1)
            {
                c1.await();
            }
            // 2 业务逻辑
            for(int i = 0; i < 5; i++)
            {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            // 3 通知
            number =2;
            c2.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    public void prints10(){
        lock.lock();
        try
        {
            while(number!=2)
            {
                c2.await();
            }
            for(int i = 0; i < 10; i++)
            {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number =3;
            c3.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    public void prints15(){
        lock.lock();
        try
        {
            while(number!=3)
            {
                c3.await();
            }
            for(int i = 0; i < 15; i++)
            {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number =1;
            c1.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public static void main(String args[]){
        final ShareResource test = new ShareResource();
        new Thread(new Runnable(){
            public void run(){
                for(int i =0 ; i < 10; i++)
                {
                    test.prints5();
                }
            }
        },"A").start();
        new Thread(new Runnable(){
            public void run(){
                for(int i =0 ; i < 10; i++)
                {
                    test.prints10();
                }
            }
        },"B").start();
        new Thread(new Runnable(){
            public void run(){
                for(int i =0 ; i < 10; i++)
                {
                    test.prints15();
                }
            }
        },"C").start();
    }
}
  1. 有三个线程T1,T2,T3,怎么确保它们按顺序执行?
    在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

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

  3. ConcurrentHashMap 的并发度
    ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最多同时可以有 16 条线程操作 ConcurrentHashMap,这也是ConcurrentHashMap 对 Hashtable 的最大优势

  4. Java中Semaphore是什么
    Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量

private Semaphore semaphore = new Semaphore(1);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码
/**
* 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
* 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
* */
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
semaphore.release();

其中acquire( int permits ) ,表示初始化permits个通路;上面semaphore.acquire() + semaphore.release() 在运行的时候,其实和 semaphore.acquire(1) + semaphore.release(1) 效果是一样的
参考博客https://www.cnblogs.com/klbc/p/9500947.html

  1. 如果你提交任务时,线程池队列已满。会时发会生什么

    • 如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
    • 如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来ArrayBlockingQueue 继续满,抛出一个RejectedExecutionException异常
  2. java线程钟submit()和execute()的区别

    1. 可以接收的任务类型:excute入参Runnable;submit入参可以为Callable,也可以为Runnable
    public interface Executor {
        void execute(Runnable command);
    }
    
    public interface ExecutorService extends Executor {
      ...
      <T> Future<T> submit(Callable<T> task);
     
      <T> Future<T> submit(Runnable task, T result);
     
      Future<?> submit(Runnable task);
      ...
    }
    
    1. 返回值:submit()方法,可以提供Future < T > 类型的返回值;executor()方法,无返回值
    2. 抛出异常:excute方法会抛出异常;sumbit方法不会抛出异常。除非你调用Future.get()
      参考博客https://www.cnblogs.com/androidsuperman/p/9784821.html
  3. 什么是阻塞方法
    阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是 指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回

  4. Swing是线程安全的吗,为什么?
    Swing不是线程安全的,Swing组件不支持多线程访问,程序要操作或更改界面的内容,必须向单一线程提出请求,我们把这个单一的线程称为事件派发线程(UI线程)

  5. Java中的ReadWriteLock是什么
    读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程 持有。写锁是独占的.ReentrantReadWriteLock 是 ReadWriteLock 的一种实现,读写锁在读多写少的情况下性能较高

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

  7. volatile 变量和 atomic 变量有什么不同
    volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性 的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作

  8. 如果同步块内的线程抛出异常会发生什么
    无论你的同步块是正常还是异常退出的,里面的线程都会释放锁。相比于lock对象方法调用,必须在finally里释放锁

  9. 写出3条你遵循的多线程最佳实践

    1. 给你的线程起个有意义的名字。
      这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。

    2. 避免锁定和缩小同步的范围
      锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。

    3. 多用同步类少用wait 和 notify
      首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断 优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。

    4. 多用并发集合少用同步集合
      这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap

  10. Java多线程中调用wait() 和 sleep()方法有什么不同
    Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。
    (1) sleep方法使当前线程暂停执行, 让出CPU给其他线程, 但是他的监控状态仍然保持(不会释放对象锁), 制定的时间到了之后会自动恢复运行状态, 可以在任何地方使用
    (2) wait方法会让线程放弃对象锁, 进入此对象的等待锁定池, 只能在同步方法和同步代码快中使用

  11. 线程池面试题

  12. 线程是如何通信

  13. 进程是如何通信

    1. 无名管道:(1)半双工(数据只能在一个方向上流动),具有固定的读端和写段(2)只能用于具有亲缘关系的进程之间的通信(父进程或兄弟进程之间)(3) 一种特殊的文件,对于其读写可以使用普通的read\write等函数。但是他不是普通的文件,并不属于其他任何文件系统,并且只存在于内存当中
    2. FIFO也称为命名管道:(1)可以在无关进程之间交换数据;(2)以特殊设备文件形式存在于文件系统中
    3. 消息队列:是消息的链接表,存储在内核中,有消息队列标识符标识;不一定按照先进先出的顺序读取消息,可以按照消息的类型字段读取消息
    4. 信号量(semephore)是一个计数器,用于实现进程间的互斥于同步,而不是用于存储进程间通信的数据。用于为多个进程提供共享数据对象的访问:使用信号量获取共享资源的步骤:1. 测试控制该资源的信号量;2. 信号量为正,进程使用该资源。并将信号量减一 3. 若信号量值为0,进程进入休眠状态。直到信号量大于0,进程被唤醒。信号种类:(1)二值信号量:只能取0或1 (2)通用信号量:可以取多个正整数
    5. 共享内存:指两个或多个进程共享一个给定的存储区。
      特点:1. 是最快的一种进程间通信(IPC,InterProcess Communication)2. 因为多个进程同时操作,所以需要进程同步 3. 信号量 + 共享内存通常结合使用

五种通讯方式总结

  1. 管道:速度慢,容量有限,只有父子进程能通讯
  2. FIFO:任何进程间都能通讯,但速度慢
  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
  4. 信号量:不能传递复杂消息,只能用来同步
  5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

博客:https://www.cnblogs.com/zgq0/p/8780893.html
https://www.jianshu.com/p/4989c35c9475

  1. 并行和并发的区别?
    并发:一个处理器可以同时处理多个任务。这是逻辑上的同时发生。
    并行:多个处理器同时处理多个不同的任务。这是物理上的同时发生。

  2. 线程安全的list:

    1. Vector, 所有方法都使用synchronized来同步保证线程安全
    2. Collections.SynchronizedList, 把List接口的实现类转换成线程安全的List
    3. CopyOnWriteArrayList, 复制再写入的List, 写时加锁(ReentrantLock可重入锁)
  3. HashMap中循环链表问题

    • HashMap扩容时对应同一个Hash位点数据, 后读取的会插在新建立的链表表头, 导致建立的新链表内容的相对位置倒序
    • 如果线程A计划扩容, 且读取到初始的链表之后, 线程B完成了扩容, 就有可能导致循环链表
      https://www.cnblogs.com/chanshuyi/p/java_collection_hashmap_17_infinite_loop.html
  4. 线程的状态

    • 创建: 生成线程对象, 没有调用start方法
    • 就绪: 调用start方法, 此时线程调度程序没有把该线程设置为当前线程. 或者线程运行时从等待或者睡眠中回来
    • 运行: 线程调度程序把处于就绪状态的线程设置为当前线程, 开始执行run函数中的代码
    • 阻塞: 被暂停, sleep方法或者suspend方法
    • 死亡: run方法结束
      Java中线程的6种状态:漫谈Java线程状态
      在这里插入图片描述
  5. synchronized关键字: https://zhuanlan.zhihu.com/p/29866981

    • 底层是使用操作系统的mutex lock(本身使用监视器锁, 监视器锁依赖mutex lock(互斥锁)实现的, 持有同一个锁的两个同步块只能串行进入
    • 线程释放锁时, JMM会把该线程对应的本地内存中的共享变量刷新到主内存中; 线程获取锁时, JMM会把该线程对应本地内存的共享变量设置为无效.
    • 线程A释放锁本质上是线程A向接下来要获取这个锁的线程发出线程A对共享变量做出了修改的消息; 线程B获取锁本质上是B接收到之前某个线程发出的在释放这个锁之前对共享变量做出修改的消息. 实际上是A线程向B线程发送消息的过程
    • JVM基于进入和退出Monitor对象来实现方法和代码块的同步, 每个monitoerenter对应一个monitorexit, synchronized同步块对同一条线程来说是可重入
  6. 偏向锁, 重量级锁, 轻量级锁
    https://blog.csdn.net/lengxiao1993/article/details/81568130

    • 几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁
    • 升级流程
      • 如果目标对象处于可偏向的状态, 使用CAS将对象头的MarkWord部分中, 标记上线程ID, 表示哪个线程获得了偏向锁
        • 如果CAS操作成功, 则认为获得了偏向锁, 一个程序执行完代码块后, 不会将对象头中的线程ID赋回原值
        • 如果CAS操作失败, 说明另一个线程B抢先获得了偏向锁, 需要撤销B的偏向锁, 将B持有的锁升级成轻量级锁
      • 如果是已偏向的状态, 检测MarkWord中存储的线程ID是否是当前线程ID
        • 如果相等, 则继续执行代码快
        • 如果不相等, 则撤销偏向锁 ,升级为轻量级锁
      • 偏向锁撤销后, 被竞争的对象可能处于两种状态
        • 不可偏向的无锁状态: 原来已经获取了偏向锁的线程可能已经执行完了同步代码块
        • 不可偏向的已锁状态: 原来已经获取了偏向锁的线程未执行完同步代码块, 此时对象被转换为轻量级加锁状态
      • 此时若是不可偏向的已锁状态, 则线程尝试使用CAS操作将对象头中的MarkWord替换为指向锁的指针
        • 如果成功则当前线程获得锁
        • 如果失败则先自旋, 再次尝试CAS争抢, 若未争抢到则升级成重量级锁(导致进程从用户态与内核态之间的切换, 开销较大)
  7. JMM和volatile
    https://blog.csdn.net/javazejian/article/details/72772461

    • JMM(Java内存模型)
      • JVM会为每个线程创建工作内存, 用于存储线程私有数据. 所有变量存储在主内存(共享区域). 所有线程对变量的操作在工作内存中进行, 将变量从主内存拷贝到工作内存空间进行操作, 再写回主内存
    • volatile
      • volatile变量在每次被线程访问时, 都强迫从主内存中读取, 发生改变后, 强迫将最新值刷新到主内存, 任何时刻, 不同线程总能看到改变量最新的值
      • 禁止指令重排序优化
      • 如果多线程对volatile变量的操作非原子, 则线程不安全
  8. ConcurrentHashMap原理

    • JDK1.7
      • 分段锁, 由一个Segment数组和多个HashEntry组成. Segment继承于ReentrantLock, 只有时加锁, value添加了volatile关键字所以读时不用加锁
      • put方法: 第一次hash确定Segment的位置, 如果Segments数组没有初始化, 就通过CAS赋值, 之后进行第二次hash找到相应的HashEntry的位置, 通过获取Segment获取锁.
      • get操作: 两次hash, 不加锁
      • size: 先不加锁进行统计, 如果两次结果一样, 则统计完成, 如果不同则最多尝试三次, 之后加锁进行统计
    • JDK1.8
      • 摒弃Segment, 使用Node数组 + 链表 + 红黑树, 并发控制使用Synchronized和CAS来操作
      • put方法: 初始化后如果没有hash冲突就直接CAS操作, 如果存在hash冲突, 就加锁插入, 统计是否需要转换成红黑树以及是否需要扩容, 乐观锁, 当有冲突的时候才进行并发处理
      • get操作: 计算hash, 遍历
      • size: 扩容和添加元素的时候计算
    • 对比
      • JDK1.8降低了锁的粒度(HashEntry), JDK1.7最小粒度时一个Segment
      • JDK1.8使用synchronized进行同步, 不需要分段锁
      • JDK1.8红黑树
  9. sleep、yield、wait、join的区别

  10. Java线程池是如何保证核心线程不被销毁的
    背景:对于Java中 Thread 对象,同一个线程对象调用 start 方法后,会在执行完run后走向终止(TERMINATED)状态,也就是说一个线程对象是不可以通过多次调用 start 方法重复执行 run 方法内容的。否则报错:IllegalThreadStateException异常
    答案:线程池当未调用 shutdown 方法时,是通过队列的 take 方法阻塞核心线程(Worker)的 run 方法从而保证核心线程不被销毁的
    参考:Java线程池是如何保证核心线程不被销毁的
    Java线程池是如何保证核心线程不被销毁的

java并发编程的任务

  1. 线程安全的list:

  2. Vector, 所有方法都使用synchronized来同步保证线程安全

  3. Collections.SynchronizedList, 使用synchronized

  4. CopyOnWriteArrayList, 复制再写入的List, 写时加锁(ReentrantLock可重入锁)
    注意: SynchronizedList读写速度差不多;CopyOnWriteArrayList写入速度较慢,读入速度非常宽

  5. 线程安全的容器:
    HashTable: synchronized
    ConcurrentHashMap: 分段多线程安全
    CopyOnWriteArraySet: 写时加锁复制

题目来源参考
https://www.cnblogs.com/Jansens520/p/8624708.html
https://www.cnblogs.com/xiaowangbangzhu/p/10443289.html

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值