JUC多线程并发

一、简介

JUC(java.util.concurrent),Java并发编程工具类。

二、LockSupport 线程阻塞工具类

简单介绍
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),
permit只有两个值1和0,默认是0。许可的累加上限是1。
几种唤醒方式区别
1.wait/notify
	a.需要在被synchronized修饰的代码块中使用。
	b.必须先notify()wait(),否则会造成无法唤醒。
	c.notify()随机唤醒一个其他线程,notifyAll()唤醒全部其他线程。

2.Condition接口:await/signal
	a.精准唤醒lock锁对象。
	b.必须先await()signal()3.LockSupport类:park/unpark
	a.unpark(Thread t)实现精准唤醒线程。
	b.permit默认0park()会需消耗permit:1->0,若permit是0则阻塞,1消费;
  	  uppark()会使prmit+1,但permit最大为1,不会累加。
	c.由于b.的机制,所以此种不会受限必须先阻塞才能唤醒,可以颠倒,先唤醒。

三、AQS (AbstractQueuedSynchronizer)

简单介绍
用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态。
底部阻塞基于LockSupport实现。
资源模式
AQS定义两种资源共享方式:
1.Exclusive(独占):个线程能执行,如 ReentrantLock。
    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
    
2.Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。
    以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
内部属性
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //头结点
	private transient volatile Node head;
	//尾节点
    private transient volatile Node tail;
 	//状态:0.未锁定 1.需要阻塞 >1.可重入(获取锁的当前线程可重复获取此锁,每次state+1)
    private volatile int state;
    
	static final class Node {
        //共享资源方式:共享线程
        static final Node SHARED = new Node();
        //共享资源方式:独占线程
        static final Node EXCLUSIVE = null;
        //资源共享节点类型
        Node nextWaiter;
        
        //线程获取锁的请求被取消,
        static final int CANCELLED =  1;
        //后继结点在等待当前结点唤醒
        static final int SIGNAL = -1;
        //结点等待在Condition上,当其他线程调用Condition的signal()后,该结点将从等待队列转移到同步队列中
        static final int CONDITION = -2;
        //共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点
        static final int PROPAGATE = -3;
        //上述等待状态标志:默认0:新结点入队时的默认状态
        //负值表示结点处于有效等待状态,正值表示结点已被取消。所以源码中很多地方用>0、<0判断结点的状态
        volatile int waitStatus;
        //前继节点
        volatile Node prev;
        //后继节点
        volatile Node next;
        //当前线程
        volatile Thread thread;
   }
}
结构图

在这里插入图片描述

三、Lock 锁

1.创建:
	Lock lock = new ReentrantLock();


2.锁方式:
	加锁:lock.lock();
	解锁:lock.unlock();
	**注意**:必须成对出现,加了锁必须解锁,否则会造成一直不释放锁,死锁。


3.锁机制(默认非公平锁):	


	公平锁:十分公平,必须先来后到。
	非公平锁:不公平,可以不遵守先来后到,插队。
	获取锁的线程会进入一个等待队列,非公平的会和第一个等待的竞争获取锁,若未获取到则进入等待队列,乖乖等锁


4.实现代码:
class Ticket2 {
    // 属性、方法
    private int number = 30;
    Lock lock = new ReentrantLock();
    public void sale(){
        lock.lock(); // 加锁
        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 可以判断是否获取到了锁 ;
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁 ;
4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,默认非公平(可以设置);
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

**注意:**notify() 和 wait() 方法必须搭配 Synchronized 使用,否则会报 IllegalMonitorStateException异常
详细见 CSDN --> 线程

四、Lock 的精准唤醒线程

private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();		// 监视器
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();

public viod add(){
	try{
		lock.lock();
		//业务代码
		condition1.await();
		condition2.signal();
	}catch(Exception e){
		//...
	}finally{
		lock.unlock();
	}
}
public viod decrement(){
	try{
		lock.lock();
		//业务代码
		condition2.await();
		condition3.signal();
	}catch(Exception e){
		//...
	}finally{
		lock.unlock();
	}
}

五、静态锁和实例锁的区别

静态锁: 在静态方法前面加上 synchronized 方法表示锁定此类,当多个线程调用这个类中的静态方法时会阻塞。
实例锁: 在实例方法前面加上 synchronized 方法表示锁定类的单个实例,当多个线程调用一个类申明的同步实例的实					例方法时会阻塞。
详情见:CSDN --> 线程

六、ArrayList 的线程不安全问题的解决

1.加 synchronized 关键字
2.使用 Vector vector = new Vector();
3.List list = Collections.SynchronizedList(new ArrayList());
4.List list = new CopyOnWriteArrayList<>();
	读不加锁,写的时候加锁,读写分离,写入时会先复制一份新数组,完成增删改在将新数组引用赋值给原对象。
	并且它的数组有 volatile 修饰,保证在增删改后其他读操作的可见性,确保数据一致。
VectorSynchronizedList 区别:
1.SynchronizedList锁的是代码块,可以指定锁的对象(默认this)。
  Vector锁的是方法,锁的对象就是this2.SynchronizedList可以把List的实现类变成线程安全的(传入实现类对象作为参数),而不改变集合结构
  例:LinkedList list = Collections.SynchronizedList(new LinkedList());
//(详情见 CSDN --> 线程)

七、set 和 map 的线程安全方式

1.前三种同上
2.Set set = new CopyOnWriteArraySet<>();
   Map map = new ConcurrentHashMap<>();(1.7利用了分段锁,详情见 CSDN --> 线程)

八、Callable 创建线程

1.与 Runnable 区别:
	a.可以有返回值
	b.可以抛出异常
	c.方法不同【run() / call() 】


2.创建方式(需要中间类 FutureTask)
方式一:实现 Callable 接口
class MyThread implements Callable<Integer> {
    @Override
    public Integer call() {
    	System.out.println("call()"); // 会打印几个call
   		// 耗时的操作
   		return 1024;
}
public class CallableTest {
    public static void main(String[] args) throws ExecutionException,
    InterruptedException {
        // new Thread(new FutureTask<V>( Callable )).start();       
        MyThread thread = new MyThread();
        FutureTask futureTask = new FutureTask(thread); // 适配类
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start(); // 结果会被缓存,效率高
        Integer o = (Integer) futureTask.get(); //这个get 方法可能会产生阻塞!把他放到最后
        // 或者使用异步通信来处理!
        System.out.println(o);
	}
}

方式二:匿名内部类
public static void main(String[] args) throws ExecutionException, InterruptedException {
       FutureTask futureTask = new FutureTask<String>(new Callable<String>() {
           @Override
           public String call() throws Exception {
               System.out.println("call()");
               return "aaaaaa";
           }
       });
       new Thread(futureTask).start();
       Object o = futureTask.get();
       System.out.println(o);
    }
方式三:lamada表达式
    FutureTask futureTask = new FutureTask<String>(()-> {
           System.out.println("aa");
           return null;
       }).run();
3.FutureTask
	a.可以包装 Runnable 和 Callable ,由构造函数方式注入参数依赖
	b.包装的 Runnable 会有两个参数,一个Runnable,一个结果类型,其实就是将 Runnable 转成 Callable
	c.FutureTask 会有缓存,第二次执行相同方法就会直接得到结果
	d.FutureTask.get() 方法会导致阻塞
	详细见 CSDN --> 线程

九、常用辅助类

1.CountDownLatch(减法计数锁)
public static void main(String[] args) throws InterruptedException {
    // 总数是6,必须要执行任务的时候,再使用!
    CountDownLatch countDownLatch = new CountDownLatch(6);//定义计数器为6
    for (int i = 1; i <=6 ; i++) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" Go out");
            countDownLatch.countDown(); // 执行此方法,计数器-1
            },String.valueOf(i)).start();
        }
    countDownLatch.await(); // 等待计数器归零,然后再向下执行
    System.out.println("Close Door");
}
2.CyclicBarrier(循环屏障)
	(1).执行流程
		a.实例化时传入参数计数器大小
		b.每执行一次 await();计数器减 1,并使该线程进入阻塞队列
		c.当计数器归 0 时,屏障消失,先执行参数线程,阻塞队列线程继续执行,计数器重新计数,回归起初定义值
		d.可以循环下个线程继续


	(2).作用:
		每当子线程执行 await() 方法后就会进入阻塞队列,当计数器归零时才被释放。(阻塞队列线程执行顺序随机)
public static void main(String[] args) throws ExecutionException, InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(6,()->{
            System.out.println("执行完毕");
        });
        for (int i = 0; i <6 ; i++) {
            new Thread(()->{
                System.out.println("开始");
                try {
                    barrier.await();
                    System.out.println("结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        for (int i = 0; i <6 ; i++) {
            new Thread(()->{
                System.out.println("开始");
                try {
                    barrier.await();
                    System.out.println("结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
3.Semaphore(信号量)
	(1).创建方式:
		a.Semaphore(int permits,boolean fair); 第一个参数指信号量;第二个参数用于指定公平或非公平
		b.Semaphore(int permits);默认非公平
	(2).方法:
		semaphore.acquire(); 信号量+1,如果已经满了,等待,等待被释放为止!
		semaphore.release(); 释放,信号量-1,等待线程可获得许可证(信号量+1)
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
    new Thread(()->{
        try {
            semaphore.acquire();
            System.out.println("获得了"+Thread.currentThread().getName());
            Thread.sleep(2000);
            System.out.println("离开了"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }finally {
        	semaphore.release();
    	}
	}).start();
}

十、读写锁(ReadWriteLock)

1.创建:
	ReadWriteLock lock = new ReentrantReadWriteLock();
2.使用:
	a.读锁:lock.readLock().lock();lock.readLock().unlock();
	b.写锁:lock.writeLock().lock();lock.writeLock().unlock();
注意:读锁的作用是为了防止在读的时候写入。
	Map<Integer, String> map = new HashMap<>();
    ReadWriteLock lock = new ReentrantReadWriteLock();
    public void get(int key) {
        lock.readLock().lock();
        try {
            String s = map.get(key);
            System.out.println(s);
        } finally {
            lock.readLock().unlock();
        }
    }
    public void put(int key, String value) {
        lock.writeLock().lock();
        try {
            map.put(key, value);
            System.out.println(map.toString());
        } finally {
            lock.writeLock().unlock();
        }
    }

十一、阻塞队列

非阻塞队列:通常不会对队列设置容量限制,如 LinkedList
阻塞队列:通常对队列进行容量设置,队列满了/空了 可以阻塞等待
非同步队列
1.创建方式:
	例:BlockingQueue queue = new ArrayBlockingQueue(3);


2.普通方法:
//公共方法
peek():返回队首元素,不移除

// 四组API
1.add()/remove()
    add():添加,队列满了抛出异常
    remove():移除队首元素并返回,队列空了就抛异常
    
2.offer()/poll()/peek()
    offer():添加,满了不抛异常
    poll():移除队首元素并返回,不抛异常

3.offer(e,time,unit)/poll(time,unit)
    offer(e,time,unit):同上,不过指定了阻塞时间
    poll(time,unit):同上,不过指定了阻塞时间
    
4.put()/take()
    put():添加,满了就一直阻塞
    take():移除队首元素并返回,队列为空一直阻
3.线程方法:
	BlockingQueue queue = new ArrayBlockingQueue(3);
        new Thread(()->{
            try {
                //添加一个元素,队列满了就会进入等待,直到队列有空闲在执行
                //多线程下,会有原子性问题,抛出异常
                //无返回值
                queue.put("a");
                System.out.println("增加了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                //取出一个元素,队列空了就进入等待状态,直到队列有元素在执行取出
                //多线程下,会有原子性问题,抛出异常
                //有返回值,返回取出元素
                Object take = queue.take();
                System.out.println("移除了"+take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                //取出一个元素,队列空了就进入超时等待状态,超时过程中有元素执行取出,没有元素返回null
                //多线程下,会有原子性问题,抛出异常
                //有返回值,返回取出元素,超时未取出元素返回null
                Object take = queue.poll(2,TimeUnit.SECONDS);
                System.out.println("移除了"+take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                //添加一个元素,队列满了就会进入超时等待,超时过程中队列有空闲执行添加,没有就返回false
                //多线程下,会有原子性问题,抛出异常
                //返回值boolean类型
                Thread.sleep(2000);
                boolean a = queue.offer("a", 1, TimeUnit.SECONDS);
                System.out.println(a);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
同步队列(SynchronousQueue)
1.创建方式:(没有容量)
	BlockingQueue queue = new SynchronousQueue<>();
2.特点:
	不作为存储,添加一个元素后,必须等待取出,才能添加下一个元素。
3.方法:
//添加一个元素,再次使用必须等待取出,才能添加下一个元素。
//无返回值
queue.put("a");
//添加一个元素,队列满了就会进入超时等待,超时过程中队列有空闲执行添加,没有就返回false
//返回值boolean类型
boolean a = queue.offer("a", 1, TimeUnit.SECONDS);
//取出一个元素,再次使用必须等待添加,直到队列有元素在执行取出
//有返回值,返回取出元素
Object take = queue.take();
//取出一个元素,队列空了就进入超时等待状态,超时过程中有元素执行取出,没有元素返回null
//有返回值,返回取出元素,超时未取出元素返回null
Object take = queue.poll(2,TimeUnit.SECONDS);

十二、线程池(重点)

1.线程池的优点:
	a.避免线程的频繁创建、销毁,降低效率,浪费资源。
	b.对线程数目进行管理,对线程进行统一管理。(控制最大并发数、管理线程)
	c.提高效率。
  主要缺点:不易定义初始池中线程数目。

在这里插入图片描述

 七大参数:
	1、corePoolSize:核心线程数
        * 核心线程会一直存活,即使没有任务需要执行
        * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
        * 设置 threadPool.allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

    2、maxPoolSize:最大线程数
        * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
        * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

    3、keepAliveTime:线程空闲时间
        * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        * 如果allowCoreThreadTimeout=true,则会直到线程数量=0
        
        
    4TimeUnit unit:keepAliveTime的时间单位

    5、 queueCapacity:任务队列容量(阻塞队列)
        * 当核心线程数达到最大时,新任务会放在队列中排队等待执行

    6ThreadFactory threadFactory: 线程工厂
        *创建线程的,一般不用动(Executors.defaultThreadFactory())

    7、rejectedExecutionHandler:任务拒绝处理器
        * 两种情况会拒绝处理任务:
            - 当线程数已经达到maxPoolSize,并且队列已满,会拒绝新任务
            - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和				线程池真正shutdown之间提交任务,会拒绝新任务
        * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
        * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
            - AbortPolicy 丢弃任务,抛运行时异常
            - CallerRunsPolicy 执行任务
            - DiscardPolicy 忽视,什么都不会发生
            - DiscardOldestPolicy 从队列中踢出最先进入队列的任务
        * 实现RejectedExecutionHandler接口,可自定义处理器

3.四大拒绝策略

1.AbortPolicy:
  new ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐   使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
2.DiscardPolicy:
  new ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢   弃,且是静默丢弃。
  使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计   阅读量就是采用的这种拒绝策略。
3.DiscardOldestPolicy:
  new ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
  此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
4.CallerRunsPolicy:
  new ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
  如果任务被拒绝了,则由调用线程(提交任务的线程/通常是主线程)直接执行此任务。

4.ThreadPoolExecutor方法

1.threadPool.allowCoreThreadTimeOut(true);
  //设置核心线程空闲超时自动关闭,不设置默认为false
2.threadPool.submit():执行任务
  //三种参数:a.Runnable		b.Callable		c.Runnable,result T
  //即可以抛出异常和有返回值,Runnable 和 Callable 的关系
3.threadPool.execute():执行任务
  //参数:Runnable
  //无返回值
  1. execute() 方法执行流程
1.判断工作线程数目是否小于 corePoolSize
2.小于核心线程数目时创先核心线程
3.如果工作线程数目大于 corePoolSize,但队列未满,则将任务放到队列中,遵循FIFO原则依次等待有线程空闲来执行
4.如果工作线程数目大于 corePoolSize,但队列已满,但线程池中的线程数量小于 maxPoolSize,则会创建新的线程来处理被添加的任务
5.如果线程池中的线程数量等于了maxPoolSize,再来任务就用RejectedExecutionHandler来做拒绝处理
6.当线程状态是非RUNNING时,就会将加入到队列的任务移除,如果移除失败,则会判断是否工作线程是否为0,为0就会创建一个非核心线程去消费队列;如果移除成功就会为再来的任务执行拒绝策略。

** 6. 四大策略代码**

1.AbortPolicy:
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 5,
         1, TimeUnit.SECONDS,
         new ArrayBlockingQueue<>(2),
         Executors.defaultThreadFactory(),
         new ThreadPoolExecutor.AbortPolicy());
    threadPool.allowCoreThreadTimeOut(true);
    try {
        for (int i = 0; i <8 ; i++) {
            final int a=i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+":"+a);
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    //未设置threadPool.allowCoreThreadTimeOut(true);则需要手动关闭线程池
    //        } finally {
    //            threadPool.shutdown();
    //        }
2.DiscardPolicy:同上,只是不抛出异常,适合一些无关紧要的业务
3.DiscardOldestPolicy:丢弃队列最前面的任务,提交被拒绝的任务(喜新厌旧型)
4.CallerRunsPolicy:如果任务被拒绝了,则由调用线程(提交任务的线程/通常是主线程)直接执行此任务。
    //执行结果:
    pool-1-thread-23
	pool-1-thread-45
	pool-1-thread-34
	main:7
	pool-1-thread-10
	pool-1-thread-32
	pool-1-thread-21
	pool-1-thread-56
  1. Executors 方式创建线程池
//缓存线程池,可伸缩
ExecutorService threadPool1 = Executors.newCachedThreadPool();
//固定线程池
ExecutorService threadPool2 = Executors.newFixedThreadPool(5);
//固定核心线程池
ScheduledExecutorService threadPool3 = Executors.newScheduledThreadPool(2);
//单一线程池,只有一个线程的池
ExecutorService threadPool4 = Executors.newSingleThreadExecutor();
  1. 最大线程数的设置
(1). CPU 密集型:
		cpu使用率较高(也就是一些复杂运算,逻辑处理),该类型的任务需要进行大量的计算,主要消耗CPU资源,所以线程数一般需要等于cpu核心线程数,以保证CPU的执行效率。 这一类型的在开发中多出现的一些业务复杂计算和逻辑处理过程中。
(2). I/O 密集型
		cpu使用率较低,程序中会存在大量I/O操作占据时间,计算机在执行IO操作时是将任务交给DMA(负责内存分配的),这样会使线程一直被占用,所以通常就需要开cpu核心线程数两倍的线程,当线程进行I/O操作cpu空暇时启用其他线程继续使用cpu,提高cpu使用率 。
//获取电脑CPU进程数目 Runtime.getRuntime().availableProcessors()
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
         Runtime.getRuntime().availableProcessors(),
         1, TimeUnit.SECONDS,
         new ArrayBlockingQueue<>(2),
         Executors.defaultThreadFactory(),
         new ThreadPoolExecutor.AbortPolicy());
threadPool.allowCoreThreadTimeOut(true);

十三、ForkJoin(多线并发处理框架,大数据处理)

概念:采用分而治之的思想。就是将一个复杂的计算,按照设定的阈值分解成多个计算,再将各个计算结果进行汇总。


1.创建方式:
	使用ForkJoin框架,需要创建一个ForkJoin的任务,而ForkJoinTask是一个抽象类,我们不需要去继承ForkJoinTask		进行使用。因为ForkJoin框架为我们提供了 RecursiveAction 和 RecursiveTask。我们只需要继承ForkJoin为我们提		供的抽象类的其中一个并且实现compute方法。


2.执行:(工作窃取,双端队列)
	task要通过ForkJoinPool来执行,分割的子任务也会添加到当前工作线程的双端队列中,
	进入队列的头部。当一个工作线程中没有任务时,会从其他工作线程的队列尾部获取一个任务(工作窃取)。


3.RecursiveTask 和 RecursiveAction 区别
	RecursiveTask 在执行 execute() 方法后会有设置泛型的返回结果类型,RecursiveAction 无返回结果。
public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        //ForkJoin方式 900毫秒
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L, 100000000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long sum = submit.get();
        //简单for循环 1300毫秒
        Long sum=0L;
        for (Long i = 0L; i <=100000000L ; i++) {
            sum+=i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum="+sum+",执行了:"+(end-start));
    }
}
class ForkJoinDemo extends RecursiveTask<Long> {
    private Long start;
    private Long end;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }
    // 计算方法
    @Override
    protected Long compute() {
        if ((end-start)<10000L){
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }else { // fork 递归
            long middle = (start + end) / 2; // 中间值
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork(); // 拆分任务,把任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);
            task2.fork(); // 拆分任务,把任务压入线程队列
            return task1.join()+task2.join();
        }
    }
}

十四、异步回调(CompletableFuture)CSDN --> 线程

十五、volatile 关键字

简介:java虚拟机提供的轻量级同步机制


线程内存图:

![image-20201126211523022.png](https://img-blog.csdnimg.cn/img_convert/75baacdff98a3dd12c49d6d9295fa767.png#clientId=uacea15c8-2fd2-4&from=ui&id=evIvg&margin=[object Object]&name=image-20201126211523022.png&originHeight=606&originWidth=1139&originalType=binary&ratio=1&size=48574&status=done&style=none&taskId=u29c35b98-8476-4dc9-8533-ea9ccb22c1f)

1.主内存和线程工作内存交互的8种操作
1.lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
2.unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量
  才可以被其他线程锁定
3.read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便
  随后的load动作使用
4.load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
5.use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机
  遇到一个需要使用到变量的值,就会使用到这个指令
6.assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变
  量副本中
7.store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,
  以便后续的write使用
8.write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内
  存的变量中
2.JMM对8种操作的规则
JMM : Java内存模型,不存在的东西,概念! 
		关于JMM的一些同步的约定: 
		a.线程解锁前,必须把共享变量立刻刷回主存。 
		b.线程加锁前,必须读取主存中的最新值到工作内存中!
		c.加锁和解锁是同一把锁
1.不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
2.不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实
  施use、store操作之前,必须经过assign和load操作
5.一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
6.如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,
  必须重新load或assign操作初始化变量的值
7.如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  对一个变量进行unlock操作之前,必须把此变量同步回主内存
3.并发编程3大概念:原子性,有序性,可见性。
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
有序性:即程序执行的顺序按照代码的先后顺序执行。(jvm会对一些代码进行重排序优化,但不会影响结果)
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
4.volatile作用(作用在类的成员变量):
1.保证了修饰变量的可见性,即修饰变量在更改后会立马刷新主存的变量,这时候其他处理器的缓存还是旧的,所以			在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查自己的缓存			是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处		理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。
2.保证了有序性,即在重排序优化时,不会对volatile修饰变量相关操作语句进行重排序。
  通过在修饰变量前后添加内存屏障方式,禁止该变量前后代码重排序。
3.不能保证原子性。
5.volatile不保证原子性示例
    private static volatile int num = 0;
    public  void add(){
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile aVolatile = new TestVolatile();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    aVolatile.add();
                }
            }).start();
        }
        while (Thread.activeCount()>2){ //必定会有两个线程 main gc,这里是为了防止主线程先执行了输出语句
	
        }
        System.out.println(num);//结果总是小于20000
    }
	//原因:num++ 并不是一个原子性操作,分为3步
	1.读取num的初始值	  2.执行num+1操作 	3.写入内存(给num赋新值)
5.使用原子类(内部通过 CAS 实现)
    private volatile static AtomicInteger num = new AtomicInteger();
    public void add(){
        num.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile aVolatile = new TestVolatile();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    aVolatile.add();
                }
            }).start();
        }
        while (Thread.activeCount()>2){ // main gc

        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
6.使用条件:
1.对变量的写操作不依赖于当前值。
2.该变量没有包含在具有其他变量的不变式中

十六、CAS算法(比较交换,CompareAndSwap)

1.简述:(类似乐观锁)
	CAS 有三个参数:V,A,B。内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做并返回 false。在多线程的情况下,当多个线程同时使用CAS操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。CAS是基于硬件级别的操作,所以效率会高一些。


2.CAS的缺点
1.循环时间长开销大导致的性能问题,我们使用时大部分时间使用的是 while(true)方式对数据的修改,直到成功为止。优势	就是响应极快,但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。
2.ABA的问题:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上   却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么ABA 就会变成1A-2B-3A。也可以使用携带类似时间戳的版本AtomicStampedReference
3.Unsafe类
Java 无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过JVM 还是开了一个后门,JDK 中有一个类 Unsafe,它提供了硬件级别的原子操作。这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,对于 Unsafe 类的使用是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。


4.通过原子类简单的演示
public static void main(String[] args) {
    AtomicInteger num = new AtomicInteger(20);
    boolean b = num.compareAndSet(20, 21);
    System.out.println(b);
    System.out.println(num.get());
}
5.解决ABA问题(原子引用,乐观锁)
AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(1,1);
    new Thread(()->{
        int stamp = reference.getStamp();	// 获得的是版本号,stamp:印记
        System.out.println(stamp);
        try {
        	Thread.sleep(1000);
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
        boolean b = reference.compareAndSet(1, 2, stamp, stamp + 1);
        System.out.println(b+""+reference.getStamp());	// 获得的是版本号
    }).start();
}

十七、各种锁的理解

1.公平锁,非公平锁
	new ReentrantLock(boolean fair);传入参数设置(不传默认非公平)


2.可重入锁(拿到外面的锁,自动可以拿到里面的锁)
public class TestReLock {
    public synchronized void call() throws InterruptedException {
        Thread.sleep(5000);
        this.find();
        System.out.println("打电话");
    }
    public synchronized void find(){
        System.out.println("找联系人");
    }

    public static void main(String[] args) {
        TestReLock testReLock = new TestReLock();
        new Thread(()->{
            System.out.println("执行call");
            try {
                testReLock.call();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(()->{
            testReLock.find();
        }).start();
    }
}
3.自旋锁(原子类内CAS就采用此方法)
public class TestSpinLock {
    AtomicReference<Thread> atomicReference = 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 static void main(String[] args) throws InterruptedException {
        TestSpinLock lock = new TestSpinLock();
        new Thread(()-> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"T1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()-> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        },"T2").start();
    }
}
4.死锁
	概念:
	例:A线程持有X对象锁,B线程持有Y对象锁,现在A想要Y对象,且不释放自己所持对象锁,B想要X对象,也不释放所持对象锁;这样就造成双方都进入阻塞状态,且一直处于该状态。

十八、jstack日志分析和问题排查

简介
jstack用于生成java虚拟机当前时刻的线程快照。
	线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
语法格式
jstack <pid>		# 可以使用 '>' or '>>' 将输出内容存到文件。

1. 不同的 JAVA虚机的线程 DUMP的创建方法和文件格式是不一样的,不同的 JVM版本, dump信息也有差别。
2. 在实际运行中,往往一次 dump的信息,还不足以确认问题。建议产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性
DUMP 文件的线程状态
死锁, Deadlock(重点关注) 

执行中,Runnable   

等待资源, Waiting on condition(重点关注) 

等待获取监视器, Waiting on monitor entry(重点关注)

暂停,Suspended

对象等待中,Object.wait() 或 TIMED_WAITING

阻塞, Blocked(重点关注)  

停止,Parked

其他

final 全面了解
final 基础
final 修饰变量:
	1.基础变量:int,long等,表示这个变量不可修改,修饰的变量将不会存在默认初始值,需要自己定义初始值。可以在声明时/构造函数内定义初始值。
	2.对象变量:表示这个变量引用指向的对象地址不可修改,但内容可以修改。可以在声明时/构造函数内定义初始值。
	3.静态变量:同上,但是需在静态代码块或者声明时定义初始值。

final 修饰方法/类:
    1.类:表示这个类不能被继承。
    2.方法:表示这个方法不能被重写。
public class TestFinal {
    private final Set<Integer> ids = new HashSet<>();
    private final Integer age = 15;
    private final boolean sex;
    private static final String name;

    static {
        name = "123";
    }

    public TestFinal(){
        ids.add(1);
        ids.add(2);
        ids.add(3);
        sex = true;
    }

    public static void main(String[] args) {
        TestFinal aFinal = new TestFinal();
        aFinal.ids.add(5);
        Set<Integer> set = new HashSet<>();
        System.out.println(aFinal.ids);
    }
}
final 对多线程的帮助
final修饰的变量会被保证防止重排序,这使得final修饰变量具有安全发布,即被final修饰的变量必须在构造函数或者声明时就被初始化,防止了对象在构建中被访问出现不一致的情况。final修饰的变量若是可变对象在多线程下依旧存在安全问题,需要使用一些同步手段。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值