美文网首页
java并发编程学习

java并发编程学习

作者: 抬头挺胸才算活着 | 来源:发表于2020-03-05 16:18 被阅读0次

    参考资料:
    Av86373641:黑马程序员 - 全面深入学习java并发编程

    • 进程和线程



    • 并行并发
      并发:同一个时间段交叉运行多个线程
      并行:同一个时间点运行多个线程

    • 线程的创建方式

    1. 覆盖run方法
    2. 编写一个Runnable接口对象然后给到Thread对象
    3. FutureTask和Callable接口(Callable比Runnable多一个返回值,并且可以抛出异常),可以返回值,FutureTask创建完也要给到Thread,然后调用Thread就可以,因为FutureTask也实现了Runnable接口。我们看下FutureTask对runnable的实现就可以知道,它是调用了Callable对象,处理了异常和返回值。
        public void run() {
            if (state != NEW ||
                !RUNNER.compareAndSet(this, null, Thread.currentThread()))
                return;
            try {
                Callable<V> c = callable;
                if (c != null && state == NEW) {
                    V result;
                    boolean ran;
                    try {
                        result = c.call();
                        ran = true;
                    } catch (Throwable ex) {
                        result = null;
                        ran = false;
                        setException(ex);
                    }
                    if (ran)
                        set(result);
                }
            } finally {
                // runner must be non-null until state is settled to
                // prevent concurrent calls to run()
                runner = null;
                // state must be re-read after nulling runner to prevent
                // leaked interrupts
                int s = state;
                if (s >= INTERRUPTING)
                    handlePossibleCancellationInterrupt(s);
            }
        }
    

    前两者推荐推荐用第二种

    方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
    用 Runnable 更容易与线程池等高级 API 配合
    用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

    // 构造方法的参数是给线程指定名字,推荐
    Thread t1 = new Thread("t1") {
    @Override
    // run 方法内实现了要执行的任务
    public void run() {
    log.debug("hello");
    }
    };
    t1.start();
    
    // 创建任务对象
    Runnable task2 = () -> log.debug("hello");
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    Thread t2 = new Thread(task2, "t2");
    t2.start();
    
    • Thread和Runnable的关系
      有了Runnable可以把线程和任务分开来,Runnable可以更容易与线程池等高级API配合,更加灵活。

    • 栈帧
      栈,每个函数有一个栈帧

    每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

    • Thread方法
      join:调用的线程等待被调用的线程运行结束,主要用于同步。
      yield:让出当前线程,从running到runnable
      interrupt:打断其他线程的运行

    如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记

    • 守护线程
      总结:依附其他现成的存在而存在,比如垃圾回收器

    默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
    护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

    • java的线程状态



      waiting是join的时候的状态
      blocked是等待锁的时候的状态

    • 临界区
      一个程序运行多个线程本身是没有问题的
      问题出在多个线程访问共享资源
      多个线程读共享资源其实也没有问题
      在多个线程对共享资源读写操作时发生指令交错,就会出现问题
      一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

    • 临界区出问题的解决方案

    阻塞式的解决方案:synchronized,Lock
    非阻塞式的解决方案:原子变量

    • synchronized的两种用法

    虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
    互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

    • synchronized用于互斥
      要注意两个都要枷锁,并且要锁住同一个对象。

    • 成员变量和静态变量是否线程安全?
      如果它们没有共享,则线程安全
      如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      如果只有读操作,则线程安全
      如果有读写操作,则这段代码是临界区,需要考虑线程安全

    • 局部变量是否线程安全?(重点关注P66)
      局部变量是线程安全的
      但局部变量引用的对象则未必
      如果该对象没有逃离方法的作用访问,它是线程安全的
      如果该对象逃离方法的作用范围,需要考虑线程安全

    • 常见线程安全类
      String
      Integer
      StringBuffer
      Random
      Vector
      Hashtable
      java.util.concurrent 包下的类

    • 线程安全类方法的组合

    Hashtable table = new Hashtable();
    // 线程1,线程2
    if( table.get("key") == null) {
    table.put("key", value);
    }
    
    • 开闭原则
      闭原则(final private)可以增加类的安全性,比如String设置为final就是。(P66)

    • monitor锁,管程/监视器


    • 锁优化(这些措施都是java虚拟机自动操作的,虽然可能可以配置)
      上面的monitor是重型锁,还有轻量锁和偏向锁。
      偏向锁:这个锁的前提是锁真的主要是其中一个线程在用。
      synchronized最开始加的是轻量级锁,后面有人来了,进行锁膨胀才加为重量级锁,锁膨胀是为了后面的线程有等待队列。
      锁膨胀:轻量级锁变为重量级锁,锁膨胀是为了后面的线程有等待队列。
      自旋优化:空转检查,避免因为进入阻塞队列带来的上下文切换,多核CPU才有用。自旋失败的时候才进行阻塞。单核的时候其实是利用任务队列当做队列,单核其实就很不好,因为自旋优化就是为了减少任务切换。
      偏向锁:轻量级锁在没有竞争的时候,每次重入仍需要执行CAS操作。在对象一开始的时候就是使用的偏向锁,如果后面有任意一次竞争或者偏向的改变,即使解锁了,重新加锁,都不是偏向锁了。或者调用wait,notify的时候也会被撤销,终身禁用。因为wait,notify只有重量级锁才有。
      批量重偏向:如果发现在t2线程内因为偏向不同而从偏向锁转向轻量级锁太多之后(即撤销偏向锁),后面把这些对象和锁都偏向于t2线程。
      批量撤销:如果撤销偏向锁的次数太多,那么后面对于同一个类的对象再也不用偏向锁了。
      锁消除:JIT即时编译器会优化,如果非必要会消除锁。

    • wait&notify
      进入synchronized代码片段之后(记住wait,notify调用的这个必要条件),调用wait的线程会进入waitset进行等待,并且放弃锁,这时处于waiting状态,等待notify/notifyAll后进入EntryList等待调度,这时处于blocked状态。这意味着A在wait之后,B进入synchronized代码,B调用notify,A还是不会马上执行,至少得等待B退出synchronized代码(或者调用wait放弃锁),因为A在EntryList等待锁,而B没有退出synchronized代码就没有释放锁。



      wait,notify都必须在sychronized里面才可以。
      两者的使用:wait表示要满足一定的条件,notify表示条件已经满足

    • wait&notify使用搭配

    synchronized(lock) {
    while(条件不成立) {
    lock.wait();
    }
    // 干活
    }
    //另一个线程
    synchronized(lock) {
    lock.notifyAll();
    }
    
    • join的实现
      join是用wait实现的,wait等待线程死掉。
        public final synchronized void join(final long millis)
        throws InterruptedException {
            if (millis > 0) {
                if (isAlive()) {
                    final long startTime = System.nanoTime();
                    long delay = millis;
                    do {
                        wait(delay);
                    } while (isAlive() && (delay = millis -
                            TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
                }
            } else if (millis == 0) {
                while (isAlive()) {
                    wait(0);
                }
            } else {
                throw new IllegalArgumentException("timeout value is negative");
            }
        }
    
    • park&unpark
      与 Object 的 wait & notify 相比
      wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
      park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll
      是唤醒所有等待线程,就不那么【精确】
      park & unpark 可以先 unpark,而 wait & notify 不能先 notify

    • ReentrantLock可重入锁



      lock:得不到锁死等。
      lockInterruptly可打断:在获取锁的时候可以打断,退出阻塞,得不到锁而返回。
      tryLock:得不到锁立即返回,或者可以设置超时时间。
      synchronized只能是死等,相当于只是lock。
      但是synchronized会自动释放锁,包括发生异常的时候。

    • Synchronized和ReentrantLock等价代码
      Synchronized{临界代码段}
      ReentrantLock lock = new ReentrantLock();
      lock.lock ();
      try {
      临界代码段
      }finally{
      lock.unlock();
      }

    • Java 内存模型
      JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
      JMM 体现在以下几个方面
      原子性 - 保证指令不会受到线程上下文切换的影响
      可见性 - 保证指令不会受 cpu 缓存的影响
      有序性 - 保证指令不会受 cpu 指令并行优化的影响

    • volatile
      保证可见性和有序性,并不能保证原子性。synchronized三者都可以。

    • volatile原理
      volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
      对 volatile 变量的写指令后会加入写屏障
      对 volatile 变量的读指令前会加入读屏障
      可见性:
      写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
      而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
      有序性:
      写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
      读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

    • happens-before
      happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

    • 无锁并发(乐观锁)
      CAS:compare and save


    • CAS
      变量必须用volatile修饰,不然不能保证获得最新的


    • 为什么无锁效率比较高
      上下文切换的损耗比较高。
      无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻,线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
      但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

    • CAS特点
      结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
      CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
      synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
      CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
      因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
      但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

    • 不可变类
      不可变类可以解决资源共享的问题

    • 内存模型


    • 可见性


    • 有序性
      指令优化的时候会进行重排,但是有些重排在多线程的情况下会出错。





    • 如何保证可见性


    • double-check locking


    • happens-before


    • 享元模式flyway
      wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects

    • 线程池继承体系
      Scheduled修饰的线程池表示这个线程池有定时执行等功能。


    • ThreadPoolExecutor参数

    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
    

    corePoolSize 核心线程数目 (最多保留的线程数)
    maximumPoolSize 最大线程数目
    keepAliveTime 生存时间 - 针对救急线程
    unit 时间单位 - 针对救急线程
    workQueue 阻塞队列
    threadFactory 线程工厂 - 可以为线程创建时起个好名字
    handler 拒绝策略

    下面几个是基于ThreadPoolExecutor的各种线程池

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

    核心线程数 == 最大线程数(没有救急线程被创建),阻塞队列是无界的,可以放任意数量的任务,适用于任务量已知,相对耗时的任务。

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

    核心线程数是0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着1. 全部都是救急线程(60s 后可以回收)2. 救急线程可以无限创建。
    队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)。但是在不超过最大线程数的情况下,每次都会新建新的线程。
    这种对比newFixedThreadPool就是另外一种极端,没有固定线程,每次需要多少就创建多少,不需要有一定容量的队列来存储任务。

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

    使用场景:希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
    区别:
    自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
    Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法。Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。

    • invoke和execute的区别
    // 执行任务
    void execute(Runnable command);
    // 提交任务 task,用返回值 Future 获得任务执行结果
    <T> Future<T> submit(Callable<T> task);
    
    • 创建线程数量多少
      简单来说,CPU密集型要创建cpu 核数 + 1个线程差不多,IO密集型要创建多一点,因为IO密集型的线程经常在IO,CPU占有率并不高,多一点CPU占有率才高。

    • 线程池任务异常

    1. 主动捕获
    2. Future
    • SynchronousQueue VS AbstractQueuedSynchronizer(AQS)
      SynchronousQueue 同步队列,放进去的时候如果没有人来取会进行阻塞,如果放进去已经有人来取了,那就不会阻塞。
      AbstractQueuedSynchronizer 简单说就是同步工具的队列
    • 任务放弃策略


    • 读写锁的示例


    • synchronize和aqs的区别


    • CountDownLatch


    • join跟CountdownLatch的区别
    1. join比较底层,CountdownLatch比较高层
    2. join必须等到线程结束的时候才可以,而CountdownLatch只需调用。
    • CopyOnWriteArrayList
      CopyOnWriteArrayList 可以实现读写并发,但是具有弱一致性,其他的并发容器一般只做到读读并发。并发高和一致性是矛盾的。

    • 线程安全类合集



      图片.png

    相关文章

      网友评论

          本文标题:java并发编程学习

          本文链接:https://www.haomeiwen.com/subject/nyxxhhtx.html