Java多线程进阶知识梳理(下)

目录

1.线程池(重点)3大方法,7大参数,4大策略

2.CPU密集型和IO密集型(确定线程池的最大线程数)

3.四大原生函数式接口(基础不多说)

4.Forkjoin详解

5.异步回调(CompletableFuture)

6.JMM和Volatile理解

7.彻底玩转单例模式

8.深入理解CAS

9.可重入锁

9.1.公平锁和非公平锁

9.2.可重入锁

9.3.自旋锁

10.死锁的解决办法


1.线程池(重点)3大方法,7大参数,4大策略

池化技术的本质:占用系统资源,优化资源的使用

线程池,连接池,内存池,常量池

创建,销毁,十分的浪费资源

线程池的好处:

降低资源的消耗

搞高响应的速度

三大方法:

//Executors 线程池的工具类,阿里巴巴开发手册禁止使用
//可以伸缩的线程池,最大21亿
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//指定线程数的线程池
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
//一个线程的线程池
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

//线程池记得要关闭
newCachedThreadPool.shutdown();
newFixedThreadPool.shutdown();
newSingleThreadExecutor.shutdown();

七大参数:

源码分析:

本质都是调用了ThreadPoolExecutor

//newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

//newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

//newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
                              int maximumPoolSize,//最大核心线程池大小
                              long keepAliveTime,//存活时间
                              TimeUnit unit,//存活时间的单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              ThreadFactory threadFactory,//线程工厂
                              RejectedExecutionHandler handler)//4种拒绝策略

四大拒绝策略

AbortPolicy:不处理这个线程,抛出异常

CallerRunsPolicy:哪来的回哪去,一般是主线程来的,交回给主线程执行

DiscardPolicy:队列满了,丢掉任务,不会抛出异常

DiscardOldestPolicy:队列满了,尝试去和最早的线程竞争,也不会抛出异常

举个例子:最大并发线程数是:max+队列长度 = 5+3=8

public static void main(String[] args){
	new ThreadPoolExecutor(
			2, 
			5, 
			2, 
			TimeUnit.SECONDS, 
			new ArrayBlockingQueue<Runnable>(3), 
			Executors.defaultThreadFactory(), 
			new ThreadPoolExecutor.DiscardOldestPolicy());
}

2.CPU密集型和IO密集型(确定线程池的最大线程数)

cpu密集型:几核就是几,保持cpu效率最高(Runtime.getRuntime().availableProcessors())

io密集型:判断程序中是否消耗IO的线程 *2

线程池调优的手段之一

public static void main(String[] args){
	new ThreadPoolExecutor(
			2, 
			Runtime.getRuntime().availableProcessors(), 
			2, 
			TimeUnit.SECONDS, 
			new ArrayBlockingQueue<Runnable>(3), 
			Executors.defaultThreadFactory(), 
			new ThreadPoolExecutor.DiscardOldestPolicy());
}

3.四大原生函数式接口(基础不多说)

方法型:Function<T,R>:R apply(T t)
布尔型:Predicate<T>:boolean test(T t)
消费型:Consumer<T>:void accept(T t)
供给型:Supplier<T>:T get()

其他的都是变种

4.Forkjoin详解

什么叫Forkjoin?大数据量计算,拆分任务,合并结果,最大的特点:工作窃取,底层是双端队列

class TaskDemo extends RecursiveTask<Long>

TaskDemo task1 = new TaskDemo(start, temp);
task1.fork();
TaskDemo task2 = new TaskDemo(temp+1, end);
task2.fork();			
return task1.join() + task2.join();
--------------------------------------------------------------
ForkJoinPool forkJoinPool = new ForkJoinPool();
TaskDemo taskDemo = new TaskDemo(0L, 10000L);
ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(taskDemo);
	
System.out.println(forkJoinTask.get());

--------------------------------------------------------------
//0到100000L的求和
LongStream.rangeClosed(0L, 100000L).parallel().reduce(0L, Long :: sum)

5.异步回调(CompletableFuture)

没有返回值的情况:public static CompletableFuture<Void> runAsync(Runnable runnable)

public static void main(String[] args) throws InterruptedException, ExecutionException {
	CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("runAsync");
	});
	System.out.println("1111111111111");
	completableFuture.get();//阻塞返回结果
}

有返回值的情况:public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

public static void main(String[] args) throws InterruptedException, ExecutionException {
	CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> {
		System.out.println("supplyAsync");
		return 1024;
	});
	
	System.out.println(supplyAsync.whenComplete((t,u) -> {
		System.out.println("t:"+t);//正确返回值信息
		System.out.println("u:"+u);//错误返回的异常信息
	}).exceptionally(e -> {
		System.out.println(e.getMessage());
		return 404;
	}).get());
}

6.JMM和Volatile理解

请你谈谈Volatile的理解

Volatile是java虚拟机提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是JMM

JMM:java内存模型,不存在的东西,概念,约定

  • 线程解锁前,必须把共享变量立刻刷回主内存
  • 线程加锁前,必须读取主内存中的最新值到工作内存中去
  • 加锁和解锁必须是同一把锁

JMM的8种操作

lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。
unclock(解锁):作用于主内存的变量,把一个处于锁定的状态释放出来。


read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
load(载入):作用于工作内存的变量,把read操作从主内存 得到的变量值放入工作内存的变量副本中。


use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值 赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。


store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存,以便write操作使用。
write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

必须成对出现

Volatile保证可见性

private static int num = 0;//主内存

public static void main(String[] args){
	new Thread(()->{
		while(num == 0) {//复制到子线程的工作内存中
			
		}
	}).start();
	
	try {
		TimeUnit.SECONDS.sleep(1);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	num = 1;//修改主内存的变量
	System.out.println("end==============");
}

//主内存的变量值已修改,但是子线程的工作内存变量一直没变,一直处于while死循环中

//如何修改呢?
private static volatile int num = 0;
//volatile 具有可见性,主内存的变量值已修改,子线程停止while死循环

Volatile的不保证原子性,执行结果少于预想的7000

public class Test5 {
	
	private static volatile int num = 0;
	
	public static void add() {
		num++;//不是一个原子操作,反编译后
	}
	
	public static void main(String[] args) throws InterruptedException{
		for (int i = 0; i < 70; i++) {
			new Thread(()->{
				for (int j = 0; j < 100; j++) {
					add();
				}
			}).start();
		}
		while(Thread.activeCount() > 2) { //除了 main gc
			Thread.yield();
		}
		System.out.println(num);
	}
}

解决办法如下:
1.关键字synchronized或者lock锁(性能很差)

public synchronized static void add() {
    num++;//不是一个原子操作,反编译后
}

//或者

static Lock lock = new ReentrantLock();
public synchronized static void add() {
	num++;//不是一个原子操作,反编译后
	try {
		lock.lock();
	} finally {
		lock.unlock();
	}
}

2.采用线程原子类AtomicInteger(底层用的是CAS(unsafe),性能很强)

private static volatile AtomicInteger atomicInteger = new AtomicInteger();
public static void add() {
    atomicInteger.getAndIncrement();
}

禁止指令重排

计算机并不是按照程序的顺序执行的,会根据数据之间的依赖性,优化重排,多线程下可能会影响结果

  1. 编译器优化的重排
  2. 指令并行页可能会重排
  3. 内存系统也会重排

volatile 是如何禁止指令重排的呢?

内存屏障,CPU指令,作用:

  1. 保证特定的操作的执行顺序
  2. 可以保证某些变量的内存可见性(利用这些特性,volatile保证了可见性)

7.彻底玩转单例模式

饿汉式单例(基础),缺点很明显,在不使用Test6的情况下,去初始化占用了大量内存的数组是非常浪费内存的

public class Test6 {
    public byte[] byteArray1 = new byte[1024*1024];
	public byte[] byteArray2 = new byte[1024*1024];
	public byte[] byteArray3 = new byte[1024*1024];
	public byte[] byteArray4 = new byte[1024*1024];

	private Test6() {
	}

	private final static Test6 test6 = new Test6();

	public static Test6 getInstance() {
		return test6;
	}
}

懒汉式单例(基础),缺点很明显,在多线程的情况下,不能保证Test6的单例,一检测就发现new了很多实例

public class Test6 {
	private Test6() {
	}
	
	private static Test6 test6 = null;
	
	public static Test6 getInstance() {
		if(test6 == null) {
			test6 = new Test6();
		}
		return test6;
	}

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

双重检测锁模式DCL懒汉模式),但是有问题。。

public static Test6 getInstance() {
	if(test6 == null) {
		synchronized(Test6.class) {
			if(test6 == null) {
				test6 = new Test6();//不是一个原子操作
                /**
					 * 1.分配内存空间
					 * 2.执行构造方法,初始化对象
					 * 3.把这个对象指向这个空间
					 * 
					 * 123
					 * 132 A
					 *     B //此时test6还没有完成构造
					 * 有可能出现指令重排,所以需要增加volatile关键字,禁止指令重排,如下:
					 * private volatile static Test6 test6 = null;
					 */
                   
			}
		}
	}
	return test6;
}

静态内部类单例

public class Test6 {
	private Test6() {
	}
	
	public static Test6 getInstance2() {
		return InnerClass.innerClass;
	}
	
	public static class InnerClass{
		private final static Test6 innerClass = new Test6();
	}
	
	public static void main(String[] args) {
		for(int i = 0;i<10;i++) {
			new Thread(()->
			Test6.getInstance2()
			).start();
		}
	}
}

以上所有的单例模式,在反射面前都是有缺陷的,都是可以破坏的,反射可以破坏单例模式

public static void main(String[] args) throws Exception {
		Test6 oldInstance = Test6.getInstance();
		System.out.println(oldInstance);
		
		Constructor<Test6> constructor = Test6.class.getDeclaredConstructor(null);
		constructor.setAccessible(true);
		Test6 newInstance = constructor.newInstance();
		System.out.println(newInstance);
	}

// 一样可以实例化两个不同的实例

jdk1.5之后的枚举,可以解决该问题(枚举类天生单例),反射破坏不了枚举,看源码:

反编译枚举类,可以看出枚举类存在有参的构造器,两个参数,一个String类型,一个int类型

enum MyEnum {
	INSTANCE;
	
	public static MyEnum getInstance() {
		return INSTANCE;
	}
	
	public static void main(String[] args) throws Exception{
		MyEnum oldInstance = MyEnum.getInstance();
		
		Constructor<MyEnum> constructor = MyEnum.class.getDeclaredConstructor(String.class,int.class);
		constructor.setAccessible(true);
		MyEnum newInstance = constructor.newInstance();
	}
}

执行报错如下:由此可见反射不能破坏枚举单例

8.深入理解CAS

需要看更深层次的sun包下的源码

题外话:如何查看sun下的源码包

1.下载eclipse反编译插件Enhanced Class Decompiler。{在help下的EclipseMarkerplace popular下}

2.windows-->preferences -->File Associations -->*.class without source-->Class Decompiler Viewer(default)

3.Ctrl+T 搜索即可

CAS:全称(CompareAndSwap)

比较当前工作内存中的值和主内存的值,如果这个值是期望的,那么执行操作,如果不是就一直do while循环

缺点:

1.循环会耗时

2.一次性只能保证一个共享变量的原子性

3.ABA问题

什么是ABA问题?(狸猫换太子)

原子引用解决ABA问题,思想跟数据库乐观锁一致

举个例子:带标签的AtomicStampedReference类

AtomicStampedReference(V initialRef, int initialStamp) 
创建一个新的 AtomicStampedReference与给定的初始值。

public boolean compareAndSet(V expectedReference,
                             V newReference,
                             int expectedStamp,
                             int newStamp)
以原子方式设置该引用和邮票给定的更新值的值,如果当前的参考是 ==至预期的参考,并且当前标志等于预期标志。 
参数 
expectedReference - 参考的预期值 
newReference - 参考的新值 
expectedStamp - 邮票的预期值 
newStamp - 邮票的新价值 
结果 
true如果成功

Integer包装类有个大坑,需要注意一下:

	public static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 10);
	
	public static void main(String[] args) throws Exception{
		new Thread(()->{
			int stamp = atomicStampedReference.getStamp();
			System.out.println("a init ="+stamp);

            try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			System.out.println(atomicStampedReference.compareAndSet(1, 2, stamp, stamp + 1));
			System.out.println("a one  ="+atomicStampedReference.getReference()+" === "+ atomicStampedReference.getStamp());
			
			stamp = atomicStampedReference.getStamp();
			System.out.println(atomicStampedReference.compareAndSet(2, 1, stamp, stamp + 1));
			System.out.println("a two  ="+atomicStampedReference.getReference()+" === "+ atomicStampedReference.getStamp());
		},"a").start();
		
		new Thread(()->{
			int stamp = atomicStampedReference.getStamp();
			System.out.println("b init ="+stamp);
			
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
			System.out.println("b one  ="+atomicStampedReference.getReference()+" === "+ atomicStampedReference.getStamp());
			
		},"b").start();
	}

执行结果如下:

a init =10
b init =10
true
a one  =2 === 11
true
a two  =1 === 12
false
b one  =1 === 12

由于线程b休眠了2秒,在这2秒的过程中,线程a做了出现了ABA的情景,虽然值没变,但是标签值变了,线程b更新变量失败

9.可重入锁

9.1.公平锁和非公平锁

公平锁:非常公平,不可以插队,先来后到

非公平锁:非常不公平,可以插队(默认都是非公平)

例如Reentrantlock

public ReentrantLock() {
    sync = new NonfairSync();
}

ReentrantLock reentrantLock = new ReentrantLock(true);

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

9.2.可重入锁

也叫递归锁,拿到了外面的锁,也就可以自动拿到里面的锁。

synchronized和reentrantlock都是可重入锁

//synchronized
class Phone {
	public synchronized void sms() {
		System.out.println(Thread.currentThread().getName()+"==>sms");
		call();
	}
	
	public synchronized void call() {
		System.out.println(Thread.currentThread().getName()+"==>call");
	}
}

//lock ReentrantLock lock()和unlock() 锁必须配对
class Phone {
	Lock lock = new ReentrantLock();
	public void sms() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName()+"==>sms");
			call();
		} finally {
			lock.unlock();
		}
	}
	
	public void call() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName()+"==>call");
		} finally {
			lock.unlock();
		}
	}
}

public static void main(String[] args) {
	Phone phone = new Phone();
	
	new Thread(()->{
		phone.sms();
	},"a").start();
	
	new Thread(()->{
		phone.sms();
	},"b").start();
}

9.3.自旋锁

Atomic包下的原子类底层使用cas原理,也是自旋锁

利用那个cas原理,自定义一个自旋锁SpinLock类

class SpinLock {
	public AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
	public void myLock() {
		Thread currentThread = Thread.currentThread();
		System.out.println(currentThread.getName()+" ==> mylock");
		while(!atomicReference.compareAndSet(null, currentThread)) {
		}
	}
	
	public void myUnLock() {
		Thread currentThread = Thread.currentThread();
		System.out.println(currentThread.getName()+" ==> myUnLock");
		atomicReference.compareAndSet(currentThread, null);
	}
}

public static void main(String[] args) throws InterruptedException {
	SpinLock spinLock = new SpinLock();
	
	new Thread(()->{
		spinLock.myLock();
		try {
			TimeUnit.SECONDS.sleep(4);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			spinLock.myUnLock();
		}
	},"a") .start();
	
	TimeUnit.SECONDS.sleep(1);
	
	new Thread(()->{
		spinLock.myLock();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			spinLock.myUnLock();
		}
	},"b") .start();
}

//执行结果
a ==> mylock
b ==> mylock
a ==> myUnLock
b ==> myUnLock

10.死锁的解决办法

先产生一个死锁

public class Test5 {
	
	public static void main(String[] args) throws InterruptedException {
		String lockA = "lockA";
		String lockB = "lockB";
		
		new Thread(new MyThread(lockA,lockB)).start();
		new Thread(new MyThread(lockB,lockA)).start();
	}
}

class MyThread implements Runnable {
	
	String lockA;
	String lockB;
	public MyThread(String lockA, String lockB) {
		super();
		this.lockA = lockA;
		this.lockB = lockB;
	}
	
	@Override
	public void run() {
		synchronized (lockA) {
			System.out.println(Thread.currentThread().getName() + " lock: "+ lockA +" == get "+ lockB);
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			synchronized (lockB) {
				System.out.println(Thread.currentThread().getName() + " lock: "+ lockB +" == get "+ lockA);
			}
		}
	}
}

运行后,卡住不动,利用jdk/bin目录下自带的工具jps和jstack,查找死锁信息

第一步:jps -l,获取线程号7268

第二步:jstack 7268,查询该线程的具体信息

分析代码,找到死锁的原因

第三步:taskkill /im 7268 -f,杀死死锁线程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彼岸花@开

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值