1.Java实现多线程有哪几种方式。
参考:https://ryderchan.github.io/2017/02/18/java%E5%AE%9E%E7%8E%B0%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%9A%84%E4%B8%89%E7%A7%8D%E6%96%B9%E5%BC%8F/
实现 Runnable 接口;
使用ExecutorService、Callable、Future实现有返回结果;
继承 Thread 类。

 

2.Callable和Future的了解。

参考:http://blog.gavinzh.com/2017/08/22/java-Callable-Future/

https://blog.csdn.net/javazejian/article/details/50896505

 

3.线程池的参数有哪些,在线程池创建一个线程的过程。

参考:https://www.cnblogs.com/dolphin0520/p/3932921.html

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。

unit:参数keepAliveTime的时间单位。

workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响。

threadFactory:线程工厂,主要用来创建线程。

handler:表示当拒绝处理任务时的策略。

创建一个线程的过程:

参考:https://blog.csdn.net/u011240877/article/details/73440993
先定义线程池的几个关键属性的值:

private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU 数*2
private static final int MAXIMUM_POOL_SIZE = 64;    // 线程池最大线程数
private static final int KEEP_ALIVE_TIME = 1;    // 保持存活时间 1秒

设置核心池的数量为 CPU 数的两倍,一般是 4、8,好点的 16 个线程;
最大线程数设置为 64;
空闲线程的存活时间设置为 1 秒。

根据处理的任务类型选择不同的阻塞队列:
如果是要求高吞吐量的,可以使用 SynchronousQueue 队列;如果对执行顺序有要求,可以使用 PriorityBlockingQueue;如果最大积攒的待做任务有上限,可以使用 LinkedBlockingQueue

private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);

创建自己的 ThreadFactory:在其中为每个线程设置个名称:

private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());
        thread.setPriority(Thread.NORM_PRIORITY);
        return thread;
    }
};

然后就可以创建线程池了:

private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
        TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
        new ThreadPoolExecutor.DiscardOldestPolicy());

 

4.volitile关键字的作用,原理。

参考:https://www.cnblogs.com/dolphin0520/p/3920373.html

作用:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。

原理:
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

 

5.synchronized关键字的用法,优缺点。

同步一个代码块:

public void func() {
    synchronized (this) {
        // ...
    }
}

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

同步一个方法:

public synchronized void func () {
    // ...
}

它和同步代码块一样,作用于同一个对象。

同步一个类:

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

同步一个静态方法:

public synchronized static void fun() {
    // ...
}

作用于整个类。

优缺点:使用synchronized,当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗。

 

6.Lock接口有哪些实现类,使用场景是什么。

Lock接口有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

大量同步时,Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

 

7.可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)。

参考:https://blog.csdn.net/yanyan19880509/article/details/52345422

https://my.oschina.net/hosee/blog/639352

 

8.悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决。

悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

CAS缺陷:
循环时间太长、只能保证一个共享变量原子操作、ABA问题。

具体参考:https://blog.csdn.net/chenssy/article/details/69640293

 

9.ABC三个线程如何保证顺序执行。

package javatest;

public class test {

    public static void main(String[] args) {
        final Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("A");
            }
        });

        final Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    a.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("B");
            }
        });

        final Thread c = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    b.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("C");
            }
        });

        a.start();
        b.start();
        c.start();
    }
}

 

10.三个线程, 按顺序输出ABC, 循环10次。

package javatest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class test {

    private static Lock lock = new ReentrantLock();
    private static int state = 0;// 用state判断轮到谁执行
    private static final int RUN_NUMBER = 10;// 循环次数

    static class ThreadA extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < RUN_NUMBER;) {
                lock.lock();
                if (state % 3 == 0) {
                    System.out.println("第" + (i + 1) + "次:");
                    System.out.println("A");
                    state++;
                    i++;
                }
                lock.unlock();
            }
        }
    }

    static class ThreadB extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < RUN_NUMBER;) {
                lock.lock();
                if (state % 3 == 1) {
                    System.out.println("B");
                    state++;
                    i++;
                }
                lock.unlock();
            }
        }
    }

    static class ThreadC extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < RUN_NUMBER;) {
                lock.lock();
                if (state % 3 == 2) {
                    System.out.println("C");
                    state++;
                    i++;
                }
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        new ThreadA().start();
        new ThreadB().start();
        new ThreadC().start();
    }
}

 

11.线程的状态都有哪些。

新建(New):创建后尚未启动。
可运行(Runnable):可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。
阻塞(Blocking):等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
无限期等待(Waiting):等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
限期等待(Timed Waiting):无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
死亡(Terminated):可以是线程结束任务之后自己结束,或者产生了异常而结束。

 

12.sleep和wait的区别。

sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度)。

sleep()方法可以在任何地方使用,wait()方法则只能在同步方法或同步块中使用。

sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态。

 

13.notify和notifyall的区别。

notify:
唤醒一个等待在该对象监视器上的线程。如果有多个线程等待在该对象上,则选择其中的一个来唤醒。该选择是随机的并且由具体的JVM实现决定。一个线程通过调用该对象的wait方法来在该对象的监视器上进行等待的。
被唤醒的线程不能马上运行,直到当前线程释放了该对象的锁。被唤醒的线程以通常的方式来和其它正在想要获取该对象上锁的线程来竞争锁,并和其它线程处于同等地位,并没有什么特殊的地方。

notifyall:
唤醒所有等待该对象监视器的线程。一个线程可以通过调用该对象的wait方法来等待该对象的监视器。被唤醒的所有线程不能马上被执行,直到当前的线程释放了该对象的锁。
被唤醒的线程以通常的方式来和其它处于活跃状态的线程共同竞争该对象的锁,且并没有什么特殊性。

 

14.ThreadLocal的了解,实现原理。

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
参考:https://www.cnblogs.com/dolphin0520/p/3920407.html

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注