线程/并发

整理了一些知识点(不适合什么都不明白的新人)

目录

同步 异步 阻塞 非阻塞

线程的生命周期

创建线程的3种方式

并发包中的锁类

利用同步代码块

ReentrantLock锁和Synchronized锁 区别

线程同步

volatile

线程池

底层实现

五种线程池

线程池的四种工作队列

线程池的拒绝策略

ABC三个线程如何保证顺序执行


同步 异步 阻塞 非阻塞

同步 异步    与   阻塞 非阻塞  没有直接关系

同步请求:A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。

异步请求:A调用B,B的处理是异步的,B在接到请求后先告诉A我已接到请求,然后异步去处理,最后通过回调等方式再通知A。

阻塞请求:A调用B,A一直等着B的返回,别的事情什么也不干。

非阻塞请求:A调用B,A不用一直等着B的返回,先去忙别的事情了。

同步和异步的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。

阻塞和非阻塞的区别就是调用者 阻塞一直等着,非阻塞去做其他的事情。

同步、异步说的是被调用者 阻塞、非阻塞说的是调用者

参考网上 老王的烧水壶的例子 

 1、同步阻塞:

       老张在厨房用普通水壶烧水,一直在厨房等着(阻塞),盯到水烧开(同步);

 2、异步阻塞:

       老张在厨房用响水壶烧水,一直在厨房中等着(阻塞),直到水壶发出响声(异步),老张知道水烧开了;

 3、同步非阻塞(这种状态 基本上没有...):

       老张在厨房用普通水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),然后时不时去厨房看看水烧开了没(轮询);

 4、异步非阻塞:

       老张在厨房用响水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),当水壶发出响声(异步),老张就知道水烧开了;

Java中的 IO操作 分为:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)  效率由低→高

线程的生命周期

1、创建   2、就绪   3、执行   4、阻塞   5、销毁

创建线程的3种方式

  • 继承自 Thread 类 (不推荐)
  • 实现 Runnable 接口
  • 实现 Callable 接口

Callable 与 Runnable 有两点不同

  • call()获得 返回值。而 Callable 和 Future 则很好地解决了 这个问题; 
  • call()可以抛出异常。而 Runnable 只有通过 setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕捉到子线程异常。

创建线程的例子:

public class CallableDemo {

    public static void main(String[] args) {
        //推荐使用线程池
//        FutureTask futureTask = new FutureTask(new StudyCall());
//        new Thread(futureTask).start(); // 启动线程
//        Object o = null;
//        try {
//            o = futureTask.get();
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        } catch (ExecutionException e) {
//            e.printStackTrace();
//        }
//        System.out.println("-结果-  "+o.toString());

        //推荐使用线程池
        StudyCall studyCall = new StudyCall();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,2, TimeUnit.MICROSECONDS,new LinkedBlockingDeque<>());
        Future future = null;
        future = pool.submit(()->{
            try {
                System.out.println(studyCall.call());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
            pool.shutdown();

    }

}

class StudyCall implements Callable{

    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 3; i++) {
            System.out.println("i:"+i);
        }
        return "qwe";
    }

}

Java 中常用锁实现的方式有两种

  • 并发包中的锁类
  • 利用同步代码块
     

并发包中的锁类

并发包的类族中, Lock 是 JUC 包的顶层接口,它的实现逻辑并未用到 synchroniz时,而是利用了 volatile 的可见性

图为 Lock 的继承类图,ReentrantLock 对于 Lock 接口的实现主要依赖了 Sync,而 Sync 继承了AbstractQueuedSynchronizer ( AQS )
它是 JUC 包实现同步的 基础工具。在 AQS 中 , 定义了一个 volatile int state 变量作为共享资源,如果线程获 取资源失败, 贝lj进入同步 FIFO 队列中等待;如果成功获取资源就执行临界区代码。
执行完释放资源时, 会通知同步队列中的等待线程来获取资源后出队并执行。
AQS 是抽象类,内置自旋锁实现的同步队列,封装入队和出 队的操作,提供独占、共享、中断等特性的方法。AQS 的子类可以定义不同的资源实现不同性质的方法。
比如可重入锁ReentrantLock,定义 state为 0 时可以获取资源并置为 l。若已获得资源,state不断加 l ,在释放资源时 state 减 l 直至为 0 ;
CountDownLatch 初始时 定义了资源总量 state=count, countDown() 不断将 state 减 l 当 state=O 时才能获得锁 ,释放后 state 就一直为 0。
所有结程调用 await() 都不会等待,所以 CountDownLatch 是 次性的,用完后如果再想用就只能重新创建一个,如果希望循环使用 ,推荐使用基于 Reentran tLock 实现的 CyclicBarrier。
Semaphore 与 CountDownLatch 略有不同 , 同样也是定义了资源总量 state=permits,
当 state>O 时就能获得锁, 并将 state 减 l ,当 state=O 时只能等待其他线程释放锁,当释放锁时 state 加 l , 其他等待线程又能获得 这个锁。
当 Semphore 的 permits 定义为 l 时,就是互斥锁,当 permits>I 就是共享锁。

JDK8 提出了一个新的锁 StampedLock, 改进了读写锁 ReentrantReadWriteLock。 这些新增的锁相关类不断丰富了 几JC 包的内容, 降低了并发编程的难度,提高了锁 的性能和安全性。

利用同步代码块

同步代码块一般使用 Java 的 synchronized 关键字来实现,有两种方式对方法进行 加锁操作第 ,在方法签名处加 synchronized 关键字;第二,使用 synchronized(对 象或类)进行同步。这里的原则是锁的范围尽可能小,锁的时间尽可能短,即能锁对象, 就不要锁类,能锁代码块,就不要锁方法。

  1、synchronized 修饰方法 ,

  2、synchronized 代码块   ---------- 属于对象  1,2 功能一样   只不过 代码块修饰的 比较方便

  2、static synchronized 修饰方法 --------属于类

同步锁:当使用synchroinzed锁住一段的代码片段,同步监视器是java中任意的一个对象,只要保证多个线程看到的该对象是"同一个",即可保证同步块中的代码是并发安全的。

互斥锁:当使用synchroinzed锁住多段不同的代码片段,但是这些同步块使用的同步监视器对象是同一个时,那么这些代码片段之间就是互斥的。多个线程不能同时执行他们。

死锁:当多个线程都持有自己的锁,但是都等对方先释放锁时就会出现"僵持"的情况,使得所有线程进入阻塞状态。

ReentrantLock锁和Synchronized锁 区别

比较方面SynChronizedReentrantLock(实现了 Lock接口)
原始构成它是java语言的关键字,是原生语法层面的互斥,需要jvm实现它是JDK 1.5之后提供的API层面的互斥锁类
代码编写    采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全,    而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成,
灵活性锁的范围是整个方法或synchronized块部分Lock因为是方法调用,可以跨方法,灵活性更大

等待可中断    

不可中断,除非抛出异常

释放锁方式:

        1.代码执行完,正常释放锁;

        2.抛出异常,由JVM退出等待

持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待

方法:

     1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;

      2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行

是否公平锁非公平锁两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁,
条件Condition 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.
高级功能 提供很多方法用来监听当前锁的信息,如:
 getHoldCount() 
 getQueueLength()
 isFair()
isHeldByCurrentThread()
isLocked()

线程同步

volatile

volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

volatile具有可见性、有序性,不具备原子性

volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。

因为所有的操作都需要同步给内存变量, 所以 volatile 一定会使线程的执行速度变慢, 故要审慎定义和使用 volatile 属性。

public class Singleton {  

    //其中使用volatile关键字修饰可能被多个线程同时访问到的singleton
    private volatile static Singleton singleton;  

    private Singleton (){}  

    // 使用双重锁校验的形式实现单例
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

线程池

底层实现

ThreadPoolExecutor pool = new ThreadPoolExecutor(7个参数);  推荐用此方式 创建线程池

int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
  1. 参数1,corePoolSize:表示常驻核心线程数。如果等于0,则任务执行完成以后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
  2. 参数2,maximumPoolSize:表示线程池能够容纳同时执行的最大线程数。从上方示例代码中的第一处来看,必须大于或等于1.如果待执行的线程数大于此值,需要借助第五个参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize相等,即是固定大小线程池。
  3. 参数3,keepAliveTime:表示线程池中线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,直到只剩下corePoolSize个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于corePoolSize时,keepAliveTime才会起作用。但是当ThreadPoolExecutor的allowCoreThreadTimeOut变量设置为true时,核心线程超时后也会被回收。
  4. 参数4,TimeUnit:表示时间单位。KeepAliveTime的时间单位通常是TimeUnit.SECONDS。
  5. 参数5,workQueue:表示缓存队列。当请求的线程数大于corePoolSize时,线程进入BlockingQueue阻塞队列(请注意,是当corePoolSize不够用时,将任务加入缓存队列,当缓存队列也容纳不下任务时,再开辟新的线程来处理任务,直到线程数到达maximumPoolSize)。后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取,是一个生产消费模型队列。
  6. 参数6,threadFactory表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
  7. 参数7,handler表示执行拒绝策略的对象。当第五个参数workQueue的任务缓存区到达上限后,并且活动线程数等于maximumPoolSize的时候,线程池通过该策略处理请求,这是一种简单的限流保护

五种线程池

Executors(Java提供用来创建线程池的类,在JUC包下)的核心方法有五个(这五个方法每个都有多种重载方法,我只选取了一种):

1、Executor.newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU数量设置为默认的并行度。

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

2、Executors.newCachedThreadPool:maximumPoolSize最大可以至Integer.MAX_VALUE,是高度可伸缩的线程池,如果达到这个上限,相信没有任何服务器能够继续工作,肯定会抛出OOM异常。keepAliveTime默认60秒,工作线程处于空闲状态,则回收工作线程。如果任务数增加,再次创建出新线程处理任务。

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

3、Executors.newScheduledThreadPool:线程数最大值Integer.MAX_VALUE,与上述相同,存在OOM风险。它是ScheduledExecutorService接口家族的实现类,支持定时及周期性任务执行。相比Timer,ScheduledExecutorService更安全,功能更加强大,与newCachedThreadPool的区别是不回收工作线程。

     public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

4、Executors.newSingleThreadExecutor:创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。

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

5、Executors.newFixedThreadPool:输入的参数即是固定线程数,即是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime等于0:

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

//这里输入的队列没有指明长度,看一下LinkedBlockingQueue的构造方法

public LinkedBlockingQueue(){
    this(Integer.MAX_VALUE);
}

//使用这样的无界队列,如果瞬间请求量很大的话,会有OOM风险
 

线程池的四种工作队列

1、ArrayBlockingQueue

是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

2、LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列

3、SynchronousQueue

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

4、PriorityBlockingQueue

一个具有优先级的无限阻塞队列。

线程池的拒绝策略

    当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。RejectedExecutionHandler接口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。

  1. AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
  2. CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
  3. DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
  4. DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。

    除了JDK默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。

 

执行流程:当线程数小于corePoolSize时,每添加一个任务,则立即开启线程执行;当corePoolSize满的时候,后面添加的任务将放入缓冲队列workQueue等待;当workQueue满的时候,看是否超过maximumPoolSize线程数,如果超过,则拒绝执行,如果没有超过,则创建线程理解执行;

 

ABC三个线程如何保证顺序执行

  • 用Thread.join() 方法,或者线程池newSingleThreadExecutor(原理是会将所有线程放入一个队列,而队列则保证了FIFO)
  • 也可以通过ReentrantLock,state整数用阿里判断轮到谁来执行
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值