JUC多线程及并发包

目录

一、谈谈你对volatile的理解

1、volatile是java虚拟机提供的轻量级同步机制

2、JMM你谈谈

(1)可见性

(2)原子性

(3)有序性

(4)禁止指令重排小结

3、你在哪些地方用到过volatile——单例模式DCL

二、CAS你知道吗

1、比较并交换

2、CAS底层原理?对UnSafe的理解

3、CAS缺点

三、原子类AutomaticInteger的ABA问题?原子更新引用?

1、ABA问题的产生

2、AutomaticReference原子引用——不能解决ABA问题

3、AutomaticStampedReference时间戳原子引用——解决ABA问题

四、ArrayList是非线程安全的,给出例子和解决方案

1、ArrayList线程不安全的例子

2、ArrayList线程不安全解决方案

3、集合类之不安全set

4、集合类之不安全Map

五、公平锁/非公平锁/可重入锁(递归锁)/自旋锁~谈谈理解

1、公平锁和非公平锁

2、可重入锁(又名递归锁)

3、自旋锁

4、独占锁(写锁)、共享锁(读锁)、互斥锁

六、CountDownLatch、CyclicBarrier、Semaphore使用过吗

1、CountDownLatch

2、CyclicBarrier

3、Semaphore

七、阻塞队列

1、队列和阻塞队列

2、为什么用?有什么好处?

3、BlockingQueue的核心方法

4、BlockingQueue架构梳理+种类分析

5、用在哪里

6、Synchronize和Lock有什么区别?

7、用新的Lock有什么好处【ReentrantLock】?

8、实现线程的四种方式

八、线程池用过吗?ThreadPoolExecutor谈谈你的理解

1、为什么使用线程池,优势?

2、线程池如何使用

3、线程池7大重要参数介绍

4、线程池的底层工作原理

5、线程池的使用距离说明底层原理——银行网点

九、线程池如何合理设置参数

1、线程池的四种拒绝策略

2、*你在工作中单一的/固定数的/可变的*三种创建线程池的方法,你用哪个多?超级大坑

3、你在工作中是如何创建线程池的——线程池的手写改造和拒绝策略

4、合理配置线程池参数

十、死锁编码及定位分析

1、什么是死锁?

2、死锁的例子

3、如何解决死锁?


一、谈谈你对volatile的理解

1、volatile是java虚拟机提供的轻量级同步机制

(1)保证可见性

(2)不保证原子性

(3)禁止指令指令重排

2、JMM你谈谈

JMM关于同步规定:
A.线程解锁前,必须把共享变量的值刷新回主内存
B.线程加锁前,必须读取主内存的最新值到自己的工作内存
C.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

(1)可见性

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的。这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享变量X对线程B来说并不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.

举例:验证volatile的可见性

public class VolatileDemo {
	public static void main(String[] args) {
		MyData data = new MyData();//资源类
		//第一个线程
		new Thread(()->{
			System.out.println(Thread.currentThread().getName()+"\t come in");
			//暂停一会线程
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			data.addTo60();
			System.out.println(Thread.currentThread().getName()+"\t udate num value:"+data.num);
		},"AAA").start();
		//第二个线程就是main线程
		while (data.num == 0) {
			//main线程就一直在这里等待,直到num值不再为0
		}
		System.out.println(Thread.currentThread().getName()+"\t mission is over");
	}
}
class MyData{
	volatile int num = 0;
	public void addTo60() {
		this.num = 60;
	}
}

没有加volatile时:线程AAA执行完,但main线程判断条件一直为真,出不来【AAA线程修改后没人通知主线程】

加volatile时:线程AAA执行完,可正常执行完最后一条语句输出

(2)原子性

i++在多线程下是非线程安全的:i++指令被拆分成了3个指令

A、执行getfield拿到原始i

B、执行iadd进行加1操作

C、执行putfield把类加厚的值写回

举例:说明volatile不能保证原子性

public class VolatileDemo2 {
	public static void main(String[] args) {
		MyData2 myData = new MyData2();
		for (int i = 1; i <= 20; i++) {//创建20个线程
			new Thread(()->{
				for (int j = 1; j <= 1000; j++) {//每个线程执行1000次
					myData.addPlusPlus();
				}
			},String.valueOf(i)).start();;
		}
		//等待上述20个线程执行完成后看最终结果:如果是20000就正确
		while (Thread.activeCount() > 2) {//后台有main线程和gc线程
			Thread.yield();//礼让线程
		}
		System.out.println(Thread.currentThread().getName()+"\t finally. num is:"+myData.num);
	}
}
class MyData2{
	volatile int num = 0;
	public void addPlusPlus() {
		num++;
	}
}

没有加volatile的结果:不是20000

加了volatile的结果:不是20000

那么如何解决原子性呢?

方式一:使用synchronize关键字

方式二:使用AutomaticInteger

结果:是20000

(3)有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种:

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.
处理器在进行重新排序是必须要考虑指令之间的数据依赖性
 
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

举例:指令重排

分析:单线程情况下,先执行语句1,再执行语句2,然后执行语句3,输出a为6;但如果是多线程环境下,会有指令重排,如语句2先执行,语句3再执行,输出a=5,语句1再执行…………

解决方法:在a 和 flag前面加上volatile关键字避免指令重排

(4)禁止指令重排小结

对volatile变量进行写操作时,会在写操作后加一条store屏障指令,将工作内存中的共享变量值刷新回主内存

对volatile变量进行读操作时,会在读操作前加一条load屏障指令,从内存中读取共享变量

3、你在哪些地方用到过volatile——单例模式DCL

public class SingletonDemo {

	 private static volatile SingletonDemo instance=null;
	    private SingletonDemo(){
	        System.out.println(Thread.currentThread().getName()+"\t 构造方法");
	    }

	    /**
	     * 双重检测机制
	     * @return
	     */
	    public static SingletonDemo getInstance(){
	        if(instance==null){
	            synchronized (SingletonDemo.class){
	                if(instance==null){
	                    instance=new SingletonDemo();
	                }
	            }
	        }
	        return instance;
	    }

	    public static void main(String[] args) {
	        for (int i = 1; i <=10; i++) {
	            new Thread(() ->{
	                SingletonDemo.getInstance();
	            },String.valueOf(i)).start();
	        }
	    }

}

DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
  原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
instance=new SingletonDem(); 可以分为以下步骤(伪代码)
 
memory=allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 
 
步骤2和步骤3不存在数据依赖关系.且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.
 

二、CAS你知道吗

1、比较并交换

举例:

public class CASDemo {
	public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2014)+"\t current"+atomicInteger.get());
    }
}

结果:

2、CAS底层原理?对UnSafe的理解

(1)atomicInteger.getAndIncrement()方法的源代码

(2)UnSafe


A、UnSafe是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后面,基于该类可以直接操作特额定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法.
注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务

B、变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的

C、变量value和volatile修饰,保证了多线程之间的可见性.

(3)CAS是什么

CAS的全称为Compare-And-Swap ,它是一条CPU并发原语.,它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的.

CAS并发原语提现在Java语言中就是sun.miscUnSaffe类中的各个方法.调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令.这是一种完全依赖于硬件 功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题.

var1 AtomicInteger对象本身.
var2 该对象值的引用地址
var4 需要变动的数值
var5 是用过var1 var2找出内存中的值
用该对象当前的值与var5比较
如果相同,更新var5的值并且返回true
如果不同,继续取值然后比较,直到更新完成

  假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):
 A、AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.
 B、线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起.
C、.线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK.
D、这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.
E、线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.

3、CAS缺点

(1)循环时间长开销大


(2)只能保证一个共享变量的原子操作

(3)ABA问题

三、原子类AutomaticInteger的ABA问题?原子更新引用?

1、ABA问题的产生

2、AutomaticReference原子引用——不能解决ABA问题

举例:

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User zs = new User("zs", 22);
        User ls = new User("ls", 22);
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        userAtomicReference.set(zs);//主物理内存设置为zs
        //如果是zs则更新为ls
        System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(zs, ls)+"\t"+userAtomicReference.get().toString());
    }
}
@Getter@Setter@AllArgsConstructor@ToString
class User{
    private String name;
    private int age;
}

结果:

3、AutomaticStampedReference时间戳原子引用——解决ABA问题

加版本号,类似于时间戳

public class ABADemo {
    private static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
    private static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        System.out.println("===以下是ABA问题的产生===");
        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();

        new Thread(()->{
            //先暂停1秒 保证完成ABA
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());
        },"t2").start();
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("===以下是ABA问题的解决===");

        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
            //暂停1秒钟t3线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第2次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第3次版本号"+stampedReference.getStamp()+"\t值是"+stampedReference.getReference());
        },"t3").start();

        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第1次版本号"+stamp+"\t值是"+stampedReference.getReference());
            //保证线程3完成1次ABA
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result+"\t最新版本号"+stampedReference.getStamp());
            System.out.println("最新的值\t"+stampedReference.getReference());
        },"t4").start();
    }
}

四、ArrayList是非线程安全的,给出例子和解决方案

补充:ArrayList底层原理和扩容机制

jdk1.8ArrayList主要方法和扩容机制(源码解析):https://blog.csdn.net/u010890358/article/details/80515284

1、ArrayList线程不安全的例子

public class ContainerNotSafeDemo {

    public static void main(String[] args) {
    	List<String> list= new ArrayList<>();
    	//方式一:
    	//List<String> list= new Vector<>();
    	//方式二:
    	//List<String> list = Collections.synchronizedList(new ArrayList<>());
    	//方式三:
        //List<String> list= new CopyOnWriteArrayList<>();
        //List<String> list= new CopyOnWriteArrayList<>();
        for (int i = 1; i <=30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(1,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

运行结果:

故障现象:java.util.ConcurrentModificationException
导致原因:并发争抢修改导致

2、ArrayList线程不安全解决方案

解决方案:
       (1)new Vector<>():Vector的add方法加了synchronize关键字
       (2)Collections.synchronizedList(new ArrayList<>());
       (3) new CopyOnWriteArrayList<>();

     * 写时复制 copyOnWrite 容器即写时复制的容器 往容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行copy 复制出一个新的object[] newElements 然后向新容器object[] newElements 里面添加元素 添加元素后,再将原容器的引用指向新的容器 setArray(newElements);
     * 这样的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁 因为当前容器不会添加任何容器.所以copyOnwrite容器也是一种读写分离的思想,读和写不同的容器.

CopyOnWriteArrayList的add方法源码:

//CopyOnWriteArrayList的add方法源码
 public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获取旧数组及其长度
            Object[] elements = getArray();
            int len = elements.length;
            //创建一个新的数组,长度为旧数组长度+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //把元素放入新数组
            newElements[len] = e;
            //将旧数组指向新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

final void setArray(Object[] a) {
        array = a;
    }

List线程->CopyOnWriteArrayList

Set线程->CopyOnWriteArraySet

Map线程->ConcurrentHashMap

3、集合类之不安全set

例子同上:

将Set<String> set = new HashSet<>();改成Set<String> set = new CopyOnWriteArraySet<>();可解决线程安全问题

(1)HashSet底层是HashMap!!!(初始容量为16,负载因子为0.75):

(2)HashSet的add只用加一个元素,而HashMap添加可是要添加两个元素键值对啊,怎么解释?

加的键是当前元素,值是一个常量

(2)CopyOnWriteArraySet的底层实际上使用的是CopyOnWriteArrayList:

4、集合类之不安全Map

例子同上:

将Map<String,String> map = new HashMap<>();

改成Map<String,String> map = new ConcurrentHashMap<String, String>();可解决线程安全问题

五、公平锁/非公平锁/可重入锁(递归锁)/自旋锁~谈谈理解

1、公平锁和非公平锁

(1)是什么?

公平锁:是指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到
非公平锁:是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象

(2)二者区别

并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 默认是非公平锁

对于synchronized而言 也是一种非公平锁.

2、可重入锁(又名递归锁)

(1)是什么?

  可重入锁(也叫做递归锁)指的是同一线程外层函数获得锁后,内层的函数仍然能获取该锁的代码。在同一线程外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块

(2)ReentrantLock和Synchronize是典型的可重入锁

(3)可重入锁最大的作用是避免死锁

(4)ReentrantLockDemo

例子:Synchronize可重入锁示例

public class ReenterLockDemo {
	public static void main(String[] args) {
		Phone phone  = new Phone();
		new Thread(()->{
			try {
				phone.sendSms();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		},"t1").start(); 
		new Thread(()->{
			try {
				phone.sendSms();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		},"t2").start(); 
	}
}
class Phone{
	public synchronized void sendSms() throws Exception{
		System.out.println(Thread.currentThread().getName()+"\tsendSms");
		sendEmail();
	}
	
	 public synchronized void sendEmail() throws Exception{
	        System.out.println(Thread.currentThread().getName()+"\tsendEmail");
	    }

}

结果:

例子:ReentrantLock可重入锁示例

public class ReenterLockDemo2 {
	public static void main(String[] args) {
		MyPhone myPhone = new MyPhone();
		Thread t1 = new Thread(myPhone,"t1");
		Thread t2 = new Thread(myPhone,"t2");
		t1.start();
		t2.start();
	}
}
class MyPhone implements Runnable{
	private Lock lock = new ReentrantLock();

	@Override
	public void run() {
		get();
	}

	private void get() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\tget");
			set();
		} finally {
			lock.unlock();
		}
	}

	private void set() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\tset");
		} finally {
			lock.unlock();
		}
	}
	
}

结果:

3、自旋锁

指尝试获取锁的线程不会立即阻塞,而是采取循环的方式去尝试获取锁

这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

举例:实现一个自旋锁

自旋锁的好处是循环比较直到取得成功为止,没有类似wait的阻塞

通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到

public class SpinLockDemo {
	public static void main(String[] args) {
		SpinLockDemo spinLockDemo = new SpinLockDemo();
		new Thread(()->{
			spinLockDemo.myLock();
			//让线程暂停一会
			try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
			spinLockDemo.myUnLock();
		},"A").start();
		
		//暂停一会,保证A线程先启动
		try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
		new Thread(()->{
			spinLockDemo.myLock();
			try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
			spinLockDemo.myUnLock();
		},"B").start();
	}
	
	// 原子引用线程
	AtomicReference<Thread> auAtomicReference = new AtomicReference<Thread>();
	
	public void myLock() {
		Thread thread = Thread.currentThread();
		System.out.println(Thread.currentThread().getName()+"\t come in!!!");
		//线程A进来时,条件为true,取反为false【auAtomicReference值被设置为thread】
		//线程B进来时,条件为false,取反为true,一直循环等待
		while (!auAtomicReference.compareAndSet(null, thread)) {}
	}
	public void myUnLock() {
		Thread thread = Thread.currentThread();
		//释放的时候把auAtomicReference置为null
		auAtomicReference.compareAndSet(thread, null);
		System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock!!!");
	}
}

运行结果:

4、独占锁(写锁)、共享锁(读锁)、互斥锁

独占锁:该锁一次只能被一个线程所持有。ReetrantLock和Synchronize都是独占锁

共享锁:该锁可被多个线程所持有

ReetrantReadWriteLock其读锁是共享锁【保证部并发读是高效的】,其写锁是独占锁

举例:使用ReetrantReadWriteLock实现读的时候允许多个线程,写的时候只允许一个线程

public class ReadWriteLockDemo {
	public static void main(String[] args) {
        MyCaChe myCaChe = new MyCaChe();
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCaChe.put(temp + "", temp);
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(() -> {
                myCaChe.get(finalI + "");
            }, String.valueOf(i)).start();
        }
    }

}

class MyCaChe {
	// 保证可见性
	private volatile Map<String, Object> map = new HashMap<>();
	private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

	// 写
	public void put(String key, Object value) {
		reentrantReadWriteLock.writeLock().lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);
			// 模拟网络延时
			try {
				TimeUnit.MICROSECONDS.sleep(300);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			map.put(key, value);
			System.out.println(Thread.currentThread().getName() + "\t完成写入");
		} finally {
			reentrantReadWriteLock.writeLock().unlock();
		}
	}

	public void get(String key) {
		reentrantReadWriteLock.readLock().lock();
		try {
			System.out.println(Thread.currentThread().getName() + "\t正在读取");
			// 模拟网络延时
			try {
				TimeUnit.MICROSECONDS.sleep(300);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			Object result = map.get(key);
			System.out.println(Thread.currentThread().getName() + "\t完成读取" + result);
		} finally {
			reentrantReadWriteLock.readLock().unlock();
		}
	}

	public void clearCaChe() {
		map.clear();
	}

}

结果:

六、CountDownLatch、CyclicBarrier、Semaphore使用过吗

1、CountDownLatch

让一些线程阻塞直到另外一些完成后才被唤醒

CountDownLatch主要有两个方法当一个或多个线程调用await方法时,调用线程会被阻塞,其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行

(1)举例:关门案例

public class CountDownLatchDemo {
	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(6);
		for (int i = 1; i <= 6; i++) {
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "\t" + "上完自习");
				countDownLatch.countDown();
			}, String.valueOf(i)).start();
		}
		countDownLatch.await();
		//一定要保证则六个人离开教室了班长才能锁门
		System.out.println(Thread.currentThread().getName() + "\t班长锁门离开教室");
	}
}

结果:

(2)举例:枚举的使用

public enum CountryEnum {

	ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");

	CountryEnum(Integer code, String name) {
		this.code = code;
		this.name = name;
	}

	@Getter
	private Integer code;
	@Getter
	private String name;

	public static CountryEnum forEach(int index) {
		CountryEnum[] countryEnums = CountryEnum.values();
		for (CountryEnum countryEnum : countryEnums) {
			if (index == countryEnum.getCode()) {
				return countryEnum;
			}
		}
		return null;
	}

	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(6);
		for (int i = 1; i <= 6; i++) {
			final int finalI = i;
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName()+"\t"+CountryEnum.forEach(finalI).name + "\t 国被灭");
				countDownLatch.countDown();
			}, String.valueOf(i)).start();
		}
		countDownLatch.await();
		//一定要灭亡了六国才可统一
		System.out.println(Thread.currentThread().getName() + "\t秦统一六国");
	}
}

结果:

2、CyclicBarrier

CyclicBarrier的字面意思是可循环使用的屏障。它要做的事情是让一组线程到达一个屏障之后时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法

举例:集齐7颗龙珠能召唤神龙

(1)方式一:使用CyclicBarrier实现

public class CyclicBarrierDemo {
	public static void main(String[] args) {
		CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
			System.out.println("召唤神龙");
		});
		for (int i = 1; i <= 7; i++) {
			final int temp = i;
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "\t 收集到第" + temp + "颗龙珠");
				try {
					cyclicBarrier.await();
				} catch (InterruptedException | BrokenBarrierException e) {
					e.printStackTrace();
				}
			}, String.valueOf(i)).start();
		}
	}
}

结果:

(2)方式二:使用CountDownLatch实现

public class CyclicBarrierDemo {
	public static void main(String[] args) {
		CountDownLatch countDownLatch = new CountDownLatch(7);
		for (int i = 1; i <= 7; i++) {
			final int temp = i;
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "\t 收集到第" + temp + "颗龙珠");
				countDownLatch.countDown();
			}, String.valueOf(i)).start();
		}
		try {
			countDownLatch.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("召唤神龙");
	}
}

运行结果:

3、Semaphore

信号量的使用目的:1)共享资源的相互排斥;2)并发资源数的控制

Semaphore具有“伸缩性”,就是资源用完了释放后还可以继续用

举例:抢车位【3个车,6个停车位】

public class SemaphoreDemo {
	public static void main(String[] args) {
		// 模拟3个停车位
		Semaphore semaphore = new Semaphore(3);
		// 模拟6部汽车
		for (int i = 1; i <= 6; i++) {
			new Thread(() -> {
				try {
					// 抢到资源
					semaphore.acquire();
					System.out.println(Thread.currentThread().getName() + "\t抢到车位");
					try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
					System.out.println(Thread.currentThread().getName() + "\t 停3秒离开车位");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					// 释放资源
					semaphore.release();
				}
			}, String.valueOf(i)).start();
		}
	}

}

运行结果:

七、阻塞队列

1、队列和阻塞队列

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如图所示

线程1往阻塞队列中添加元素二线程2从队列中移除元素

当阻塞队列是空时,从队列中获取元素的操作将会被阻塞.
当阻塞队列是满时,往队列中添加元素的操作将会被阻塞.
同样,试图往已满的阻塞队列中添加新的线程同样也会被阻塞,直到其他线程从队列中移除一个或者多个元素或者全清空队列后使队列重新变得空闲起来并后续新增.

2、为什么用?有什么好处?

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即线程阻塞),一旦条件满足,被挂起的线程优先被自动唤醒
 
为什么需要使用BlockingQueue:好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为BlockingQueue都一手给你包办好了。在concurrent包 发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度.

3、BlockingQueue的核心方法

4、BlockingQueue架构梳理+种类分析

Iterable->Collection->Queue->BlockingQueue->LinkedTransferQueue、LinkedBlockingDeque、SynchronousQueue、ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityBlockingQueue

(1)ArrayBlockingQueue:由数组组成的有界阻塞队列

(2)LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列

(3)PriorityBlockingQueue:支持优先级排序的无界阻塞队列

(4)DelayQueue:使用优先级队列实现的延迟无界阻塞队列

(5)SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列

SynchronousQueue没有容量,与其他BlcokingQueue不同,SynchronousQueue是一个不存储元素的BlcokingQueue
 每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然.

举例:

public class SynchronousQueueDemo {
	public static void main(String[] args) {
		BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
		new Thread(() -> {
			try {
				System.out.println(Thread.currentThread().getName() + "\t put 1");
				blockingQueue.put("1");
				System.out.println(Thread.currentThread().getName() + "\t put 2");
				blockingQueue.put("2");
				System.out.println(Thread.currentThread().getName() + "\t put 3");
				blockingQueue.put("3");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "AAA").start();

		new Thread(() -> {
			try {
				try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
				System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
				try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
				System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
				try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
				System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "BBB").start();
	}

}

结果:A线程Put一个,必须等到B线程take了才能再继续Put

(6)LinkedTransferQueue:由链表结构组成的无界阻塞队列

(7)LinkedBlockingDeque:由链表结构组成的双向阻塞队列

5、用在哪里

(1)生产者消费者模式

举例:传统版“生产者消费者模式”【一个初始值为0的变量 两个线程交替操作 一个加1 一个减1来5轮】

public class ProdConsumerTraditionDemo {
	//一个初始值为0的变量 两个线程交替操作 一个加1 一个减1来5轮
	public static void main(String[] args) {
		ShareData shareData = new ShareData();
             //生产者线程
		new Thread(()->{
			for (int i = 1; i <= 5; i++) {
                try {
                    shareData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
		},"AA").start();
            //消费者线程
		new Thread(()->{
			for (int i = 1; i <= 5; i++) {
                try {
                    shareData.deIncrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
		},"BB").start();
	}
}
class ShareData {
	private int num = 0;
	private Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();
	
	public void increment() throws InterruptedException {
		lock.lock();
		try {
			//判断
			while (num != 0) {
				//等待,不生产
				condition.await();
			}
			//干活
			num++;
			System.out.println(Thread.currentThread().getName() + "\t" + num);
            //通知唤醒
            condition.signalAll();
		} finally {
			lock.unlock();
		}
	}
	
	public void deIncrement() throws InterruptedException {
		lock.lock();
		try {
			//判断
			while (num == 0) {
				//等待,不生产
				condition.await();
			}
			//干活
			num--;
			System.out.println(Thread.currentThread().getName() + "\t" + num);
            //通知唤醒
            condition.signalAll();
		} finally {
			lock.unlock();
		}
	}
}

结果:

举例:阻塞队列版“生产者消费者模式”【一个初始值为0的变量 两个线程交替操作 一个加1 一个减1来5轮】

public class ProdConsumerBlockQueueDemo {
	public static void main(String[] args) throws Exception {
        MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
            try {myResource.myProductor();} catch (Exception e) {e.printStackTrace();}
        },"Productor").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t消费线程启动");
            try {myResource.myConsumer();} catch (Exception e) {e.printStackTrace();}
        },"consumer").start();
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println();
        System.out.println();
        System.out.println();
        System.out.println("时间到,停止活动");
        myResource.stop();
    }

}

class MyResource {
	// 默认开启,进行生产+消费的交互
	private volatile boolean flag = true;
	// 默认值是0;多线程环境下不要做i++/i--,使用AtomicInteger
	private AtomicInteger atomicInteger = new AtomicInteger();
	// 构造注入,传入的阻塞队列类型-->使程序更加通用
	private BlockingQueue<String> blockingQueue = null;

	public MyResource(BlockingQueue<String> blockingQueue) {
		this.blockingQueue = blockingQueue;
		System.out.println(blockingQueue.getClass().getName());// 看传的是什么类
	}

	// 生产者
	public void myProductor() throws InterruptedException {
		String data = null;
		boolean returnValue;
		while (flag) {
			data = atomicInteger.incrementAndGet() + "";// 传入的是String类型
			returnValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);// 设置超时时间
			if (returnValue) {
				System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "成功");
			} else {
				System.out.println(Thread.currentThread().getName() + "\t 插入队列数据" + data + "失败");
			}
			TimeUnit.SECONDS.sleep(1);
		}
		System.out.println(Thread.currentThread().getName() + "\t 停止 表示 flag=" + flag+"生产线程结束");
	}

	// 消费者
	public void myConsumer() throws Exception {
		String result = null;
		while (flag) {
			result = blockingQueue.poll(2L, TimeUnit.SECONDS);
			if (null == result || "".equalsIgnoreCase(result)) {
				flag = false;
				System.out.println(Thread.currentThread().getName() + "\t" + "超过2m没有取到 消费退出");
				System.out.println();
				System.out.println();
				return;
			}
			System.out.println(Thread.currentThread().getName() + "消费队列" + result + "成功");
		}
	}

	public void stop() throws Exception {
		flag = false;
	}
}

结果:

(2)线程池

(3)消息中间件

6、Synchronize和Lock有什么区别?

(1)原始构成

Synchronize是关键字,属于JVM层面:monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或者方法中才能调用wait/notify等方法)、monitorexit

Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁

(2)使用方法

Synchronize不需要用户去手动释放锁,当synchronize代码执行完后系统会自动让线程释放对锁的占用

ReentrantLock则需要用户去手动释放锁若没有主动释放锁,则有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成

(3)等待是否可以中断

synchronize不可中断,除非·抛出异常或者正常运行完成

ReentrantLock可中断:1)设置超时方法tryLock(long timeout,TimeUnit unit)

                                      2)lockInterruptibly()放代码块中,调用interrupt方法可中断

(4)加锁是否公平

synchronize是非公平锁

ReentrantLock两者都可以默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

(5)锁绑定多个条件condition

synchronize没有

ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像Synchronize要么随机唤醒一个线程,要么唤醒全部线程

7、用新的Lock有什么好处【ReentrantLock】?

  题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
 AA打印5次,BB打印10次,CC打印15次
 AA打印5次,BB打印10次,CC打印15次……来10轮

public class SyncAndReentrantDemo {
	public static void main(String[] args) {
		ShareResource shareResource = new ShareResource();
		//A线程打印5次
		new Thread(()->{
			for (int i = 1; i <= 10; i++) {
				try {shareResource.print5();} catch (InterruptedException e) {e.printStackTrace();
				}
			}
		},"A").start();
		new Thread(()->{
			for (int i = 1; i <= 10; i++) {
				try {shareResource.print10();} catch (InterruptedException e) {e.printStackTrace();
				}
			}
		},"B").start();
		new Thread(()->{
			for (int i = 1; i <= 10; i++) {
				try {shareResource.print15();} catch (InterruptedException e) {e.printStackTrace();
				}
			}
		},"C").start();
	}
}
class ShareResource{
	private int num  = 1;//A:1;B:2;C:3
	private Lock lock = new ReentrantLock();
	//定义三个Condition->一把锁里面有三把备用钥匙
	private Condition c1 = lock.newCondition();
	private Condition c2 = lock.newCondition();
	private Condition c3 = lock.newCondition();
	
	public void print5() throws InterruptedException {
		lock.lock();
		try {
			//1、判断
			while (num != 1) {
				c1.await();
			}
			//2、干活
			for (int i = 1; i <= 5; i++) {
				System.out.println(Thread.currentThread().getName()+"\t"+i);
			}
			//3、通知唤醒
			num = 2;
			c2.signal();
		} finally {
			lock.unlock();
		}
	}
	
	public void print10() throws InterruptedException {
		lock.lock();
		try {
			//1、判断
			while (num != 2) {
				c2.await();
			}
			//2、干活
			for (int i = 1; i <= 10; i++) {
				System.out.println(Thread.currentThread().getName()+"\t"+i);
			}
			//3、通知唤醒
			num = 3;
			c3.signal();
		} finally {
			lock.unlock();
		}
	}
	
	public void print15() throws InterruptedException {
		lock.lock();
		try {
			//1、判断
			while (num != 3) {
				c3.await();
			}
			//2、干活
			for (int i = 1; i <= 15; i++) {
				System.out.println(Thread.currentThread().getName()+"\t"+i);
			}
			//3、通知唤醒
			num = 1;
			c1.signal();
		} finally {
			lock.unlock();
		}
	}
}

结果:AA打印5次,BB打印10次,CC打印15次【重复10次】

8、实现线程的四种方式

(1)继承Thread类

定义:

调用:

(2)实现Runable接口

定义:

调用:

(3)实现Callable接口

Thread构造方法中没有参数是Callable的构造方法!!!!怎么办呢?分析如下:

A、可以看到Thread的构造函数中包含了含有Runable参数的构造函数

B、查看Runable的API,可看到它的子接口有RunableFuture<V>

C、查看RunableFuture<V>接口的API,可知它所有实现类中有FutureTask这个子类

D、查看FutureTask的API,可以看到它实现了Runable接口,而它的构造方法中,其中一个参数就是Callable,另一个参数是Runable。FutureTask在其中起到穿针引线的作用!!!

经过也上分析可知,可以使用适配解决上述问题~~

定义:

调用:结果输出1024

注意:get方法一般放在最后。要求获得callable线程的计算结果,如果没有计算完成就要去强求,会导致堵塞,直到计算完

public class CallableDemo2 {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread());
		//这里要传一个实现了Runable接口的类,而FutureTask实现了Runable接口
		new Thread(futureTask).start();
		//int result1 = futureTask.get();//放在此处main线程会被阻塞
		
		System.out.println(Thread.currentThread().getName()+"\t main.");
		
		//get方法一般放在最后。要求获得callable线程的计算结果,如果没有计算完成就要去强求,会导致堵塞,直到计算完
		int result1 = futureTask.get();//放在此处main线程不会被阻塞
		System.out.println(result1 + 4);
	}
}
class MyThread implements Callable<Integer>{

	@Override
	public Integer call() throws Exception {
		System.out.println(Thread.currentThread().getName()+"\t get into call.");
		TimeUnit.SECONDS.sleep(3);//休息3s
		return 1024;
	}
	
}

运行结果:

或者说在调用get方法事前判断FutureTask是否执行完成,如果执行完成了就获取

注意:以下这种情况只会调用call方法一次,因为一样的事儿不会重复干,复用就可以了!!!

那如果非要它调用两次怎么办呢?

(4)使用线程池

八、线程池用过吗?ThreadPoolExecutor谈谈你的理解

1、为什么使用线程池,优势?

  线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果线程超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行.
 
它的主要特点为:线程复用、控制最大并发数、管理线程.
 
第一:降低资源消耗.通过重复利用自己创建的线程降低线程创建和销毁造成的消耗.
第二: 提高响应速度.当任务到达时,任务可以不需要等到线程创建就能立即执行.
第三: 提高线程的可管理性.线程是稀缺资源,如果无限的创建,不仅会消耗资源,还会较低系统的稳定性,使用线程池可以进行统一分配,调优和监控.

2、线程池如何使用

(1)架构实现

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类.

线程池的底层就是ThreadPoolExecutor这个类!!!

(2)五种类型的线程池分别怎么使用?

A、newFixedThreadPool(int):执行一个长期的任务,性能好很多

主要特点如下:
1.创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待.
2.newFixedThreadPool创建的线程池corePoolSize和MaxmumPoolSize是 相等的,它使用的的LinkedBlockingQueue

B、newSingleThreadExecutor():一个任务一个线程执行的场景

主要特点如下:
1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序执行.
2.newSingleThreadExecutor将corePoolSize和MaxmumPoolSize都设置为1,它使用的的LinkedBlockingQueue

C、newCachedThreadPool():适用于执行很多短期异步的小程序或者负载较轻的服务器

 主要特点如下:
1.创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新线程.
2.newCachedThreadPool将corePoolSize设置为0,MaxmumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQUeue,也就是说来了任务就创建线程运行,如果线程空闲超过60秒,就销毁线程

D、newScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer

E、newWorkStealingPool(int):使用目前机器上可以用的处理器作为它的并行级别

举例:一池5个处理线程;模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程

public class MyThreadDemo2 {
	public static void main(String[] args) {
		//ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
		//ExecutorService threadPool = Executors.newSingleThreadExecutor();//一池1个处理线程
		ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个处理线程
		
		//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
		try {
			//ExecutorService含有两个方法,一个是不带返回值的execut方法,一个是带返回值的submit方法
			//Future<?> submit(Runnable task)提交一个可运行的任务执行,并返回一个表示该任务的未来。 未来的get方法将返回null 成功完成时。 
			//void execute(Runnable command)在将来的某个时间执行给定的命令。 该命令可以在一个新线程,一个合并的线程中或在调用线程中执行,由Executor实现。
			for (int i = 1; i <= 10; i++) {
				threadPool.execute(()->{
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			threadPool.shutdown();
		}
	}
}

3、线程池7大重要参数介绍

ThreadPoolExecutor构造函数

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

4、线程池的底层工作原理

 以下重要!!!!!!!! 以下重要!!!!!!!! 以下重要!!!!!!!! 以下重要!!!!!!!! 以下重要!!!!!!!!

5、线程池的使用距离说明底层原理——银行网点

以银行网点的例子形象说明:

public class ThreadPoolDemo {
	public static void main(String[] args) {
		ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
				2, //今日当值窗口
				5, //受理窗口只有5个
				100L, TimeUnit.SECONDS, 
				new LinkedBlockingQueue<Runnable>(3),//等候区
				Executors.defaultThreadFactory(), 
				new ThreadPoolExecutor.AbortPolicy());
		try {
			for (int i = 1; i <= 6; i++) {//模拟6个客户,受理窗口只有5个
				final int tempI = i;
				threadPool.execute(()->{
					System.out.println(Thread.currentThread().getName()+"号窗口,服务客户"+tempI);
					try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}
				});
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			threadPool.shutdown();
		}
	}
}

模拟6个客户运行结果:1、2先执行;3、4、5进入等待队列;6创建非核心线程立刻执行,所以6比345先执行

模拟8个客户运行结果:1、2、6、7、8先执行【核心线程2个,扩容的3个】;3、4、5进入等待队列

      

模拟10个客户运行结果:因为队列满了,且正在运行的线程数量大于等于了最大线程数,启用拒绝策略,避免把线程池冲垮

九、线程池如何合理设置参数

1、线程池的四种拒绝策略

等待队列也已经排满了,再也塞不下新的任务。同时,线程池的max也到达了,无法接续为新任务服务。这时我们需要拒绝策略机制合理的处理这个问题.

-----------------------------------------------------------------------------------------------------

JDK内置的拒绝策略:

ThreadPoolExecutor.AbortPolicy:默认拒绝策略,直接抛出 RejectedExecutionException异常阻止系统正常运行

ThreadPoolExecutor.CallerRunsPolicy:“调用者运行”一种调节机制,该策略不会抛弃任务,也不抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量

ThreadPoolExecutor.DiscardPolicy :直接丢弃任务,不予处理也不抛出异常。若允许任务丢失,这是最好的拒绝策略

ThreadPoolExecutor.DiscardOldestPolicy :抛弃队列中等待时间最久的任务然后把当前任务加入队列中尝试再次提交

如果执行程序没有关闭,则工作队列头部的任务被删除,然后重试执行(可能会再次失败,导致重复)。

以上策略均实现了RejectedExecutionHandler接口

-----------------------------------------------------------------------------------------------------

还是上述代码,模拟10个客户运行结果,各个拒绝策略运行结果如下:

(1)AbortPolicy:先执行任务12678,后执行任务345,不执行任务9和10,抛出异常

(2)CallerRunsPolicy:先执行任务12678和9(由主线程执行),后执行任务345和10(由主线程执行)

(3)DiscardPolicy :类似于AbortPolicy策略,先执行任务12678,后执行任务345,不执行任务9和10,但不抛出异常

(4)DiscardOldestPolicy :先执行任务12678,而后删除3459和10中等待时间最长的2个任务

2、*你在工作中单一的/固定数的/可变的*三种创建线程池的方法,你用哪个多?超级大坑

答案是一个都不用,我们在生产中只能使用自定义的!!!Executors的JDK中给出了为什么不用!!!

 -----------------------------------------------------------------------------------------------------

参考阿里巴巴java开发手册
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPoolSingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。【因为这两种线程池底层阻塞队列是LinkedBlockingQueue】
2)CachedThreadPoolScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

-----------------------------------------------------------------------------------------------------

3、你在工作中是如何创建线程池的——线程池的手写改造和拒绝策略

4、合理配置线程池参数

(1)CPU密集型

System.out.println(Runtime.getRuntime().availableProcessors());查看CPU核数

CPU密集型的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:【CPU核数+1个线程的线程池】

(2)IO密集型

A、由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如【CPU核数*2】

B、IO密集型,即该任务需要大量的IO,即大量的阻塞。

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力在等待。所以IO密集型任务中使用多线程可以大大加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间

IO密集型时,大部分线程都阻塞,故需要多配制线程数:【CPU核数/(1-阻塞系数)】 阻塞系数在0.8~0.9之间

比如8核CPU:8/(1-0.9)=80个线程数

十、死锁编码及定位分析

1、什么是死锁?

产生死锁的原因:系统资源不足、进程运行推进的顺序不合适、资源分配不当

2、死锁的例子

public class DeadLockDemo {
	public static void main(String[] args) {
		String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldThread(lockA, lockB), "threadAAA").start();
        new Thread(new HoldThread(lockB, lockA), "threadBBB").start();
	}
}
class HoldThread implements Runnable{
	
	private String lockA;
	private String lockB;
	
	public HoldThread(String lockA, String lockB) {
		this.lockA = lockA;
		this.lockB = lockB;
	}

	@Override
	public void run() {
		synchronized (lockA) {
			System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockA + "尝试获得" + lockB);
            try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t 自己持有锁" + lockB + "尝试获得" + lockA);
            }

		}
	}
}

执行结果:

3、如何解决死锁?

(1)jps命令定位进程编号

(2)jstack找到死锁查看

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值