JUC其实就是JDK中的三个包:
java.util.concurrent 并发相关的
java.util.concurrent.atomic 原子性
java.util.concurrent.locks lock锁
Runnable 没有返回值,效率相比Callable相对较低。
进程:一个程序,QQ,Music.exe 程序的集合 ;一个进程可以包含多个线程,至少包含一个。
Java默认有几个线程? 2个 main线程和GC线程(做垃圾回收)
Java可以开启线程嘛?
不可以,它是通过本地方法,也就是native方法调用底层的C++。 Java无法直接操作硬件。
并发编程的本质:
充分利用CPU的资源。
线程的生命周期都有:
NEW(新生),RUNNABLE(运行),BLOCKED(阻塞),WAITING(等待,四死死地等),TIMED_WAITING(超时等待),TERMINATED(终止)
wait和sleep的区别:
1.来自不同的类 wait来自Object类 sleep来自Thread类。
企业当中不会用sleep的。
2.关于锁的释放
wait会释放锁,sleep睡觉了但是不释放锁。
3.使用的范围是不同的
wait只能在同步代码块中。
sleep可以在任何地方睡。
4.是否需要捕获异常
wait不需要捕获异常
sleep需要捕获异常
Lock是一个接口,具有三个实现类:
ReentrantLock可重入锁, ReentrantReadWriteLock.ReadLock读锁, ReentrantReadWriteLock.WriteLock写锁
ReentrantLock里面有个概念:
public ReentrantLock(boolean fair){
//公平锁和非公平锁 公平锁:先来后到 非公平锁:可以插队(默认的)
非公平锁容易造成线程饥饿,也就是说有的线程长时间得不到锁。但是在高并发的场景下,非公平锁的性能会更高,开销更小。公平锁性能比较低,开销比较大的原因就是,因为公平锁讲究一个先来后到,所以公平锁需要管理一个有序队列。
sync = fair ? new FairSync() : new NonfairSync();
}
class Ticket2{
private int number = 30;
//可重入锁
Lock lock = new ReentrantLock();
public void sale(){
lock.unlock();
try{
//业务代码
if(number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}catch(Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
Synchronized 和 Lock的区别是什么?
1. Synchronized 内置的Java关键字 Lock是一个Java类,一个接口。
2.Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁,isHeldByCurrentThread(),检查当前线程是否持有此锁,isLocked(),是否有任何线程持有此锁,如果有返回true。查询等待队列中的线程数量:getQueueLength()。
3.Synchronized 会自动释放锁, Lock需要手动释放锁。如果不释放锁,就会产生死锁。
4.Synchronized 当线程想要获取锁的时候,发现锁被占用了,那么线程就会被挂起,进入阻塞状态直到这个锁可以用了,也就是说没有超时机制(tryLock)或者中断处理机制(lockInterruptibly)。Lock接口提供了更多的选择,除了能阻塞地获取锁以外,还有tryLock()方法提供非阻塞获取锁的能力,"Boolean tryLock(long time,TimeUnit unit)"提供带超时的获取锁机制,以及lockInterruptibly()提供对中断的响应能力,如果一个线程等待一个锁的过程中(锁被占有),线程中断了,那么就抛出一个异常InterruptedException。
5.Synchronized 不可以中断的,非公平的。 Lock,可以判断锁,可以自己设置公平还是不公平。
锁是什么,如何判断锁的是谁?
生产者和消费者问题:
注意:经常考察的问题都有,单例模式,排序算法,生产者消费者模式,死锁问题。
public class A { public static void main(String[] args) { Data data = new Data(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); } } class Data{ //数字 资源类 private int number = 0; //+1 public synchronized void increment() throws InterruptedException { if(number!=0){ //等待 this.wait(); } number++; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他线程,+1完毕了。 this.notifyAll(); } //-1 public synchronized void decrement() throws InterruptedException { if(number==0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他线程,-1完毕了。 this.notifyAll(); } }
这种是正常的生产者消费者,但是如果面试官让加多个线程,那么就有可能出现虚假唤醒的情况,为了保证虚假唤醒的情况下依然是安全的,那么就需要将if改为while。如下:
public class A { public static void main(String[] args) { Data data = new Data(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D").start(); } } class Data{ //数字 资源类 private int number = 0; //+1 public synchronized void increment() throws InterruptedException { while(number!=0){ //等待 this.wait(); } number++; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他线程,+1完毕了。 this.notifyAll(); } //-1 public synchronized void decrement() throws InterruptedException { while(number==0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他线程,-1完毕了。 this.notifyAll(); } }
虚假唤醒是什么意思呢,比如说,现在num=0,生产者C线程就打算开始生产了,然后刚生产了一个数字,现在num=1了,然后唤醒了所有的线程,然后生产者A线程被唤醒了,并且获取到了锁,然后从上次中断的地方继续执行代码,发现if里面的wait语句已经执行结束了,所以下面就会继续执行剩下的代码,但是其实,生产者A是不符合条件的不应该执行代码的。但是依然执行了,所以又生产了一个数字,于是现在num=2了,这就是错的。即使发生虚假唤醒,也要保证它不会出问题。
用Lock代替synchronized:
用lock代替synchronized,await代替wait,signal代替notify。
public class B { public static void main(String[] args) { Data2 data = new Data2(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D").start(); } } class Data2{ //数字 资源类 private int number = 0; Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); //+1 public void increment() throws InterruptedException { lock.lock(); try{ while(number!=0){ //等待 condition.await(); } number++; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他线程,+1完毕了。 condition.signalAll(); }catch(Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } //-1 public void decrement() throws InterruptedException { lock.lock(); try{ while(number==0){ //等待 condition.await(); } number--; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他线程,-1完毕了。 condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } }
换成了Lock形式的了。
怎么能按顺序输出我们想要的内容呢?就像生产线一样。
public class C { public static void main(String[] args) { Data3 data3 = new Data3(); new Thread(()->{ for(int i=0;i<10;i++){ data3.printA(); } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ data3.printB(); } },"B").start(); new Thread(()->{ for(int i=0;i<10;i++){ data3.printC(); } },"C").start(); } } class Data3{ Lock lock = new ReentrantLock(); int number = 1; //1A 2B 3C //精准唤醒 Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); Condition condition3 = lock.newCondition(); public void printA(){ lock.lock(); try{ while(number!=1){ condition1.await(); } System.out.println(Thread.currentThread().getName()+"===AAA"); number=2; condition2.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void printB(){ lock.lock(); try{ while(number!=2){ condition2.await(); } System.out.println(Thread.currentThread().getName()+"===BBB"); number = 3; condition3.signal(); }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } } public void printC(){ lock.lock(); try{ while(number!=3){ condition3.await(); } System.out.println(Thread.currentThread().getName()+"===CCC"); number = 1; condition1.signal(); }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } } }
关于锁的8个问题:就是面试题。回看
List不安全,得用CopyOnWriteArrayList才安全。
package unsafe; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; public class ListTest { //java.util.ConcurrentModificationException 并发修改异常!! public static void main(String[] args) { //并发下,Arraylist是不安全的。 /* * 解决方案: 1. 换成Vector * 2. Collections.synchronizedList(); * 3. 这个才是应该回答的 CopyOnWriteArrayList*/ /*List list = new ArrayList<>(); for(int i=0;i<100;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); }*/ /*List list = new Vector<>(); for(int i=0;i<100;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); }*/ /*List list = Collections.synchronizedList(new ArrayList<>()); for(int i=0;i<100;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); }*/ //CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略。 //多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)。 //在写入的时候避免覆盖,造成数据问题。 //读写分离。 //CopyOnWriteArrayList 比Vector 牛在哪里? //在写入的时候复制一份。 Vector的底层用的是Synchronized关键字,而CopyOnWriteArrayList底层用的是 //Lock锁。而且在向容器里面赋值的时候,用的是先创造一个容器,然后确定好长度之后,将之前的元素复制进去。 List list = new CopyOnWriteArrayList<>(); for(int i=0;i<100;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } } }
Set也不安全,得用CopyOnWriteArraySet才行。
package unsafe; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArraySet; public class SetTest { public static void main(String[] args) { //Setset = new HashSet<>(); //Set set = Collections.synchronizedSet(new HashSet<>()); Set set = new CopyOnWriteArraySet<>(); for(int i=0;i<100;i++){ new Thread(()->{ set.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(set); },String.valueOf(i)).start(); } } }
Map也不安全,得用ConcurrentHashMap才行。
package unsafe;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MapTest {
public static void main(String[] args) {
//map是这样用的吗?默认等价于什么?
//加载因子0.75,初始化容量16。
//Map
//Map
//Map
Map
for(int i=0;i<30;i++){
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
线程的创建方式:
1.可以有返回值。
2.可以抛出异常。
3.方法不同。run(),call()。
package callable; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread myThread = new MyThread(); FutureTask futureTask = new FutureTask(myThread); Thread thread = new Thread(futureTask); Thread thread1 = new Thread(futureTask); thread.start(); thread1.start(); //get可能会产生阻塞。 String str = (String)futureTask.get(); System.out.println(str); } } class MyThread implements Callable{ @Override public String call() throws Exception { System.out.println("----------------"); return "990921"; } }
这里面有个问题就是为什么不是执行两边这个Callable里面的输出语句呢?
创建FutureTask对象,然后调用run方法来执行任务。但是这里面有一个状态码是State,来表示任务的执行状态,如果State是New的话,那么就是直接执行Callable中的call()方法并且存储起来方法的返回值,但是执行完这个方法之后,State就会由COMPLETING(完成中)转换到NORMAL(正常结束)。然后下次再调用run方法的时候,State状态就不是New了,就不会再运行Callable的call()方法了。
CountDownLatch:
package add;
import java.util.concurrent.CountDownLatch;
//计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//总数是6
CountDownLatch countDownLatch = new CountDownLatch(6);
for(int i=0;i<6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Go out");;//数量-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零,然后再向下执行。
System.out.println("close door");
}
}
CyclicBarrier
package add; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{ System.out.println("计数器执行完毕了"); }); for(int i=0;i<7;i++){ final int temp = i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"计数了已经,现在是第"+temp+"个!"); try { //阻塞住 cyclicBarrier.await(); System.out.println("完成了"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }
Semaphore
package add; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); for(int i=0;i<8;i++){ new Thread(()->{ //拿到资源 try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"拿到了"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } finally{ //释放了资源 semaphore.release(); System.out.println(Thread.currentThread().getName()+"释放了"); } },String.valueOf(i)).start(); } } }
package rw; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * ReadWriteLock */ public class ReadWriteLockDemo { public static void main(String[] args) { MyCacheLock myCacheLock = new MyCacheLock(); //写入操作 for(int i=0;i<5;i++){ final int temp = i; new Thread(()->{ myCacheLock.put(temp+"",temp+""); },String.valueOf(i)).start(); } //读取操作 for(int i=0;i<5;i++){ final int temp = i; new Thread(()->{ myCacheLock.get(temp+""); },String.valueOf(i)).start(); } } } //自定义一个缓存 class MyCache{ private volatile Mapmap = new HashMap<>(); //存,写 public void put(String key,Object value){ System.out.println(Thread.currentThread().getName()+"写入"+key); map.put(key,value); System.out.println(Thread.currentThread().getName()+"写入ok"); } //取,读 public void get(String key){ System.out.println(Thread.currentThread().getName()+"读取"+key); Object o = map.get(key); System.out.println(Thread.currentThread().getName()+"读取ok"); } } class MyCacheLock{ private volatile Map map = new HashMap<>(); //读写锁,更加细粒度的控制 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //存,写 写入的时候只希望同时只有一个线程写 public void put(String key,Object value){ readWriteLock.writeLock().lock(); try{ System.out.println(Thread.currentThread().getName()+"写入"+key); map.put(key,value); System.out.println(Thread.currentThread().getName()+"写入ok"); }catch (Exception e){ e.printStackTrace(); }finally{ readWriteLock.writeLock().unlock(); } } //取,读 所有人都可以去读我写入的内容 public void get(String key){ readWriteLock.readLock().lock(); try{ System.out.println(Thread.currentThread().getName()+"读取"+key); Object o = map.get(key); System.out.println(Thread.currentThread().getName()+"读取ok"); }catch(Exception e){ e.printStackTrace(); }finally{ readWriteLock.readLock().unlock(); } } }
说白了,写的时候不能读,读的时候不能写。但是可以多个线程一起读,不可以多个线程一起写。
在Collection这个接口下面,有List,Set,还有Queue接口
BlockingQueue(阻塞队列)继承了Queue接口。
已知实现类:
ArrayBlockingQueue LinkedBlockingQueue SynchronousQueue(同步队列)
ArrayBlockingQueue:
基于数组实现的阻塞队列,内部使用的是单一的公平的重入锁(ReentrantLock)和两个条件(notEmpty和notFull)来控制对内部数组的并发访问。当阻塞队列已经满了的时候,那么插入操作的线程就会被阻塞并且进入notFull的等待集。当队列不满的时候,就会唤醒这个等待集中的线程。同样的,当阻塞队列为空的时候,那么移除操作的线程就会受到阻塞并且进入notEmpty的等待集。当队列不为空的时候,就会唤醒这个等待集中的线程。
LinkedBlockingQueue:
基于链表实现的阻塞队列,内部使用两个锁,分别是putLock管理插入操作,另一个是takeLock管理移除操作。这种锁分离开来的设计可以让并发的性能更好,因为可以做到读写分离。同时也是有两个条件(notFull和notEmpty)来管理。理论上来讲,LinkedBlockingQueue没有一个规定的上限,因为底层是链表,所以不需要连续的内存空间,可以在一定程度上降低内存的碎片化。但是还有一个问题就是,链表中的每个节点都需要有一个next指针,来存储下一个节点的位置,那么这一部分其实也是占据了比较多的内存,相对于ArrayBlockingQueue来说。因为ArrayBlockingQueue在创建的时候,大小就已经确定了,有一个属性是Integer.MAX_VALUE确定了大小,并且不会再改变的。所以如果对内存要严格的限制要求的话,使用ArrayBlockingQueue更好。但是底层是链表的话,增删的效率会更高。
SynchronousQueue:也叫做同步队列
这个队列比较特殊,因为容量为0。在SynchronousQueue中,生产者线程和消费者线程会直接进行数据交换。当一个线程试图向队列里面插入一个元素的时候,会被阻塞,直到有另一个线程从队列中取出这个元素。当一个线程视图向队列里面取出一个元素的时候,会被阻塞,直到有另一个线程向队列中插入这个元素。
为什么说容量是0呢,因为实际上元素没有被存储到synchronousQueue当中,直接在线程之间进行的传递。
在需要进行线程间直接交换的场景下非常有用。可以被用来直接在工作线程之间传递任务。
public class TestSynchronousQueue { public static void main(String[] args) { BlockingQueue synchronousQueue = new SynchronousQueue(); new Thread(()->{ try { synchronousQueue.put("1"); System.out.println("1放进去了"); synchronousQueue.put("2"); System.out.println("2放进去了"); synchronousQueue.put("3"); System.out.println("3放进去了"); } catch (Exception e) { e.printStackTrace(); } },"线程1").start(); new Thread(()->{ try { synchronousQueue.take(); System.out.println("取出来1了"); synchronousQueue.take(); System.out.println("取出来2了"); synchronousQueue.take(); System.out.println("取出来3了"); } catch (Exception e) { e.printStackTrace(); } },"线程2").start(); } }
什么时候会使用阻塞队列呢?
1.多线程并发
2.线程池
3.解决生产者消费者问题
Deque接口
Deque继承了Queue接口,代表了一个双端队列,即可以从两端(队头,队尾)添加和移除。使得Deque即可以作为队列使用(FIFO,先进先出),也可以作为栈使用(LIFO,后进先出)。Deque的实现有ArrayDeque,LinkedList,LinkedBlockingDeque,AbstractQueue(抽象类)等。
四组API:
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
添加 | add | offer | put | offer(,时间数字,TimeUnit的类型) |
移除 | remove | poll | take | poll(时间数字,TimeUnit的类型) |
判断队首元素 | element | peek |
池化技术是一种优化策略。说白了就是重新使用已经创建的对象,而不是为每个请求都创建对象。因为创建对象也是有成本和开销的,池化技术可以提高应用程序的性能和效率。
线程池的好处:
1.降低系统的消耗 2.提高效率 3.使程序运行的开销变小。
真正用处是:重复使用并且管理线程,控制最大并发数。
代码实例:
package pool; import java.util.concurrent.*; //使用了线程池之后,要使用线程池来创建线程 public class Demo1 { public static void main(String[] args) { //ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程 //ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小 //ExecutorService threadPool = Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱 ExecutorService threadPool = new ThreadPoolExecutor( 2,5, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), //new ThreadPoolExecutor.AbortPolicy()//阻塞队列满了,但是如果再进入,那就不处理并报错 //new ThreadPoolExecutor.CallerRunsPolicy() //哪儿来的去哪里,说白了就是交给了main线程 //new ThreadPoolExecutor.DiscardPolicy() //队列满了,就丢掉任务,也不抛出异常。只输出了8个。 new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,就尝试和最早的竞争。 //如果竞争成功就执行,如果竞争失败就输出8个,等于是没了。但是就是不会抛出异常。 ); try { for(int i=0;i<9;i++){ threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+"=====ok"); }); } }catch (Exception e){ e.printStackTrace(); }finally{ //线程池用完,程序结束,关闭线程池 threadPool.shutdown(); } } }
首先关于线程池,创建的方式是,new ThreadPoolExecutor,返回的是一个ExecutorService的类,我们可以给他命名为threadPool。在创建的时候需要使用7个参数,分别介绍如下:
1.corePoolSize : 这个就是核心的先干活的”先遣部队“。
2.MaximumPoolSize:最多的干活的人,这里面的最多能够干活的线程的数量。
3.keepAliveTime:这个参数的意思就是说,除了先遣部队的那几个线程的其他线程,他们是可有可无的,当然这个取决于我们要处理的任务量,如果在”keepAliveTime“这个时间里面,这些线程都没有用到的话,那么这些超过corePoolSize的线程就会被终止,这样可以避免线程消耗系统资源。
4.unit:这个参数是keepAliveTime这个时间的单位。
5.workQueue:这是一个工作队列,用于存放等待的请求任务。类型可以是ArrayBlockingQueue,LinkedBlockingQueue 和 SynchronousQueue这三种。
6.threadFactory:线程工厂,用于创建新的线程并且确定属性。就一般默认使用Executors.defaultThreadFactory()。
7.RejectedExecutionHandler:非常重要的参数,这个是拒绝策略,也就是说当任务队列也满了的时候,那么还有请求的任务,这个参数就决定了如何去处理那些在任务队列之外的任务。
一共是有4种策略,分别是:
ThreadPoolExecutor.AbortPolicy() 任务队列满了,但是如果再有任务请求,那就不处理并抛异常。
ThreadPoolExecutor.CallerRunsPolicy() 任务队列满了,但是如果再有任务请求,那就让他哪来的回哪去,一般来说就是交给main线程去处理。
ThreadPoolExecutor.DiscardPolicy() 任务队列满了,但是如果再有任务请求,就不处理直接丢掉。
ThreadPoolExecutor.DiscardOldestPolicy() 任务队列满了,但是如果再有任务请求,就让他去跟最早的任务去抢夺线程处理,如果抢到了那就处理,如果抢不到那就也直接丢掉了。
线程池的两种创建方式,一种是ThreadPoolExecutor的构造方法,一种是Executors的静态方法,他们的区别是什么,如何选择?
相比较来说ThreadPoolExecutor这种构造方法创建线程池更加的灵活并且安全,Executors的方法下容易引起OOM,FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE(21亿),可能会堆积大量请求,导致OOM。CacheThreadPool和ScheduledThreadPool允许创建线程的数量为Integer.MAX_VALUE(21亿),可能会创建大量的线程,导致OOM。
那么使用ThreadPoolExecutor的构造方法,里面的MaximumPoolSize又该如何选择呢?
1.CPU密集型 2.IO密集型 这是一个调优的考点。
首先插播一下,检查电脑是几核的代码如下:
System.out.println(Runtime.getRuntime().availableProcessors());
CPU密集型的就直接MaximumPoolSize设置为 Runtime.getRuntime().availableProcessors()就行。IO密集型的 就直接判断程序中十分耗IO的线程数量,只要大于这个就行了,比如是这个数量的两倍。
四大函数式接口分别是:
function 函数型接口:有输入参数,有返回值。
predicate 断定型接口:有输入参数,返回值只能返回boolean类型。
consumer 消费型接口:有输入参数,没有返回值。
supplier 供给型接口:没有输入参数,只有返回值。
这四个函数式接口都可以被lambda表达式改写,并且非常有效率,简化了代码。具体实例如下:
function 函数型接口:
public class Demo01 {
public static void main(String[] args) {
//工具类,输出输入的值
/*Function function = new Function(){
@Override
public String apply(String o) {
return o;
}
};*/
//lambda表达式
Function function = (str)->{return str;};
System.out.println(function.apply("asd"));
}
}
predicate 断定型接口:
public class Demo02 { public static void main(String[] args) { /*Predicatepredicate = new Predicate (){ @Override public boolean test(String s) { if(s!=null){ return true; } return false; } };*/ Predicate predicate = (str)->{ if(str!=null){ return true; } return false; }; System.out.println(predicate.test(null)); } }
consumer 消费型接口:
public class Demo03 { public static void main(String[] args) { /*Consumer consumer = new Consumer(){ @Override public void accept(String s) { System.out.println("Consumer消费型接口真的没有返回值啊!"); System.out.println("输入的值是"+s); } }; consumer.accept("sad");*/ Consumer consumer = (s)->{ System.out.println("Consumer消费型接口真的没有返回值啊!"); System.out.println("输入的值是"+s); }; consumer.accept("sad"); } }
supplier 供给型接口:
public class Demo04 { public static void main(String[] args) { /*Supplier supplier = new Supplier() { @Override public Integer get() { System.out.println("supplier接口真的是只能有返回值,没有参数"); return 1024; }; }; System.out.println(supplier.get());*/ Supplier supplier = ()->{ System.out.println("supplier接口真的是只能有返回值,没有参数"); return 1024; }; System.out.println(supplier.get()); } }
计算就应该交给流来操作。
/** * 1.ID是偶数 * 2.年龄必须大于23岁 * 3.用户名转为大写字母 * 4.用户名字母倒着排序 * 5.只输出一个用户 */ 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(6,"e",25); //集合用来存储 Listusers = Arrays.asList(u1, u2, u3, u4, u5); //计算操作让流来完成 //filter底层是Predicate 断定型接口,有输入参数,但是返回值只能是boolean类型 //forEach底层是Consumer 消费型接口,里面有输入参数,但是没有返回值。 //map底层是Function 函数型接口, 里面有输入参数,也有返回值。 users.stream(). filter(u->{return u.getId()%2==0;}). filter(u->{return u.getAge()>23;}). map(u->{return u.getName().toUpperCase();}). sorted((uu1,uu2)->{return uu2.compareTo(uu1);}). limit(1). forEach(System.out::println); } }
public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { /*//发起一个请求 //没有返回值的 runAsync 异步回调 CompletableFuturecompletableFuture = CompletableFuture.runAsync(()->{ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"runAsync=>Void"); }); System.out.println("1111"); completableFuture.get();*/ //有返回值的 supplyAsync 异步回调 CompletableFuture completableFuture = CompletableFuture.supplyAsync(()->{ System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer"); int i = 10/0; return 1024; }); System.out.println(completableFuture.whenComplete((t, u) -> { System.out.println(t + "...." + u); }).exceptionally((e) -> { e.printStackTrace(); return 233; }).get()); } }
在这里面,当main线程向下执行的时候,首先睡两秒,这个操作就直接交给了别的单独的线程或线程池去执行了。main线程就继续向下执行输出语句”1111“。然后其他线程就去执行睡眠两秒的这个操作,但是到了completableFuture.get()方法的时候主线程会阻塞,等待另一个线程的完成。
CompletableFuture是一个实现了Future和CompletionStage的类,他的get()方法是一个阻塞方法,代表他会等待异步计算的完成并且返回异步计算的结果。
什么是JMM?
java内存模型,是一个概念。
关于JMM的一些同步的约定:
这里面store和write写反了。
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的。
lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便后面的load操作使用。
load(载入):作用于工作内存的变量,把read操作从主存中的变量放到工作内存中。
use(使用):作用于工作内存中的变量,把工作内存中的变量传输给执行引擎。
assign(赋值):作用于工作内存中的变量,把执行引擎中的值放入到工作内存中的变量中。
store(存储):作用于主内存中的变量,把工作内存中的变量传入到主内存当中。
write(写入):将store操作的变量的值放入到主内存中。
下面举例子:
程序不知道主内存的值已经被修改过了。
public class demo1 { private static int num = 0; public static void main(String[] args) { new Thread(()->{ while(num==0){ } }).start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } num=1; System.out.println(num); } }
请你谈谈对于volatile的理解:
这个关键字是一个Java虚拟机提供的轻量级的同步机制,类似于synchronized。
1.保证可见性
public class demo1 { //这种情况下就是无法可见的,对于主内存中的变量来说。 //private static int num = 0; //这种情况下就是可以被看见的,加了volatile关键字之后。 private volatile static int num = 0; public static void main(String[] args) { new Thread(()->{ while(num==0){ } }).start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } num=1; System.out.println(num); } }
2.不保证原子性
java.util.concurrent.atomic
原子性:不可分割
volatile是不保证原子性的,这里面如果想保证原子性的话,那就需要使用synchronized关键字或者lock锁的方法,但是也可以使用AtomicInteger的方法,底层是CAS原理,效率更高。
public class demo2 {
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
//AtomicInteger + 1方法,CAS效率超级高,并且可以保证原子性。
num.getAndIncrement();
}
public static void main(String[] args) {
for(int i=0;i<20;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
add();
}
}).start();
}
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
Unsafe类是一个很特殊的存在。
3.禁止指令重排
什么是指令重排?
由于程序在执行的过程中,不一定是按照我们想要的顺序执行的,所以有的时候可能由于执行顺序的变化造成了结果的变化,这就叫做指令重排。
但是如果加了volatile就可以有效的避免指令重排。
因为有内存屏障,CPU指令。
内存屏障:禁止上面指令和下面指令顺序交换。
volatile作用:
1.保证特定的操作的执行顺序。
2.可以保证某些变量的内存可见性。
缺点:无法保证原子性。
什么是单例模式:
单例模式是确保一个类只有一个实例存在,并提供一个全局访问点。
为什么需要单例模式:
有的时候对象的创建是比较消耗资源的,这个时候我们可以尝试使用单例模式,比如说读取配置,读取资源文件等的情况下。还有的时候就是只要求这个类的实例只能有一个,比如说操作系统只能有一个,应用程序也只能启动一个。
接下来举例几种单例模式的实现方法,但是每一种都有自己的缺点和优点:
饿汉式:
public class Hungry { //私有化构造器 private Hungry(){ } //在这里就创建出来了一个常量Hungry private final static Hungry HUNGRY = new Hungry(); //调用这个方法就直接调用返回常量。每次都是返回上面的常量。 public static Hungry getInstance(){ return HUNGRY; } }
优点:
private final static Hungry HUNGRY = new Hungry();这行代码保证了饿汉式在类加载的时候就进行了实例化,所以不需要担心多线程同步的安全问题。这里前面使用了static关键字进行声明的时候对对象进行实例化,就保证了这个实例化操作只会被执行一次,这是由Java虚拟机的类加载机制所保证的。所以多个线程操作,只能有一个线程能够执行初始化操作。这就解决了多线程环境下的线程安全问题。
缺点:
1. 不管是否需要这个对象,我这个类只要是加载了,那么这个对象就产生了,就被实例化了,那么如果这个实例化的过程开销是比较大的,但是我们又没有使用这个对象,那么这样就亏了。
2.如果使用反射机制调用私有构造器或者使用反序列化构造多个对象,那么这个单例模式就被破解了。
懒汉式+双重检查(double check)+在构造器中添加防止多次实例化:
public class Lazy { //下面会解释加volatile关键字的原因 private volatile static Lazy lazy; //私有构造器 private Lazy(){ if ( lazy != null) { throw new RuntimeException("Cannot construct instance twice"); } } public static Lazy getInstance(){ if(lazy==null){ synchronized(Lazy.class){ if(lazy==null){ lazy = new Lazy(); } } } return lazy; } }
解释为什么这种双重检测会更好:
因为如果没有双重检测,就给整个方法加一个synchronized的话,这样就有一个问题就是,每次在判断lazy是否为null的时候都进入了这个synchronized方法,这样效率比较低。在这种双重检测情况下,如果已经存在了lazy,那么就会被拦在第一个if前面了,就不会进入到这个synchronized语句块了,这样代码的效率更高了。
解释为什么需要前面有volatile关键字来修饰Lazy类:
1.加了volatile关键字之后,被volatile关键字修饰的Lazy变量,那么如果Lazy的值有了变化的话,其他的线程也都会知道的,因为这利用了volatile的可见性,一旦主内存中lazy的值变化了,各个线程中的工作内存中的lazy副本的值也会跟着变化的。
2.加了volatile关键字之后,能够使得lazy = new Lazy();的过程禁止指令重排。因为这个虽然是一行代码,但是其实这个不是一个原子性操作。这行代码可以分为三步:一,为对象分配内存空间 二,初始化对象 三,将初始化的对象指向这个分配的内存空间。如果没有禁止指令重排,那么有可能先执行第一步,然后是第三步,其次才是第二步。那么在多线程的环境下,执行完第一步和第三步的时候对象就已经是非空的了,但是并没有被初始化。有可能这个没有被初始化的对象会被其他线程拿到,这个时候还没有执行第二步,那么现在返回的这个对象就是有问题的。
优点:
1.double check的方法实现了,只有第一次实例化的时候需要进入到synchronized语句块中,一旦存在对象,就不需要进入了,非常高效。
2.synchronized和volatile使得在多线程环境下也可以安全。
3.改变了私有构造方法,也防止了通过反射机制强行调用私有构造方法和反序列化的方式创造多个对象来破解单例模式。
缺点:
唯一缺点可能也就是代码比较繁琐。
静态内部类:
//静态内部类 比饿汉式来说改进了很多很多 public class Singleton { //一个私有构造器 private Singleton(){ } private static class InnerClass{ private static final Singleton SINGLETON = new Singleton(); } private static Singleton getInstance(){ return InnerClass.SINGLETON; } }
静态内部类实际上也是使用的Java虚拟机的类加载机制来实现的单例模式,但是这个和饿汉式的不同点在于,饿汉式每次都要在类加载的时候实例化一个对象出来,不管需要不需要。但是静态内部类里面,只有当我们有需要的时候,才会通过调用getInstance方法来实例化对象。即使Singleton类加载了也不代表InnerClass会实现类加载,只有调用getInstance方法的时候,才会将InnerClass类加载。
优点:效率高,实现了懒加载。
缺点:还是可以通过反射机制强行调用私有构造方法和反序列化的方式创造多个对象来破解单例模式。
枚举实现单例模式(先去把枚举全部学一遍,很重要):
public enum SingletonEnum { INSTANCE; public void doSomething(){ } }
优点:
线程安全:由于枚举类型本身的特性,它保证了在任何情况下都是单例的。
反射安全:即使是在高并发、多线程的环境下,通过Java反射也无法创建新的实例,保证了单例模式的一致性。
序列化安全:对于一个实现了序列化接口的单例类,如果不做特殊处理,每次反序列化都会得到一个新的实例,破坏了单例模式。而枚举类型的单例模式默认就是序列化安全的。
缺点:
没有延迟加载:枚举类型的单例模式会在类加载的时候就创建实例,这样可能会导致资源的浪费,特别是如果这个单例的创建过程非常耗时或者耗资源,而长时间不使用,就可能造成浪费。
不能继承其他类:由于Java不支持枚举类型继承其他类(枚举类型已经默认继承了Enum类,并且Java是单继承),因此如果你的单例类需要继承其他类,那么枚举类型的单例模式就不能使用。
CAS是解决并发问题的一种操作,一种算法。CAS操作包括三个操作数:内存位置(V),预期原值(A)和新值(B)。如果内存位置的当前值与预期原值相匹配,是一样的,那么就将内存位置的当前值改为新值,如果不一样,那么就不执行此操作。这个过程是一个原子过程,因为是在一个事务中完成的。
cas在java中的实操:
AtomicInteger类中有一个方法compareAndSet,使用的就是cas操作。
如果解决ABA问题:
利用原子引用:带版本号的。
public class CASDemo2 { public static void main(String[] args) { //int Integer AtomicStampedReference atomicInteger = new AtomicStampedReference(1,1); new Thread(()->{ int stamp = atomicInteger.getStamp(); //获得版本号 System.out.println("a1=>"+stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //乐观锁执行完之后都会将Version+1 System.out.println(atomicInteger.compareAndSet(1,2, atomicInteger.getStamp(),atomicInteger.getStamp()+1)); System.out.println("a2=>"+atomicInteger.getStamp()); System.out.println(atomicInteger.compareAndSet(2,1, atomicInteger.getStamp(),atomicInteger.getStamp()+1)); System.out.println("a3=>"+atomicInteger.getStamp()); },"a").start(); new Thread(()->{ int stamp = atomicInteger.getStamp(); //获得版本号 System.out.println("b1=>"+stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //乐观锁执行完之后都会将Version+1 System.out.println(atomicInteger.compareAndSet(1,6, stamp,stamp+1)); System.out.println("b2=>"+atomicInteger.getStamp()); },"b").start(); } }
公平锁:非常公平,不能够插队。先来后到。
非公平锁:非常不公平,可以插队。
Lock lock = new ReentrantLock(True);//默认都是非公平锁,所以设置为true之后就是公平锁了。
可重入锁(递归锁):
我在拿到了这把锁的同时,又拿到了这把锁,也就是说,我一个线程可以多次重复地获取同一把锁,这就叫做可重入锁。
自旋锁:
自旋锁的工作方式是,在获取锁的时候,如果锁已经被占用,那么就一直在循环中检查锁是否被释放。
public class SpinLock { AtomicReferenceatomicReference = new AtomicReference<>(); public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"==>mylock"); //自旋锁 while(!atomicReference.compareAndSet(null,thread)){ } } public void myUnLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"===>myUnLock"); atomicReference.compareAndSet(thread,null); } }
public class SpinLockTest { public static void main(String[] args) throws InterruptedException { SpinLock spinLock = new SpinLock(); new Thread(()->{ spinLock.myLock(); try{ TimeUnit.SECONDS.sleep(10); }catch(Exception e){ e.printStackTrace(); }finally { spinLock.myUnLock(); } },"T1").start(); TimeUnit.SECONDS.sleep(5); new Thread(()->{ spinLock.myLock(); try{ TimeUnit.SECONDS.sleep(2); }catch(Exception e){ e.printStackTrace(); }finally{ spinLock.myUnLock(); } },"T2").start(); } }
这就是死锁:
public class DeadLockTest {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
new Thread(()->{
synchronized (o1){
System.out.println("o1被T1线程锁上了");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("o2被T1线程锁上了");
}
}
},"T1").start();
new Thread(()->{
synchronized (o2){
System.out.println("o2被T2线程锁上了");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("o1被T2线程锁上了");
}
}
},"T2").start();
}
}
使用jps定位进程号,在terminate中输入,jps -l 命令。
使用jstack进程号来找到死锁问题,jstack 14230。能够得到很多具体的信息,如果里面有 found deadlock这种情况。
排查问题在面试中就回答:
1.查看日志
2.查看堆栈信息
AbstractQueuedSynchronizer:他是维护了一个先进先出的队列。这个队列里面存储的是获取锁失败的线程,获取锁失败的线程就会进入到AQS的等待队列中。AQS的等待队列中会按照两种方式维护队列,一种是非公平的,一种是公平的。非公平就代表后面排队的线程可以以插队的方式先获取锁,公平的就是按照等待时间来获取锁,等待时间越长的先获取锁,但是这样容易造成线程饥饿。
也可以通过状态变量支持可重入锁的实现,就是说一个线程可以多次获取锁。