【Thread】Thread并发知识 JUC all-in-one

1 并发知识库

2 JAVA 线程实现/创建方式

  • 继承Thread类,native start()方法;
  • 实现Runnable接口(自己的类已经extends 另一个情况,new Thread());
  • ExecutorService、Callable<Class>、Future 有返回值线程(有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口)
  • 基于线程池的方式(线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销 毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池)

3 4种线程池

真正的线程池接口是 ExecutorService;

  • newCachedThreadPool(多短期异步任务的程序而言,这些线程池通常可提高程序性能;可重用;60S回收;空闲时不占任何资源(cached缓存特征:expire 60s))
  • newFixedThreadPool(创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程;执行期间异常:一个新线程将代替它执行后续的任务;一直存活)
  • newScheduledThreadPool(在给定延迟后运行命令schedule或者定期地执行scheduleAtFixedRate)
  • newSingleThreadExecutor(只有一个线程的线程池,这个线程 池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去)

 

 

4 线程生命周期(状态)

  • New (new 关键字创建一个线程后,JVM分配内存,初始化成员变量)
  • Runnable(调用start()方法之后,就绪状态,JVM创建方法调用栈和程序计数器等待调度)
  • Running(就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体)
  • Blocked(因为某种原因放弃了 cpu 使用权,1 等待堵塞:o.wait->等待对列;2 同步堵塞:线程在获取对象的同步锁时lock->锁池;3 sleep/join)
  • Dead(三种方式结束:

1 正常结束,run()或 call()方法执行完成

2 异常结束,线程抛出一个未捕获的 Exception 或 Error

3 调用 stop,直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用)

 

5 终止线程 4 种方式

 

  • 5.1 正常运行结束
  • 5.2 使用退出标志退出线程(常常有些线程是伺服线程,public volatile boolean exit = false;Java 关键字 volatile,这个关键字的目的是使 exit 同步,同一时刻只能由一个线程来修改 exit 的值?)
  • 5.3 Interrupt 方法结束线程(分两个情况:线程处于阻塞状态(sleep,同步锁的 wait,socket 中的 receiver,accept当调用线程的 interrupt()方法时,会抛出 InterruptException 异常) 和 线程未处于阻塞状态(使用 isInterrupted()判断线程的中断标志来退出循环,当使用 interrupt()方法时,中断标志就会置 true))
  • 5.4 stop 方法终止线程。
  • 5.5 stop为什么不安全?
  • 程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关 闭计算机电源, 而不是按正常程序关机一样, 可能会产生不可预料的结果, 不安全主要是: thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子 线程所持有的所有锁。 一般任何进行加锁的代码块, 都是为了保护数据的一致性, 如果在调用 thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因 此,并不推荐使用 stop 方法来终止线程。//被保护数据就有可能呈现不一致性

 

6 sleep 与 wait 区别

  • 6.1 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中.
  • 6.2 sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持,当指定的时间到了又会自动恢复运行状态。
  • 6.3 在调用 sleep()方法的过程中,线程不会释放对象锁。
  • 6.4 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态.类比 LOCK的等待队列和CLH队列;

7 start 与 run 区别

  • 7.1 start()方法来启动线程,真正实现了多线程运行。无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码;
  • 7.2 通过调用 Thread 类的 start()方法来启动一个线程(JVM创建方法调用栈和程序计数器等待调度), 这时此线程是处于就绪状态Runnable, 并没有运 行。
  • 7.3 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束线程终止。

8 JAVA 后台线程(守护线程)

  • 8.1 为用户线程 提供公共服务,在没有用户线程可服务时会自动离开。
  • 8.2 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  • 8.3 设置:通过 setDaemon(true)来设置线程为“守护线程”
  • 8.4 在 Daemon 线程中产生的新线程也是 Daemon的。
  • 8.5 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的 生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
  • 8.6 gc线程,当垃圾回收线程是 JVM 上仅剩的线 程时,垃圾回收线程会自动离开
  • 8.7 生命周期:守护线程不依赖于终端,但是依 赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退 出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

 

 

9 线程基本方法

wait,notify,notifyAll,sleep,join,yield

  • wait:调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的 是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
  • notify:Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象 上等待,则会选择唤醒其中一个线程,选择是任意的(因为WaitSet是一个Set结构),并在对实现做出决定时发生,线程通过调 用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
  • sleep:sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致 线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
  • Join: 等待其他线程终止,join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞 状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态。(很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要 在子线程结束后再结束,这时候就要用到 join() 方法。)
  • yield:yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下, 优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

interrupt:中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这 个线程本身并不会因此而改变状态(如阻塞,终止等)。

  • 1 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线 程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  • 2 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出 InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  • 3 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异 常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  • 4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止 一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以 根据 thread.isInterrupted()的值来优雅的终止线程。

 

10 线程上下文切换

利用时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存 下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做 上下文切换。

  • 进程:是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且 与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。
  • 上下文:是指某一时间点 CPU 寄存器和程序计数器的内容。
  • 寄存器:是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内 存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速 度。
  • 程序计数器:是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
  • PCB-“切换桢”:上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下 文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称 作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。

上下文切换的活动:

  • 1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
  • 2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
  • 3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序 中。

引起线程上下文切换的原因:

  • 1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
  • 2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
  • 3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
  • 4. 用户代码挂起当前任务,让出 CPU 时间;
  • 5. 硬件中断;

12 同步锁与死锁

同步锁:当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程 同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可 以使用 synchronized 关键字来取得一个对象的同步锁。

死锁:何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待所需的资源被释放。

 

13 线程池原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,实现生产者消费者模型。

他的主要特点为:

  • 线程复用;
  • 控制最大并发数;
  • 管理线程。

13.1 线程复用,重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以 是阻塞的。

13.2 线程池的组成

  • 13.2.1 线程池管理器:用于创建并管理线程池
  • 13.2.2 工作线程:线程池中的线程
  • 13.2.3 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  • 13.2.4 任务队列:用于存放待处理的任务,提供一种缓冲机制

13.3 ThreadPoolExecutor 的构造方法如下:

  • 13.3.1. corePoolSize:指定了线程池中的线程数量。
  • 13.3.2. maximumPoolSize:指定了线程池中的最大线程数量。
  • 13.3.3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多少时间内会被销毁。
  • 13.3.4. unit:keepAliveTime 的单位。
  • 13.3.5. workQueue:任务队列,被提交但尚未被执行的任务。
  • 13.3.6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  • 13.3.7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

13.4 拒绝策略

JDK 内置的拒绝策略如下

  • 1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
  • 2. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢 失,这是最好的一种方案。
  • 3. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的 任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
  • 4. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

13.5 Java 线程池工作过程

  • 1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  • 2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:

a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要 创建非核心线程立刻运行这个任务;

d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池 会抛出异常 RejectExecutionException。

  • 3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运 行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

 

14 JAVA堵塞队列原理

线程堵塞原理:

  • Blocking consumer:当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列;
  • Blocking producer:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有 空的位置,线程被自动唤醒。

14.1 主要方法:

  • add 如果调用offer返回false就抛出异常;同理remove调用poll;
  • drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

14.2 Java 中的阻塞队列

  • 1. ArrayBlockingQueue :由数组结构组成的有界阻塞队列。

队列按照先进先出(FIFO)的原则对元素进行排序,默认情况下 不保证访问者公平的访问队列;创建一个公平的阻塞队列:

fairQueue = new ArrayBlockingQueue(1000,true);

  • 2. LinkedBlockingQueue :由链表结构组成的有界阻塞队列。

两个独立锁提高并发,LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者 端和消费者端分别采用了独立的锁来控制数据同步;

  • 3. PriorityBlockingQueue :支持优先级排序的无界阻塞队列。

一个支持优先级的无界队列。compareTo 排序实现优先。需要注意的是不能保证同优先级元素的顺序(不稳定排序)。

  • 4. DelayQueue:使用优先级队列实现的无界阻塞队列。

一个支持延时获取元素的无界阻塞队列;

应用:(缓存到期,时间调度到期,时间轮?)

1 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期, 使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。

2 定 时 任 务 调 度 : 使 用 DelayQueue 保 存 当 天 将 会 执 行 的 任 务 和 执 行 时 间 , 一 旦 从 DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。

  • 5. SynchronousQueue:不存储元素的阻塞队列。

是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。

应用:

非常适合于传递性场景,比如在一个线程中使用的数据,传递给 另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和 ArrayBlockingQueue。

  • 6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。

是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。 相 对 于 其 他 阻 塞 队 列 , LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。

应用:

1 tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费 者等待接收元素,则返回 false。

2 transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的 poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素 被消费者消费了才返回。

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

双向队列指的你可以从队列的两端插入和移出元素。

应用:在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在 “工作窃取”模式中。

 

15 CyclicBarrier、CountDownLatch、Semaphore 的用法

  • CountDownLatch(线程计数器 ):等待其他任务执行完毕之后才能执行;并行子任务调用,主线程等待所有任务完成后,做合并;CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后。
  • CyclicBarrier(回环栅栏-等待至 barrier 状态再全部同时执行):让一组线程等待至某个状态之后再全部同时执行;回环:是因为当所有等待线程都被释放以后, CyclicBarrier 可以被重用。await()挂起当前线程,直至所有线程都到达 barrier 状态;await(long timeout, TimeUnit unit):让这些线程等待至一定的时间;应用:比如赛马;CyclicBarrier 一般用于一组线程互相等待至某个状态。
  • private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;
    
            if (g.broken)
                throw new BrokenBarrierException();
    
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
    
            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration(); // 唤醒所有等待线程
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }
    
            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await(); // 进入等待队列,等待被唤醒
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos); 
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }
    
                if (g.broken)
                    throw new BrokenBarrierException();
    
                if (g != generation)
                    return index;
    
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
    
    
    /**
     * Updates state on barrier trip and wakes up everyone.
     * Called only while holding lock.
     */
    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        // parties – the number of threads that must invoke await before the barrier is tripped
        count = parties;
        generation = new Generation(); // 重用
    }

     

  • Semaphore(信号量-控制同时访问的线程个数):阻塞acquire(int permits):获取 permits 个许可;阻塞release() { } :释放许可;立即得到执行结果:tryAcquire():尝试获取一个许可,tryAcquire(int permits):尝试获取 permits 个许可,tryAcquire(int permits, long timeout, TimeUnit unit): 尝试获取 permits 个许可, availablePermits()方法得到可用的许可数目。

应用:若一个工厂有 5 台机器,但是有 8 个工人,一台机器同时只能被一个工人使用,只有使用完 了,其他工人才能继续使用。那么我们就可以通过 Semaphore 来实现;Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限

 

16 volatile 关键字的作用(变量可见性、禁止重排序)

  • Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他 线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的 地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
  • 变量可见性:其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
  • 禁止重排序:volatile 禁止了指令重排。
  • 比 sychronized 更轻量级的同步锁。

在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。必须同时满足下面两个条件才能保证在并发环境的线程安 全:

(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。

(2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

17 如何在两个线程之间共享数据

  • Java 里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有三个:可见性、有序性、原子性。Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。
  • 实现方法:将数据抽象成一个类,并将数据的操作作为这个类的方法public synchronized void add(){j++;}

18  ThreadLocal 作用(线程本地存储)

  • 这种变量在线程的生命周期内起作用;
  • ThreadLocalMap(线程的一个属性)
  • 1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中, 各管各的,线程可以正确的访问到自己的对象。
  • 2. 将一个共用的 ThreadLocal 静态实例作为 key, 将不同对象的引用保存到不同线程的 ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
  • 3. ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义ThreadLocal.ThreadLocalMap threadLocals = null;

使用场景

最常见的 ThreadLocal 使用场景为 用来解决 数据库连接、Session 管理、classLoader SPI 机制等。

 

19 synchronized 和 ReentrantLock 的区别

两者的共同点:

  • 1. 都是用来协调多线程对共享对象、变量的访问
  • 2. 都是可重入锁,同一线程可以多次获得同一个锁
  • 3. 都保证了可见性和互斥性

两者的不同点:

  • 1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
  • 2. ReentrantLock 可响应中断(InterruptLock)、可轮回(重复使用),synchronized 是不可以响应中断的(遇到sychronized关键字进行排队,期间不响应中断),为处理锁的不可用性提供了更高的灵活性
  • 3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
  • 4. ReentrantLock 可以实现公平锁
  • 5. ReentrantLock 通过 Condition 可以绑定多个条件
  • 6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
  • 7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言 实现。
  • 8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁。
  • 9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时, 等待的线程会一直等待下去,不能够响应中断。
  • 10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  • 11. Lock 可以提高多个线程进行读操作的效率,ReentrantReadWriteLock既就是实现读写锁等。

20 Java 中用到的线程调度

  • 抢占式调度(左图):抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种 运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至 某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞;
  • 协同式调度(右图):指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样, 一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程 本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

20.1 JVM 的线程调度实现(抢占式调度)

  • java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高 越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

20.2 线程让出 cpu 的情况

  • 1. 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系 统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
  • 2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
  • 3. 当前运行线程结束,即运行完 run()方法里面的任务。

20.3 进程调度算法

优先调度算法:

  • 先来先服务调度算法(FCFS)(特点是:算法比较 简单,可以实现基本上的公平。)
  • 短作业(进程)优先调度算法(该算法未照顾紧迫型作业。)

高优先权优先调度算法(FPF):

  • 非抢占式优先权算法(系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下 去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中; 也可用于某些对实时性要求不严的实时系统中);
  • 抢占式优先权调度算法( 在其执行期间,只 要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程)故而常用于要求比较严格的实时系统中,以及对性能要求较高的批 处理和分时系统中。
  • 高响应比优先调度算法(为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时 间的增加而以速率 a 提高)简言之,该算法既照顾了短作业,又考虑了作业到 达的先后次序,不会使长作业长期得不到服务。该算法实现了一种较好的折衷。当然,在 利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

基于时间片的轮转调度算法:

  • 时间片轮转法;保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。
  • 多级反馈队列调度算法;
  • (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二 个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各 不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的 时间片要比第一个队列的时间片长一倍,……,第 i+1 个队列的时间片要比第 i 个队列的时间片长 一倍。
  • (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当 轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时 尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按 FCFS 原则等待调度执行;如果 它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个 长作业(进程)从第一队列依次降到第 n 队列后,在第 n 队列便采取按时间片轮转的方式运行。
  • (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第 1~(i-1)队列均空时, 才会调度第 i 队列中的进程运行。如果处理机正在第 i 队列中为某进程服务时,又有新进程进入优 先权较高的队列(第 1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即 由调度程序把正在运行的进程放回到第 i 队列的末尾,把处理机分配给新到的高优先权进程。在

多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间 时,便能够较好的满足各种类型用户的需要。

 

21 CAS(比较并交换-乐观锁机制)

  • CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作;

21.1 原子包 java.util.concurrent.atomic(锁自旋)

  • 由于一般 CPU 切 换时间比 CPU 指令集操作更加长,所以 J.U.C 在性能上有了很大的提升.如:getAndIncrement 采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行 CAS 操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成 CPU 指令的操作。

21.2 CAS 会导致“ABA 问题”

  • CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时 刻比较并替换,那么在这个时间差类会导致数据的变化。
  • 解决方案:部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修 改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本 号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问 题,因为版本号只会增加不会减少。

22 AQS(抽象的队列同步器)

  • ReentrantLock/Semaphore/CountDownLatch/CyclicBarrier(内部ReentrantLock)。
  • volatile int state(代表共享资源)和一个 FIFO 线程等待队列;
  • AQS 定义两种资源共享方式:
  • Exclusive 独占资源-ReentrantLock;
  • Share 共享资源-Semaphore/CountDownLatch;

  • 独 占 模 式 下 只 用 实 现 tryAcquire-tryRelease;线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意, 获取多少次就要释放多么次,这样才能保证 state 是能回到零态的;以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与 线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程 就会从 await()函数返回,继续后余动作。
  • ReentrantReadWriteLock 实现独占和共享两种方式:一般来说, 自定义同步器要么是独占方法,要么是共享方式, 他们也只需实现 tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器 同时实现独占和共享两种方式,如 ReentrantReadWriteLock;
  • 共 享 模 式 下 只 用 实 现 tryAcquireShared-tryReleaseShared;

具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS 已经在顶层实现好了。

 

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页