0.介绍
本文只是个人学习过程中的汇总,部分存在一些问题,欢迎指正。
1. 什么是并发?什么是并行?
- 并发是指多个线程争夺一个CPU,因为每个线程的执行时间很短,所以在微观上是轮换执行但在宏观上来看却是多个线程同时执行,这就是并发;
- 并行是指在多个CPU的情况下,每个CPU执行不同的线程,实现宏观以及微观上的多个线程同时执行。
2. 线程有多少个状态?
- 线程有5个状态,分别是新建,运行,等待,超时等待,终止;
- 线程新建完毕之后调用start之前,就处于新建状态;
- 运行状态分为就绪和正在运行,线程调用了start之后并不会立即执行,而是等待CPU调度,此时线程就处于就绪状态,当CPU空闲并调度了当前线程,则此时线程处于正在运行状态;
- 等待状态:一般是由于CPU调度的原因或者申请不到所需的资源进入阻塞等待状态,也可以通过Object.wait()方法或者Thread.join()方法使得当前线程进入到阻塞等待状态。如果是通过wait()方法进入的阻塞等待状态,那么需要通过Object.notify()或者Object.notifyAll()方法来唤醒;
- 超时等待:在阻塞等待的基础上增加一个阻塞时间,可以通过调用Thread.sleep(int)或者Thread.join(int)进入该状态,等待时间过了之后,该线程重新拿到CPU继续执行;
- 终止状态:线程运行完毕进入终止状态。
3. wait和sleep的区别?
- wait是Object类的方法,sleep是Thread类的方法;
- wait释放锁,而sleep不释放锁;
- wait不用捕获异常,sleep需要捕获异常;
- wait需要在同步代码块使用,而sleep没有这种要求;
- wait需要notify或者notidyAll唤醒,而sleep不需要,时间到了自动唤醒。
4. synchronized的作用?
- synchronized是Java提供的一个并发控制的关键字,主要有两种用法,分别是同步方法块和同步代码块;
- synchronized关键字可以给类或者对象进行加锁操作,保证共享资源在同一时间只会被一个线程访问到,这里要注意,对象锁针对的目标是实例化的对象,可以通过创建多个实例而实现多把锁,而类锁针对的目标是类文件,即Class本身,只存在唯一一个类锁;
- synchronized保证原子性,在同步代码块或者同步代码方法中,即在锁未释放前,共享资源无法被其他线程访问到,通过这种方式实现了synchronized代码的原子性;
- synchronized保证可见性,Java内存模型中规定了所有的变量都存储在主存,而每个线程又拥有自己的工作内存,线程对变量的所有操作都是基于工作内存的,而不能直接操作主存,synchronized保证,在解锁之前,必须把工作内存中的内存同步回主存中,通过这种方式保证了可见性;
- synchronized保证有序性,这里的有序性并不是指synchronized杜绝了指令重排的情况,而是说目标代码在单线程的情况下不管如何指令重排,结果都是一致的,而synchronized实现了加锁条件下其他线程的不可访问,即实现了单线程的环境,在这种情况下synchronized代码是有序的。
5. Lock接口的实现?
-
Lock接口和ReadWriteLock是锁的两大根接口,常见的实现类有ReentrantLock(可重入锁)和ReentrantReadWriteLock(可重入读写锁);
-
可重入锁的相关API:
void lock(); //获取锁 void lockInterruptibly(); //如果当前线程未被中断,则获取锁,可以响应中断 Condition newCondition(); //返回绑定到此Lock实例的心Condition实例 boolean tryLock(); //非阻塞式获取锁,成功返回true,失败返回false boolean tryLock(long time,TimeUnit unit); //在tryLock()的基础上添加等待时间 void unlock(); //释放锁
-
读写锁的相关API:
Lock readLock(); //获得读锁,共享锁 Lock writeLock(); //获得写锁,排他锁
-
Lock接口的实现类经常搭配Condition进行线程间的通信;
-
Lock接口通常搭配使用Condition实现线程间的通信,具体实现方式如下:
Condition condition = lock.newCondition(); //获得监视器对象 condition.await(); //等待,相当于Object.wait(); condition.signal(); //唤醒,相当于Object.notify(),但是是精准唤醒;
常规的synchronized加锁,wait/notify唤醒机制无法实现精准唤醒,而Lock加锁,await()/signal()则可以实现多路监视并精准唤醒,同时锁的粒度也更加地小;
-
考虑到同一时刻允许多个线程对共享资源进行读操作,同一时刻只允许一个线程对共享资源进行写操作。Lock接口还有一个常用实现类ReentrantReadWriteLock,用于实现读写分离,一定程度上提高了线程执行的效率。
6. 可重入锁的构造方式有哪些?默认构造是怎样?
- 有两种,可以构造公平锁或非公平锁;
- 默认获得非公平锁:Lock lock = new ReentrantLock();
- 有参数获得公平锁:Lock lock = new ReentrantLock(true);
7. synchronized和Lock的区别?
- synchronized是一个关键字,而Lock是一个接口;
- synchronized可以自动获取锁自动释放锁,即使线程执行捕获异常也会正常释放,而Lock需要自己加锁自己解锁,如果遇到异常的时候可能会造成死锁问题,所以需要在finally代码块中释放锁保证一定释放;
- synchronized获取锁的过程是阻塞式的且锁的状态无法确定,而Lock则有多种方式获取锁,同时锁的状态是可以确定的;
- synchronized是可重入的,不可中断的非公平锁,Lock是可重入的,可判断且可中断的,默认非公平锁;
- 在资源竞争激烈的情况下,使用ReentrantLock的性能优于synchronized,反过来在资源竞争不是那么激烈的情况下,synchronized的性能则要更好一些。synchronized适合少量同步代码,而Lock适合大量同步代码;
- synchronized底层维护两个指令,加锁指令给锁计数器+1,解锁指令给锁计数器-1,通过0/1判断当前锁的状态;Lock底层是CAS(Compare And Swap)乐观锁,依赖AbstractQueueSynchronizer类,把所有请求线程构成一个FIFO双向队列,对该队列进行操作。
8. 什么是锁?如何判断锁的是谁?
- 锁是将某种共享资源私有化的一种手段,包括某个方法,某个变量或者某个通道,它可以保证私有资源在某个时刻下只能被一个占用,只有当这个锁释放了另外的线程才可以使用;
- synchronized关键字锁的目标是对象或者类文件,而Lock锁的目标也是;
- Lock锁的使用一般是在对象的方法中去使用,根据该方法的是否是静态方法来判断锁的是对象还是类文件。
9. 什么是虚假唤醒问题?
-
当一定的条件出发时会唤醒很多在阻塞态的线程,但在逻辑上只有部分的线程唤醒是有效的,其余线程的唤醒则是多余的;
-
在典型的生产者消费者问题中,虚假唤醒的案例如下:
public class ConsumerAndProducer { public static void main(String[] args){ Product product = new Product(); Producer producer = new Producer(product); Consumer con1 = new Consumer(product); Consumer con2 = new Consumer(product); new Thread(()->{ for(int i=0;i<20;i++){ producer.run(); } },"工厂").start(); new Thread(()->{ for(int i=0;i<10;i++){ con1.run(); } },"顾客A").start(); new Thread(()->{ for(int i=0;i<10;i++){ con2.run(); } },"顾客B").start(); } } class Product{ private int num = 0; Lock lock = new ReentrantLock(); Condition producer = lock.newCondition(); Condition consumer = lock.newCondition(); public void create(){ lock.lock(); try{ if(num>0){ producer.await(); } num++; System.out.println(Thread.currentThread().getName()+"生产了一个产品,目前:"+num); consumer.signal(); }catch(Exception e){ }finally{ lock.unlock(); } } public void consume(){ lock.lock(); try{ if(num==0){ consumer.await(); } num--; System.out.println(Thread.currentThread().getName()+"消费了一个产品,目前:"+num); producer.signal(); }catch(Exception e){ }finally{ lock.unlock(); } } } class Consumer{ public Product product; public Consumer(Product product){ this.product = product; } public void run(){ product.consume(); } } class Producer{ public Product product; public Producer(Product product){ this.product = product; } public void run(){ product.create(); } }
-
运行结果:
工厂生产了一个产品,目前:1 顾客A消费了一个产品,目前:0 工厂生产了一个产品,目前:1 顾客A消费了一个产品,目前:0 工厂生产了一个产品,目前:1 顾客A消费了一个产品,目前:0 顾客B消费了一个产品,目前:-1 顾客B消费了一个产品,目前:-2 顾客B消费了一个产品,目前:-3 顾客B消费了一个产品,目前:-4 顾客B消费了一个产品,目前:-5 工厂生产了一个产品,目前:-4 工厂生产了一个产品,目前:-3 工厂生产了一个产品,目前:-2 工厂生产了一个产品,目前:-1 工厂生产了一个产品,目前:0 工厂生产了一个产品,目前:1 顾客A消费了一个产品,目前:0 工厂生产了一个产品,目前:1 顾客A消费了一个产品,目前:0
-
虚假唤醒的原因:因为判断条件是使用if语句,在这种情况,producer.signal()会唤醒所有处于监视状态下的线程,而非部分线程,比如说,在这个案例里有2个消费者,但是它会在只有1个产品的情况下唤醒所有消费者,此时就会出现产品数目是负数的情况,即它没有办法在阻塞等待状态退出后逻辑条件判断不成立的情况下再次进入阻塞等待状态,因此,在逻辑判断的时候使用while语句进行判断即可。
10. Condition的优势?可以根据角色不同实例化不同的Condition,从而实现精准唤醒。
11. 生产者和消费者问题的代码实现:
-
synchronized
public class ConsumerAndProducer { public static void main(String[] args){ Product product = new Product(); Producer producer = new Producer(product); Consumer con1 = new Consumer(product); Consumer con2 = new Consumer(product); new Thread(()->{ for(int i=0;i<20;i++){ producer.run(); } },"工厂").start(); new Thread(()->{ for(int i=0;i<10;i++){ con1.run(); } },"顾客A").start(); new Thread(()->{ for(int i=0;i<10;i++){ con2.run(); } },"顾客B").start(); } } class Product{ private int num = 0; public synchronized void create() throws InterruptedException { if(num>0){ this.wait(); } num++; System.out.println(Thread.currentThread().getName()+"生产了一个产品,目前:"+num); this.notifyAll(); } public synchronized void consume() throws InterruptedException { if(num==0){ this.wait(); } num--; System.out.println(Thread.currentThread().getName()+"消费了一个产品,目前:"+num); } } class Consumer{ public Product product; public Consumer(Product product){ this.product = product; } public void run(){ try { product.consume(); } catch (InterruptedException e) { e.printStackTrace(); } } } class Producer{ public Product product; public Producer(Product product){ this.product = product; } public void run(){ try { product.create(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
Lock
public class ConsumerAndProducer { public static void main(String[] args){ Product product = new Product(); Producer producer = new Producer(product); Consumer con1 = new Consumer(product); Consumer con2 = new Consumer(product); new Thread(()->{ for(int i=0;i<20;i++){ producer.run(); } },"工厂").start(); new Thread(()->{ for(int i=0;i<10;i++){ con1.run(); } },"顾客A").start(); new Thread(()->{ for(int i=0;i<10;i++){ con2.run(); } },"顾客B").start(); } } class Product{ private int num = 0; Lock lock = new ReentrantLock(); Condition producer = lock.newCondition(); Condition consumer = lock.newCondition(); public void create(){ lock.lock(); try{ if(num>0){ producer.await(); } num++; System.out.println(Thread.currentThread().getName()+"生产了一个产品,目前:"+num); consumer.signal(); }catch(Exception e){ }finally{ lock.unlock(); } } public void consume(){ lock.lock(); try{ if(num==0){ consumer.await(); } num--; System.out.println(Thread.currentThread().getName()+"消费了一个产品,目前:"+num); producer.signal(); }catch(Exception e){ }finally{ lock.unlock(); } } } class Consumer{ public Product product; public Consumer(Product product){ this.product = product; } public void run(){ product.consume(); } } class Producer{ public Product product; public Producer(Product product){ this.product = product; } public void run(){ product.create(); } }
12. 关于线程执行顺序与锁的8个问题——判断加锁锁的是对象还是类文件,判断多对象情况下是否会阻塞同步代码;
13. 集合类不安全
-
什么是并发修改异常?
并发修改异常是一个RuntimeException,这个异常通常情况下在多线程的并发访问容器时抛出。容器类内部维护一个变量modiCount,用于记录容器被修改的次数,在多线程的环境下,容易出现modiCount不一致的情况,这时候就会抛出并发修改异常。在单线程的环境下也有可能抛出并发修改异常。
-
List解决方案:
-
使用Vector,底层源码使用synchronized关键字,保证线程安全;
-
使用集合工具类;
List<> list = Collections.synchronizedList(new ArrayList<>());
-
使用CopyOnWriteArrayList类,采用了写时复制的思想和Lock,保证了线程安全。
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); //加锁了 try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
-
-
Set解决方案:
-
使用集合工具类;
Set<> set = Collections.synchronizedSet(new HashSet<>());
-
使用CopyOnWriteArraySet类,底层复用了CopyOnWriteArrayList类。
-
-
Map解决方案:
-
使用集合工具类;
Map<> map = Collections.synchronizedMap(new HashMap<>());
-
使用HashTable或ConcurrentHashMap,加锁的方式有什么不同?
-
Map1.7线程不安全的原因,1.8线程不安全的原因?
-
14. 线程实现的几种方式:
-
继承Thread类,重写run方法;
-
实现Runnable接口,重写run方法;
class MyRunnable implements Runnable{ @Override public void run(){ ……………… } } public class Test{ public static void main(String[] args){ new Thread(new MyRunnable()).start(); } }
-
实现Callable接口,重写call方法;
class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception{ ……………… return int; } } public class Test{ public static void main(String[] args){ MyCallable callable = new MyCallable(); FutureTask<Integer> task = new FutureTask<>(callable); new Thread(task).start(); try{ int result = task.get(); }catch(Exception e){ } } }
-
使用线程池创建线程。
15. FutureTask的作用?
- FutureTask实现了Future接口和Runnable接口;
- Future接口建模了一种异步计算,返回一个执行运算结果的引用,即get()方法,运算过程结束则返回,运算过程未结束则阻塞等待;
- FutureTask实现了Runnable接口,所以对于实现了Callable接口的类,需要多封装一层交给FutureTask去执行任务。
16. CountDownLatch,CyclicBarrier,Semaphore
-
CountDownLatch:线程减法计数器,类似于join的作用,用于某个线程等待其他线程执行完任务再执行;
public class Test{ staric final int N=4; static CountDownLatch latch = new CountDownLatch(N); public static void main(String[] args){ for(int i=0;i<N;i++){ new Thread(()->{ try{ System.out.println("线程"+Thread.currentThread().getName()+"正在执行"); }finally{ latch.countDown(); } }).start(); } //latch.await() 直到CountDownLatch中的计数值归0,即for循环中的四个线程全部执行完毕,才会执行后面代码 //latch.await(1000,TimeUnit.MILLSECONDS) //等待一定时间后,若计数值还没有变为0,则继续执行 System.out.println("线程main结束"); } }
-
CyclicBarrier:线程加法计数器,它的作用就是让所有线程都等待完成后才会继续下一步行动;
//构造方法 public CyclicBarrier(int parties); public CyclicBarrier(int parties,Runnable barrierAction); //parties是参与线程的个数 //barrierAction是最后一个到达线程要执行的任务 //重要方法: public int await() throws InterruptedException,BrokenBarrierException; public int await(long timeout,TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException; //调用await()方法的线程,告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞,直到patrties个参与线程到达同步点为止,有参数的情况下,可能会出现超时而抛出异常的情况,这里await()也有可能因为同一代的其他线程被中断而抛出BrokenBarrierException。
public class Test{ public static CyclicBarrier barrier = new CyclicBarrier(3,new Thread(()->{ System.out.println("线程"+Thread.currentThread().getName()+"最后完成任务"); })); public static void main(String[] args){ for(int i=0;i<3;i++){ new Thread(()->{ System.out.println("线程"+Thread.currentThread().getName()+"已到达栅栏"); try { barrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
运行结果: 线程0已到达栅栏 线程1已到达栅栏 线程2已到达栅栏 线程2最后完成任务
-
CountDownLatch和CyclicBarrier的区别:
- CountDownLatch是一次性的而CyclicBarrier可以重复使用;
- CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
- CountDownLatch中参与线程的职责不一样,而CyclicBarrier中参与线程的职责都是一致的;
- CountDownLatch的底层实现是基于AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。
-
Semaphore:计数信号量,类似于锁,可以用来控制同时访问特定资源的线程数量,保证合理地使用资源,常用于限流,比如:数据库连接池。
常用API:
Semaphore semaphore = new Semaphore(2); //默认创建一个非公平的锁的同步阻塞队列,同时将初始令牌数量赋值给同步队列的state状态 semphore.acquire(); //当前线程尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子操作更改同步队列的state,每次获取都要减1 //当计算出来的state<0,则说明此时令牌不够,会创建一个Node节点加入阻塞队列,挂起当前线程 semaphore.release(); //线程会尝试释放一个令牌,同时使得同步队列的state状态加1 //释放令牌成功之后,同时会唤醒同步队列的一个线程
public class Test{ public static void main(String[] args){ Semaphore semaphore = new Semaphore(2); for(int i=0;i<5;i++){ new Thread(()->{ try{ semaphore.acquire(); System.out.println("线程"+Thread.currentThread().getName()+"正在占用"); Thread.sleep(2000); System.out.println("线程"+Thread.currentThread().getName()+"运行结束"); }catch(Exception e){ }finally{ semaphore.release(); } }).start(); } } }
运行结果
线程Thread-0正在占用 线程Thread-1正在占用 线程Thread-1运行结束 线程Thread-0运行结束 线程Thread-2正在占用 线程Thread-3正在占用 线程Thread-3运行结束 线程Thread-2运行结束 线程Thread-4正在占用 线程Thread-4运行结束 Process finished with exit code 0
17. 在线程中如何拿到外部的局部变量?为什么这么用?
在线程中,或者说在内部类中调用的外部变量必须是final修饰的,主要是为了解决局部变量的生命周期和局部内部类的生命周期不一致的问题。如果不用final修饰,当外部类方法执行完毕时,这个局部变量也就被GC了,然而内部类的某个方法还没有执行完,这个时候它所引用的外部变量已经找不到了。
18. 什么是自旋锁?什么是互斥锁?
- 自旋锁是为了实现保护共享资源的互斥使用而提出的一种锁机制,和互斥锁比较类似,但是两者在调度机制上略有不同;
- 对于互斥锁,如果请求资源已经被占用了,那么资源申请者只能进入阻塞状态,等待重新调度请求;
- 对于自旋锁,如果请求不到目标资源,该线程将会等待,间隔一段时间后再次尝试获取,相比较于互斥锁,自旋锁不会因为请求不到资源而阻塞本身线程。
19. 读写锁的作用?区别?
-
读写锁是一种特殊的自旋锁,它把对共享资源的访问划分为读锁和写锁,读锁指堆共享资源进行读访问,写锁则需要对共享资源进行写操作,同时读锁是共享的而写锁是独占的;
-
读写锁允许读与读之间是共享的,即多个线程之间可以实现并行读,但不允许读锁与写锁共存,同时写锁是排他的;
-
读写锁是非公平锁,允许读请求插队也允许写请求插队,但是读插队的前提是队列中的头结点不能是写请求的线程,即插队的前提是前面只有读请求线程;如果不这么做的话,可能会出现线程饥饿的情况,即线程1,线程2并行读,此时线程3请求写锁,于是等待,然后又有一个读请求线程4,如果这时候读线程允许插队的话,那么容易造车线程3一直处于等待状态,即线程饥饿;
-
锁的升降级:允许降级不允许升级,允许锁的升级容易造成死锁。假设有线程1和线程2实现并行读,此时线程1,线程2的读锁想要升级,都要求对方释放读锁,互相等待形成死锁,因此不允许升级;
-
锁降级的必要性:保证数据的可见性;假设在写请求后有一个读请求,然后写锁解锁,但是解锁完成后的一瞬间,又有一个写请求同时先执行写请求,这种情况下,当新的写请求执行完毕后再执行读请求,此时的数据并不是最新的数据,因此需要引入锁降级;
-
锁降级的实现:
Lock lock = new ReentrantLock(); Lock write = lock.writeLock(); Lock read = lock.readLock(); ………………………………; public void test(){ write.lock(); try{ ………………………………; read.lock; }catch(){ }finally{ read.unlock(); write.unlock(); } }
20. 阻塞队列
-
在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue,DelayQueue,LinkedBlockingDeque,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue,它们的区别主要体现在存储结构或对元素操作上不同。
-
同步阻塞队列——SynchronousQueue
- SynchronousQueue是一个特殊的队列,因为它的容量只有1,当其中有元素时,任何其他线程进行put(object)都会被阻塞;当其中为空时,任何其他线程进行take()都会被阻塞;
-
数组阻塞队列——ArrayBlockingQueue
- ArrayBlockingQueue是一个有界的阻塞队列,内部实现是将对象放到一个数组里,有界意味着不能存放无限多的元素,在初始化的时候需要设定其上限,一旦初始化便无法修改;
-
链表阻塞队列——LinkedBlockingQueue
- LinkedBlockingQueue以一个链式结构对其元素进行存储,可以在初始化的时候设定存储上限,默认的大小是Integer.MAX_VALUE作为上限;
-
延迟阻塞队列——DelayedQueue
- DelayedQueue底层使用PriorityQueue来实现,队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取,只有在延迟期满时才能从队列中提取出元素,如果队列中没有符合条件的元素,那么poll()的结果为空;同时,延迟队列的内部会根据剩余时间的多少来进行排序,处于队头位置的是最接近延时时间的或超过延时时间最多的元素;
-
优先阻塞队列——PriorityBlockingQueue
- PriorityBlockingQueue是一个支持优先级的无界阻塞队列,默认情况下元素采取自然顺序升序排列。放置其中的元素必须实现Comparable接口,该队列中元素的排序就取决于自定义的Comparable实现;
-
链表双端队列——LinkedBlockingDeque
- LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,既可以从队首插入和删除,也可以从队尾插入和删除,每个方法中有First和Last的区分。链表双端队列可以运用在"工作窃取"模式中。
21. 阻塞队列四组API:
抛异常 | 有返回值不抛异常 | 阻塞等待 | 超时等待 | |
---|---|---|---|---|
插入 | add(o) | offer(o) | put(o) | offer(o,timeout,unit) |
移除 | remove(o) | poll(o) | take(o) | poll(o,timeout,unit) |
检查 | element(o) | peek(o) |
抛异常:如果试图执行的操作无法立即执行,那么就会抛出一个异常;
有返回值不抛异常:如果试图执行的操作无法立即执行,此类方法不会抛出异常但不论执行成功失败都会返回一个目标值,成功返回true失败返回false;
阻塞等待:如果试图执行的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行为止;
超时等待:如果试图执行的操作无法立即执行,该方法调用将会发生阻塞,在阻塞时间内执行成功则返回true,否则返回false;
无法向一个BlockingQueue中插入null,如果试图插入null,BlockingQueue会抛出一个NullPointerException。
22. 线程池
-
为什么使用线程池?
- 可以降低资源的消耗;
- 可以提高响应的速度;
- 方便管理;
-
三大方法——不允许通过使用Executors创建线程池,而是通过ThreadPoolExecutors的方式,这样的好处是可以让编写代码的人员更加明确线程池的运行规则,规避资源耗尽的风险;
- Executors.newSingleThreadExecutor(); 单一线程,多余线程放到阻塞队列
- Executors.newFixedThreadPool(int num); 指定最大可执行线程数,多余线程放到阻塞队列
- Executors.newCachedThreadPool(); 可伸缩,根据任务量决定线程池的大小
缺点:
- FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM;
- CachedThreadPool允许创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程从而导致OOM;
-
七大参数
-
建议使用以下方式创建线程池:
ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,timeUnit,blockingQueue,threadFactory,handler);
-
corePoolSize:核心线程数
-
maxPoolSize:最大线程数,最大线程数大于等于核心线程数
-
keepAliveTime:非核心线程空闲时的存活时间
-
timeUnit:存活时间单位
-
blockingQueue:阻塞队列
-
threadFactory:线程工厂,创建线程的,一般不用动
-
handler:属于类RejectedExecutionHandler,拒绝策略
-
-
四大拒绝策略
-
new ThreadPoolExecutor.AbortPolicy(); 阻塞队列满了且线程数达到最大则抛弃任务且抛出异常
-
new ThreadPoolExecutor.CallerRunsPolicy(); 返回给调用线程执行任务
-
new ThreadPoolExecutor.DiscardPolicy(); 阻塞队列满了且线程数达到最大则抛弃任务,不抛出异常
-
new ThreadPoolExecutor.DiscardOldestPolicy(); 阻塞队列满了且线程数达到最大
丢弃队列最前面的任务重新提交目标任务
-
-
CPU密集型和IO密集型的选择策略
-
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 CPU数目 + 1。这里的+1是因为,计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作;
-
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,但CPU的使用率不高。简单的说,就是需要大量的输入输出,不如读文件、写文件、传输文件、网络请求。如果是IO密集型任务,参考值可以设置为 2 * CPU数目。可以根据实际情况使用相关公式进行计算:
Nthreads = Ncpu x Ucpu x (1 + W/C) //Nthreads:目标线程数 //Ncpu:CPU数目 //Ucpu:目标CPU的使用率 //W/C:等待时间(主要是I/O)与计算时间的比率
-
-
如何知道自己的电脑中有多少个CPU
-
int N_CPUS = Runtime.getRuntime().availableProcessor();
-
23. 什么是函数式接口?
-
lambda表达式——是一个匿名函数,()->{ 核心代码 },可以简化代码的书写;
-
链式编程——对于建造者模式的应用,在函数的末尾返回本身,即return this,因此可以实现反复调用;
-
函数式接口(四大基本函数式接口)——简化方法的定义
-
定义:
- 只包含一个抽象方法的接口,称为函数式接口;
- 可以通过lambda表达式来创建该接口的对象;
-
函数型接口——Function<T,R>,参数类型为T,返回类型为R,重写apply()方法;
Function<String,Void> function = new Function<String, Void>() { @Override public Void apply(String s) { System.out.println(s); return null; } }; function.apply("Hello World");
-
断定型接口——Predicate<T>,参数类型为T,返回类型为Boolean,重写test()方法;
-
消费型接口——Consumer<T>,参数类型为T,无返回类型,重写accept()方法;
-
供给型接口——Supplier<T>,无参数,返回类型为T,重写get()方法。
-
-
Stream流式计算——流用于集合的计算,例如List集合;
-
forEach——借助消费性函数型接口,实现遍历
-
filter——可以通过filter方法将一个流转换为另一个子集流,该方法接受一个断言型接口
-
map——将流中的数据映射到另一个流中,该方法接受一个函数型接口,可以将T类型元素转换为R类型元素
-
count——统计流中的元素个数
-
limit——取用流中的前几个元素
-
skip——跳过流中的前几个元素
-
concat——将两个流合并
-
应用:
/** . *题目要求:一分钟内完成此题,只能用一行代码实现! *现在有5个用户!筛选: * 1、ID必须是偶数 * 2、年龄必须大于23岁 * 3、用户名转为大写字母 * 4、用户名字母倒着排序 * 5、只输出一个用户! */ public class User { public int id; public String name; public int age; } public class Test { public static void main(String[] args) { User u1 = new User(1,"a",21); User u2 = new User(2,"b",22); User u3 = new User(3,"c",23); User u4 = new User(4,"d",24); User u5 = new User(5,"e",25); User u6 = new User(6,"f",26); User u7 = new User(7,"g",27); User u8 = new User(8,"h",28); List<User> list = Arrays.asList(u1, u2, u3, u4, u5, u6, u7, u8); list.stream().filter(user->{return user.id%2==0;}) .filter(user->{return user.age>23;}) .map(user->{return user.name.toUpperCase();}) .sorted((u1,u2)->{return u2.comparedTo(u1)};) .limit(1) .forEach(user->{System.out.println(user);}); } }
-
24. ForkJoin——分支合并,用于并行执行任务。特点是工作窃取,实现结构是双端队列。
- 是一个可以将大任务分割成小任务后并行运行,然后将小任务的最终结果合并成大任务的最终结果的框架。被分割的子任务还可以继续分割以满足实际需求;
- 使用Fork/Join的一个前提是:子任务之间是互相独立的;
- 框架内部使用了线程池执行各个子任务,它的工作原理为,线程池中的每个线程都有自己的工作队列,当自己的工作队列中的任务都完成之后会从其他线程中窃取一个任务执行,提高运行效率;也正是因此,所以工作队列使用双端队列的结构来维护;
25. ForkJoin怎么使用?
-
ForkJoinPoll:用于执行调度分割的线程池,实现了ExecutorService接口并提供三种执行任务的方式:
- execute——异步执行,没有返回结果;
- submit——异步执行且有返回结果,返回结果是封装后的Future对象,可以通过get获取;
- invoke和invokeAll:调用线程直到任务执行完成才会返回,同步方法且有返回结果;
-
ForkJoinTask:抽象类,表示运行在ForkJoinPool中的任务,实现了Future接口,是一个异步任务,主要有以下方法:
- fork——在当前任务正在运行的池中异步执行任务,简单理解就是再创建一个子任务;
- join——任务完成时返回结算结果;
- invoke——开始执行并等待任务完成;
这个类有两个子类:
- RecusiveAction:异步任务,无返回结果,自定义的任务需要继承该类并重写compute方法;
- RecusiveTask:异步任务,有返回结果,自定义的任务需要继承该类并重写compute方法。
-
代码:
public class SumTask extends RecursiveTask<Integer> { private static final int THRESHOLD = 3; //任务中计算数字最大上限 private int start; private int end; public SumTask(int start,int end){ this.start = start; this.end = end; } @Override protected Integer compute(){ int sum = 0; boolean flag = (end-start)<THRESHOLD; if(flag){ System.out.println("计算区间为:"+start+"-"+end); for(int i=start;i<=end;i++){ sum = sum + i; } }else{ int mid = (start+end)/2; SumTask task1 = new SumTask(start,mid); SumTask task2 = new SumTask(mid+1,end); task1.fork(); task2.fork(); if(task1.isCompletedAbnormally()){ System.out.println(task1.getException()); } if(task2.isCompletedAbnormally()){ System.out.println(task2.getException()); } int res1 = (int)task1.join(); int res2 = (int)task2.join(); sum = res1 + res2; } return sum; } public static void main(String[] args){ ForkJoinPool pool = new ForkJoinPool(); SumTask task = new SumTask(1,10); Future<Integer> result = pool.submit(task); try{ System.out.println(result.get()); }catch(Exception e){ } } }
26. 异步调用——Future<T>接口,通过get方法获得返回值
27. JMM是什么?——Java内存模型
-
在Java中,所有的变量都存储在主内存中,而每条线程都有自己的工作内存,在线程运行期间,对变量的任何操作都需要从主内存中取得变量,放到工作内存,然后再进行操作,这是虚拟机的规范;
-
JMM对于共享变量的获取流程——对此有4组8个方法提供相关操作:
- lock:作用于主内存的变量,将其标识为某一线程占用的状态;
- unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来;
- read:作用于主内存的变量,把一个变量值从主内存传输到工作内存中,搭配load一起使用;
- load:作用于工作内存的变量,它把read操作得到的变量值放入工作内存的变量副本中;
- user:作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
- assign:作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作变量;
- store:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,搭配write使用;
- write:作用于主内存的变量,它把store操作从工作内存取到的变量值传送到主内存的变量中;
- 基本上要求成对使用!!!
-
如何在多线程中保证共享变量的可见性——使用volatile关键字
28. 讲一下volatile?——使用内存屏障实现
- 保证可见性
- 一旦线程对这个共享变量的副本做了修改,会立马刷新最新值到主内存中;
- 一旦线程对这个共享变量的副本做了修改,其他线程中对这个共享变量拷贝的副本值会失效,其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载;
- 内存屏障的一个作用是刷新各种CPU缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本;
- 不保证原子性——因为修改操作包括取值,修改,更新,不是原子操作,有一定时延
- 怎么解决不保证原子性的问题?为什么?——使用原子类
- 禁止指令重排
- 什么是指令重排?怎么做到禁止指令重排?
- 代码并不是按照我们编写的顺序进行运行的,编译器会进行代码优化,在这个过程中指令会进行重排,而volatile修饰的变量则禁止指令重排;
- 底层使用内层屏障实现禁止指令重排,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
29. 一个main运行中至少有多少个线程在执行?
Reference Handler:用于处理引用对象本身的垃圾回收问题;
Finalizer:用于垃圾收集前,调用对象的finalize方法;
Attach Listener:负责接收到外部的命令,并把结果返回给发送者;
Signal Dispatcher:Attach Listener接收到指令,会交给该线程去进行分发到各个模块处理命令然后返回结果;
Monitor Ctrl-Break:Idea运行时会出现该线程,好像是做监视的;
虽然创建的线程有6个,但是活动线程一般只有两个,即main和Monitor Ctrl-Break,因此,在多线程开发中,判断子线程是否结束的代码是
While(Thread.activeCount()>2){
Thread.yield();
}
//在windows中Monitor Ctrl-Break不算活动线程,应该是1,在linux下做判断则是2
30. 单例模式
-
饿汉——一上来就加载,可能会浪费空间
public class HungrySingleton{ private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton(){ //构造方法 } public HungrySingleton getInstance{ return instance; } }
-
懒汉——需要使用再加载
public class LazySingleton{ private static volatile LazySingleton instance; private LazySingleton(){ //构造方法 } public static synchronized LazySingleton getInstance() { //getInstance 方法前加同步 if (instance == null) { instance = new LazySingleton(); } return instance; } public static LazySingleton getInstance() { //getInstance 方法前加同步 if (instance == null) { synchronized(LazySingleton.class){ if(instance==null){ instance = new LazySingleton(); } } } return instance; } }
-
静态内部类
public class Singleton{ private Singleton(){ //构造方法 if(SingletonHolder.singleton==null){ throw new IllegalStateException(); } //防止反射 } private static class SingletonHolder{ private static final Singleton singleton = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.singleton; } }
好处:兼顾了懒汉模式的内存优化和饿汉模式的安全(不被反射入侵?有待考究),不使用sunchronized关键字,时间上得到了优化;
坏处:需要多加载一个类;
-
懒汉式是否安全?不安全,有反射入侵问题和序列化问题。
31. 单元素的枚举类型已经成为实现Singleton的最佳方法(避免反射,反序列化问题)
public enum Singleton{
INSTANCE;
public void doSomething(){
//自定义代码;
}
public static void main(String[] args){
Singleton.INSTANCE.doSomething();
}
}
枚举即使被反射获取到了构造器,也是安全的,因为Constructor类的newInstance源码中,如果想要新建的实例是枚举类型的,那么它会异常,然后反射失败。
32. 什么是CAS?
- CAS,Compare And Swap,比较并交换,是线程并发运行时用到的一种技术;
- CAS是原子操作,可以保证并发安全;AtomicInteger底层调用了该方法;
- CAS是CPU的一个指令;(需要JNI调用Native方法,才能调用CPU的指令);
- CAS是乐观锁的主要实现方式。
存在的问题:
- ABA问题;
- 如果与期望值的比较结果是false,会导致长时间自旋,消耗CPU资源;
- 只能保证一个共享变量的原子性;
33. CAS的ABA问题怎么解决?——添加一个版本号判断,先比较版本号再比较值
AtomicStampedReference——带版本号的引用类型原子类,可用于解决ABA问题
34. 什么是乐观锁,悲观锁?有什么区别?
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
35. 关于Integer在原子引用中可能存在的一个问题?
Integer类型在请求-128~127的数据时,得到的都是同一个数据,而请求这之外的数据时,因为返回的结果是直接新建的实例,即使值相等,但是==判断的结果仍旧是false。
36. 各种锁:
-
公平锁和非公平锁——是否先申请先得到锁;
-
可重入锁——可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响;
-
自旋锁——获取锁时如果获取不到则一直尝试,而非阻塞线程,互斥锁则选择阻塞线程;
-
乐观,悲观锁。