多线程编程

一、 基础概念

1.1 进程和线程

  进程是程序资源分配的最小单位
  进程是操作系统资源分配的最小单位,资源包括:CPU、内存、磁盘IO等。同一进程中的线程共享该进程中的所有系统资源。进程分为系统进程和用户进程。

  线程是CPU调度的最小单位
  线程是进程中的一个实体,必须依赖进程而存在,是CPU调度和分派的基本单位。

1.2 并发和并行

  并发
  指应用能够交替执行不同任务。

  并行
  指应用能够同时执行不同任务。

1.3 并发编程的意义和注意事项

   意义

充分利用CPU资源
加快用户响应时间
代码模块化、异步化和简单化

   注意事项

线程安全
死锁
线程太多会耗尽服务器资源导致宕机。

二、线程基础

2.1 线程的实现

  第一种方式,实现 Runnable 接口

public class MyTread implements Runnable {
    
    @Override
    public void run() {
        //do something
    }
}

  第二种方式,继承 Thread 类

public class MyTread extends Thread {
    
    @Override
    public void run() {
       //do something
    }
}

2.2 理解线程的中断

  安全的中止则是其他线程调用某个线程的interrupt方法对其进行中断操作,但并不代表被中断的线程会立即停止工作。
  线程通过isInterrupted方法或Thread.interrupter方法判断是否被中断。其中Thread.interrupter方法会将标志位改为false。如果线程在阻塞状态(sleep、wait、join)检查中断标志位时,如果标志位为true,则会在阻塞方法调用出抛出异常,并将标志位设为false。

2.3 线程再认识

2.3.1 线程的方法

  start:线程进入就绪状态等待分配CPU资源。
  run:线程的业务方法,分配到CPU资源后调用。
  sleep:线程休眠指定时间,不会释放锁资源。
  yield:使当前线程暂时放弃CPU占用权,但不会释放锁资源。
  wait:在线程中调用某个对象的该方法,线程会进入WATING状态,并且会释放对象的锁。
  notify/notifyAll:通知一个/所有在对象上等待的线程,使其从wait方法返回。返回的其他是该线程获得了对象的锁。
  join:把指定线程加入到当前线程中,将两个交替执行的线程合并为顺序执行。

  废弃方法
  suspend、resume、stop:暂停、恢复、停止线程,线程不会释放占有的资源(锁等)。

2.3.2 线程的状态

  新建、开启、执行、阻塞、销毁。

2.3.3 线程的优先级

  通过一个int型变量priority来控制优先级,范围1到10,在线程新建的时候可以调用setPriority方法设置优先级 ,默认为5。
  优先级高的线程分配时间片的数量多于优先级低的线程。

 Thread tread = new Thread(new MyTread());
 tread.setPriority(10);//线程开启前设置优先级
 tread.start();

2.3.4 守护线程

  守护线程是一种支持线程,主要负责后台调度等工作,可以在线程start之前调用setDaemon(true)方法将线程设置为守护线程。
  垃圾回收线程是守护线程。

Thread tread = new Thread(new MyTread());
tread.setDaemon(true);
tread.start();

2.4 线程间的共享

2.4.1 synchronized

  修饰方法或以同步块的方式使用,来确保同一时刻只有一个线程进入方法或同步块中。保证了线程对变量访问的可见性和排它性。
  synchronized是隐式的、可重入锁。

  三种加锁方式
  1) 同步块
  2) 修饰静态方法
  3) 修饰实例方法

  类锁相互排斥,对象锁互不干扰。类锁和对象锁也互不干扰。

  synchronized 实现原理
  synchronized语句块在编译成class文件后会在同步块起始位置插入moniterenter指令,在同步块结束位置插入moniterexit指令。在执行moniterenter指令时,首先尝试获取monitor对象的所有权,如果获取成功或者当前线程已拥有monitor,计数器加1;在执行moniterexit指令时计数器减1,当为0时就会释放锁。如果获取失败,则线程进入阻塞状态,直到计数器为0再尝试获取monitor对象。
  synchronized修饰方法时, 其常量池中多了 ACC_SYNCHRONIZED 标识符,在方法调用时会检查方法是否被设置了ACC_SYNCHRONIZED标识符,如果设置了,执行线程将先获取moniter的所有权,获取成功则执行方法体,执行完毕后释放moniter。在方法执行期间,其他任何线程都无法获得同一个moniter对象。

2.4.2 volatile

  最轻量的同步机制,保证了多个线程对某个变量进行操作时的可见性,即一个线程修改了某个变量的值,修改后的值对其它线程立即可见。但是不能保证共享数据在多线程下同时写时的线程安全。

  volatile实现原理
  JMM规定所有变量都存储在主内存(Main Memory)中,每个线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量在主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,同时本线程工作内存的变量也不能被其他线程直接访问。
  被volatile修饰的变量,在编译成字节码时会添ACC_VOLATILE的访问标志。当对有ACC_VOLATILE标志的变量执行写操作时,JMM会把工作内存中的新值强制刷新到主内存中,并且会使其它线程中的缓存无效。当其他线程使用缓存时,发现工作内存中的变量无效,便会从主内存中重新获取,从而得到了变量的最新值。

2.4.3 ThreadLocal

  ThreadLocal为每个线程提供了变量的副本,隔离了多个线程对数据的共享,从而规避了线程安全问题。

  ThreadLocal的使用
  void set(Object value):设置当前线程局部变量的值。
  Object get():返回当前线程对应的局部变量的值。
  void remove():删除当前线程局部变量的值(非必须操作,线程结束后,对应局部变量会被GC回收,显示调用可以加快内存回收的速度)。
  protected Object initialValue():为子类覆盖而设计,当调用get()方法时,如果没有获得线程所对应的变量的值,会调用到setInitialValue方法,在setInitialValue方法中调用initialValue方法,将initialValue方法返回的值设置为该线程对应的变量的值,并返回该值。

  ThreadLocal实现原理
  一个线程内可以存在多个ThreadLocal对象,所以ThreadLocal内部维护了一个ThreadLocalMap的静态内部类,ThreadLocalMap内部维护了一个Entry[]数组,Entry为ThrealLocalMap的静态内部类。

  ThreadLocal引发内存泄漏原因

四大引用
强引用:只要引用还存在,垃圾收集器就不会回收掉被引用的对象实例。
软引用:描述一些有用但非必须的对象。在系统发生内存溢出之前,会将软引用关联的对象实例列入垃圾回收范围进行二次回收,如果垃圾回收后内存足够,软引用关联的对象实例就不会被回收。通过SoftReference来实现。
弱引用:被弱引用关联的对象只能生存到下一次垃圾回收之前。当发送GC时无论内存是否足够,都会回收被弱引用关联的对象实例。通过WeakReference来实现。
虚引用:和没有引用几乎一样,在任何时候都可能会被垃圾收集器回收。它的作用在于跟踪垃圾回收的过程,在对象被收集器回收时会收到一个系统通知。通过PhantomReference来实现。

  ThreadLocalMap的key为ThreadLocal的弱引用,下一次垃圾回收就会被清理掉,但是value为强引用,不会被清理。
考虑到这种情况,ThreadLocal在调用set、get和remove方法的时候会清理掉key为null的记录。
在出现key为null的情况下,如果没有手动调用remove方法,并且之后也不再调用get、set方法就会出现内存泄漏。

  ThreadLocal的线程不安全
  当ThreadLocal的数据副本为静态属性时,ThreadLocalMap内部保存的其实是一个对象的引用,当其他线程对这个引用的对象实例修改时,也会影响所有线程持有对象引用所指向的同一对象实例。

2.5 线程间的协作

2.5.1 等待通知机制

  一个线程调用了某个对象的wait方法时进入等待状态,其他线程调用该对象的notify/notifyAll方法时,等待状态的线程从wait方法中返回。

  wait(), 调用该方法的线程进入阻塞状态,只有等待别的线程通知或被中断才会返回。调用wait方法,会释放对象的锁。
  wait(long timeout), 超时等待一段时间(毫秒),如果没有通知就超时返回。
  wait(long timeout, int nanos), 粒度更细的控制超时时间。
notify() 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是获取到对象的锁。
  notifyAll(), 通知所有在对象上等待的线程。

  等待通知机制的标准范式
  等待方
  1) 获取对象的锁
  2) 如果条件不满足,调用该对象的wait方法,被通知后仍要检查条件。
  3) 条件满足则执行对应的逻辑。

synchronized(对象){
		while(条件){
			对象.wait();
		}
		//处理逻辑
}

  通知方
  1) 获得对象的锁
  2) 改变条件
  3) 通知所有等待在对象的线程

synchronized(对象){
		//改变条件
		对象.notifyAll();
}

  notify/notifyAll该用谁
  尽量使用notifyAll,因为notify只能唤醒一个线程,并且无法确保唤醒的线程是需要被唤醒的线程。

2.5.2 join方法

  在当前线程中调用某个指定线程的join方法,将指定线程加入到当前线程中执行。直到指定线程执行完毕后,才会继续执行当前线程。

public static void main(String[] args) throws InterruptedException {

        Thread tread = new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程下执行完成...");
        });
        tread.start();
        tread.join();
        System.out.println("主线程执行完成...");
    }

三、并发工具类

3.1 fork-join

  分而治之
  将一个规模为N的问题分解为K个规模较小的子问题(K <= N),这些子问题相互独立且与原问题性质相同,求出子问题的解,就可以求出原问题的解。

  fork-join 原理
  将一个大任务拆分成若干个小任务,再将一个个小任务的结果进行汇总。

  工作密取
  当前线程的任务全部执行完毕后,继续取到其它线程任务池中的任务继续执行。

  使用的标准范式
  只需要继承ForkJoinTask类的子类:
  1) RecursiveAction,用于没有返回结果的任务
  2) RecursiveTask, 用于有返回结果的任务

3.2 CountDownLatch

  使一个线程等待其它线程各自执行完毕后再执行。
  通过一个计数器来实现,计数器的初始值为线程的数量。每当一个线程执行完毕后,计数器就减1,当计数器为0时,表示所有的线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

  应用场景
  实现最大的并行性,单元测试等。

3.3 CyclicBarrier

  让一组线程到达一个屏障(barrier,也叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会解除,所有在被阻塞的线程才会继续执行。可以循环使用。

  CyclicBarrier的使用
  public CyclicBarrier(int parties)
  public CyclicBarrier(int parties,Runable barrierAction)

  parties:参与的线程个数。
  barrierAction:最后一个线程到达屏障时执行的任务。

CountDownLatch和CyclicBarrier比较
CountDownLatch的计数器只能使用一次,CyclicBarrier的计数器可以重复使用。
CountDownLatch的await方法阻塞线程,countDown方法计数器减1。CyclicBarrier的awit方法进行阻塞,并且计数器减1。
在控制多个线程同时运行时,CountDownLatch可以不限线程数,CyclicBarrier是固定线程数。
CyclicBarrier提供了barrierAction用于合并多线程计算结果。

3.3 Semaphore

  用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用公共资源。

  应用场景
  流量控制。如数据库连接。

  Semaphore的方法
  public Semaphore(int permits) 参数为初始许可证数量。

  void acquire() 获取许可证。
  void release() 归还许可证。
  void tryAcquire() 尝试获取许可证。

  int availablePermits() 返回可以许可证数量。
  int getQueueLength() 返回正在等待获取许可证的线程数量。
  boolean hasQueueThreads() 是否有线程正在等待获取许可证。

3.4 Exchanger

  用于线程之间的数据交换,它提供一个同步点,在这个同步点上,两个线程可以交换彼此的数据。
通过调用exchange方法交换数据,如果第一个线程优先执行exchange方法,它会一直等待第二个线程也执行exchange方法,当两个线程都执行到同步点时,彼此交换数据。

3.5 Callable、Future、FutureTask

  Callable
  java.util.concurrent 包下的接口,在它里面也只声明了一个方法call(),这是一个泛型接口,call()函数返回的类型就是传递进来的 V 类型
  Furure
  对于具体的Runnable或者Callable任务进行取消、查询和获取结果。
  V get() 阻塞获取任务结果。

  FutureTask
  实现了RunnableFuture接口,RunnableFuture继承了Runnable和Future接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

  使用

FutureTask<String> task = new FutureTask(()->”abc”);
new Thread(task).start();
String value = task.get();

四、原子操作CAS

4.1 CAS原理

  每一个CAS(CompareAndSwap)操作包含三个运算符:内存地址V,期望值A和一个新值B。如果内存地址上的值V等于期望的值A,则将内存地址上的值赋为新值B,否则不做什么操作。
循环CAS就是在一个循环里不断进行CAS操作,直到成功为止。
  CAS利用CPU的多处理能力,实现硬件层面的阻塞,再加上volatile变量的特性实现了原子操作的线程安全。

4.2 CAS的问题

  1) ABA问题。
  2) 循环时间长开销大。
  3) 只能保证一个共享变量的原子操作。

4.3 原子操作类的使用

  原子操作基本类型:
  AtomicInteger
  int addAndGet(int delta) 与当前值相加,并返回计算结果。
  int getAndIncrement() 当前值加1,并返回当前值。
  int getAndSet(int newValue) 设置为新值,并返回旧值。
  boolean compareAndSet(int expect,update) 如果当前值等于期望值,则更新当前值。

  AtomicIntegerArray
  int addAndGet(int i,int delta) 将输入值与数组中指定索引的元素相加,并返回计算结果。
  int getAndAdd(int i,int delta) 将输入值与数组中指定索引的元素相加,并返回旧值。
  int incrementAndGet(int i) 将数组中指定索引的元素加1,并返回计算结果。
  boolean compareAndSet(int i,int expect,int update) 如果数组中指定索引的元素等于期望的值,则更新当前元素的值。
  通过构造函数传入的数组,在AtomicIntegerArray内部会复制一份,所有AtomicIntergetArray的操作不会影响传入的数组。

  原子操作基本类型,只能更新一个变量,如果要更新多个变量,就可以使用原子操作的引用类型提供的类:

  AtomicReference
  不会对外部对象传入的对象产生影响。

  AtomicStampedReference
  利用版本戳的形式记录了每次改变以后的版本号,可以避免ABA的问题。

  AtomicMarkableReference
  原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。

  原子更新字段类:
  AtomicIntegerFieldUpdater
  抽象类,使用时通过静态方法newUpdater创建更新器,并设置更新的类和属性。更新的属性需要用public volatile修饰。
  会对外部对象属性的值产生影响。

  AtomicLongFieldUpdater

  AtomicReferenceFieldUpdater

五、 显示锁和AQS

5.1 显示锁

  使用者需要手动编写获取锁和释放锁的过程。

  Lock的标准用法

lock.lock();
        try {
            //do something
        }finally {
            lock.unlock();
        }

  Lock的API
  void lock() ,使用阻塞方式获取锁。
  boolean tryLock(), 尝试使用非阻塞方式获取锁。
  boolean tryLock(long time,TimeUnit unit) throws InterruptedException,超时获取锁。该方法在获得锁、被中断或超时会返回。
  void lockInterruptibly() throws InterruptedException ,该方法在获取锁的过程中可以响应中断。
  void unlock() 释放锁。

  ReentrantLock

锁的可重入性
同一线程如果已经获得了锁,可以多次申请到该锁的使用权。

公平锁和非公平锁
公平锁
多个线程按照申请锁的顺序获取锁。每个线程在获取锁时先查看等待队列,如果队列为空或当前线程是队列的第一个,就占有锁。否则,就加入等待队列。
所有线程最终都会获得锁,不会出现饥饿现象。
队列中除了第一个线程,其它线程都会阻塞,CPU唤醒阻塞线程的开销很大。
非公平锁
多线程并不按照申请锁的顺序获取锁。每个线程在获取锁的时候,先尝试获取锁,如果失败就加入等待队列。
需要排队获取锁的线程较少,可以减少CPU唤醒线程的开销。
高并发场景下,可能导致饥饿现象。
在高并发场景下,非公平锁的性能高于公平锁的性能。

  ReentrantReadWriteLock
  读写锁在同一时刻允许多个读线程访问,但在写线程访问时,其他线程都会阻塞。
大多数场景都是读多写少,所以读写锁的性能高于排它锁。

  Condition 接口
  常用方法
  void await() throws InterruptedException 当前线程进入等待状态,直到被通知(signal)或中断。
  void awaitUninterruptibley() 当前线程进入等待状态,直到被通知。
  long awaitNanos(long nanosTimeout) throws InterruptedException 当前线程进入等待状态,直到被通知、中断或超时。返回值为剩余时间。
  boolean awaitUntil(Date deadline) throws InterruptedException 当前线程进入等待状态,直到被通知、中断或到达指定时间。如果没有到达指定时间返回true,否则返回false。
  void signal() 唤醒一个等待在condition上的线程,该线程从等待方法上返回,并获得condition相关的锁。
  void signalAll() 唤醒所有等待在condition上的线程。

  Condition 使用范式

public void conditionAwait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        }finally {
            lock.unlock();
        }

    }

    public void conditionSignal()  {
        lock.lock();
        try {
            condition.signal();
        }finally {
            lock.unlock();
        }

    }

  LockSupport
  定义了一组静态方法,这些方法提供了线程最基本的阻塞和唤醒功能。
  LockSupport.park();
  LockSupport.unpark();

5.2 AQS

  CLH队列锁
  由 Craig、Landin 和 Hagersten发明。
  CLH队列锁是一种基于链表的可扩展、高性能、公平的自旋锁。申请锁的线程在本地变量上自旋,不断轮询前驱节点的状态,加入发现前驱节点释放了锁就结束自旋。
  获取锁的过程:
  1) 当一个线程需要获取锁时,先检查等待队列中是否有元素,如果没有则获取锁,否则创建一个节点Node(将locked属性设置为true,preNode设置为前驱节点)加入到队列尾部。
  2) 当前线程不断自旋检查前驱节点(preNode)locked属性的值。
  3) 当前驱节点释放锁后,将locked属性修改为false,当前线程停止自旋,并获得锁。

  AQS(AbstractQueuedSynchronizer)
  是CLH队列锁的一种变体实现,是用来构建锁或其他同步组件的基本框架。

  AQS的使用方式:
  子类继承AQS并实现它的方法来管理同步状态。推荐被定义为自定义同步组件的内部类,AQS自身并没有实现任何同步接口,它仅仅定义了同步状态获取和释放的方法来供自定义同步组件使用。
  同步器(AQS)面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒的底层操作。

  AQS中的方法:
  模板方法
  final void acquire(int arg) 独占式获取同步状态,如果获取成功则返回,否则将会进入同步队列等待。
  final void acquireInterruptibly(int arg) throws InterruptedException 该方法可以响应中断,如果当前线程被中断,则抛出异常。
  final void acquireShared(int arg) 共享式获取同步状态,同一时刻可以有多个线程获取到同步状态。
  final void acquireSharedInterruptibly(int arg) throws InterruptedException
  final boolean release(int arg) 独占式释放同步状态,该方法在释放同步状态后,将同步队列中第一个节点等待的线程唤醒。
  final boolean releaseShared(int arg) 共享式释放同步状态
  final Collection getQueuedThreads() 获取同步队列的线程集合。

  可重写的方法
  protected boolean tryAcquire(int arg) 独占式获取同步状态。
  protected boolean tryAcquireShared(int arg) 共享式获取同步状态。
  protected boolean tryRelease(int arg) 独占式释放同步状态。
protected boolean tryReleaseShared(int arg) 共享式释放同步状态。

  访问或修改同步状态的方法
  protected final int getState() 获取同步状态
  protected final void setSteate(int newState) 设置同步状态。
  protected final boolean compareAndSetState(int expect,int update)使用CAS设置当前状态。

六、并发容器

6.1 ConcurrentHashMap

  JDK1.7 HashMap 死循环原因:
  两个线程并发的进行扩容操作,一个线程在执行过程中被挂起,而另外一个线程完成了扩容操作,被挂起的线程在恢复执行后,要进行头插法操作,造成了next相互指向对方的循环链表,当执行查找时会产生死循环。
  ConcurrentHashMap

  • 待整理

6.2 其他并发容器

  CurrentSkipHashMap
  ConcurrentLinkedQueue

6.3 阻塞队列

  队列
  是一种操作受限的线性表,只允许在表的前端进行删除操作,在表的后端进行插入操作。插入操作的一端称为队尾,删除操作的一端称为队首。

  阻塞队列
  支持阻塞的插入和获取元素。阻塞队列常用于生产者和消费者的场景。

  队列的方法
  插入:
  boolean add(E e) 插入成功返回true,否则(队列已满)抛出异常。
  boolean offer(E e) 插入元素成功返回true,否则返回fasle。
  void put(E e) 阻塞式插入元素,响应中断。
  boolean offer(E e,long timeout) 插入成功返回true,失败时进入超时阻塞,响应中断。

  移除:
  boolean remove() 移除失败(队列为空)时,抛出异常。
  E poll() 移除成功返回被移除元素,失败返回null。
  E take() 移除成功返回被移除元素,失败进入阻塞,响应中断。
  E poll(long timeout, TimeUnit unit) 移除成功返回被移除对象,失败进入超时阻塞,响应中断。

  检查方法:
  E element() 检查但不删除元素,为空时抛出异常。
  E peek() 检查但不删除元素,为空返回null。

  常用阻塞队列
  ArrayBlockingQueue:由数组构成的有界阻塞队列。
  LinkedBlockingQueue:由链表结构组成的双向阻塞队列(即可以从队列的两端插入和移除元素)。
  PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  DelayQueue:支持延时的无界阻塞队列。
  SynchronousQueue:不存储元素的阻塞队列。
  LinkedTransferQueue:由链表组成的双向阻塞队列。

七、 线程池

7.1 为什么使用线程池

  1) 降低资源消耗
  2) 提高响应速度
  2)3) 提高线程的可管理性

7.2 JDK中的线程池

  Executor 框架
在这里插入图片描述

  线程池的参数
  int corePoolSize 核心线程数
  int maximumPoolSize 最大线程数
  long keepAliveTime 线程空闲时存活时间
  TimeUnit unit线程空闲时存活时间单位
  ThreadFactory threadFactory 线程池工厂
  BlockingQueue workQueue 阻塞队列
  RejectedExecutionHandler handler 拒绝策略,提供了四种策略:
    AbortPolicy 直接抛出异常,默认
    DiscardPolicy 直接丢弃任务
    DiscardOldestPolicy 丢弃队列中最靠前的任务
    CallerRunsPolicy 用调用者所在的线程执行任务

  线程池的工作机制
  1) 如果当前运行的线程少于corePoolSize,则创建新的线程来执行任务(需要获取全局锁)。
  2) 如果当前运行的线程大于等于corePoolSize,则将任务加入到阻塞队列。
  3) 如果队列已满,则创建新的线程来处理任务。
  4) 如果创建线程超出maximumPoolSize,任务将被拒绝。

  线程池提交任务
  void execute(Runnable command) 提交不需要返回值的任务,所以无法判断任务是否执行成功。
  Future<?> submit(Runnable task) 提交需要返回值得任务。

  线程池关闭
  遍历线程池中的工作线程,然后逐个调用线程的interrupt方法中断线程。
  shutdown将线程池的状态设置成 STOP
  shutdownNow将线程池的状态设置成 SHUTDOW

  合理配置线程池
  首先需要分析任务的特性,可以从以下几个角度分析:
  1) 任务的特性:CPU密集型、IO密集型、混合型。
  2) 任务的优先级:高、中、低。
  3) 任务的执行时间:长、中、短。
  4) 任务的依赖性:是否依赖其他系统资源,如数据库连接。

  CPU密集型任务应配置尽可能少的线程,如Ncpu+1。IO密集型应配置更多的线程,如2*Ncpu。混合型任务应劲量拆分。

  IO密集型任务的最佳线程数:
  Nthreads=Ncpu * Ucpu * (1 + W / C)
  Ncpu是处理器的核的数目
  Ucpu是期望的 CPU 利用率(该值应该介于 0 和 1 之间)
  W/C 是等待时间与计算时间的比率

  JDK预定义的线程池
  Executors.newFixedThreadPool(10);
  Executors.newSingleThreadExecutor();
  Executors.newCachedThreadPool();
  Executors.newWorkStealingPool();
  Executors.newScheduledThreadPool(10);
  Executors.newSingleThreadScheduledExecutor();

八、并发安全

8.1什么是线程安全

  当多个线程访问同一个对象时,无论运行环境采用何种调度方式或者线程如何交替执行,并且不需要进行额外的同步操作,调用这个对象的行为都能获得正确的结果,那么这个对象就是线程安全的。

8.2 怎样做到线程安全

  1) 栈封闭
  局部变量不会被多个线程共享,所以不会出现并发安全问题。
  2) 无状态的类
  没有任何成员的类,叫无状态的类。
  3) 让类不可变
  类不可变,有2种方式:第一种方式是成员变量加fianl关键字,第二种方式不提供任何可修改成员变量的地方。
  4) volatile
  并不能保证类的线程安全,只能保证类的可见性,最适合一个写线程,多个读线程的情况。
  5) 加锁和CAS
  6) 安全的发布
  7) ThreadLocal

8.3 线程不安全引发的问题

  死锁
  死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象,若无外力推进,它们都将无法进行下去。

  死锁的危害:
  1) 线程不工作了,但是程序是活的。
  2) 没有任何异常信息可以查看。
  3) 一旦发生死锁,只能重启程序。

  死锁的解决办法:
  通过 jps 查询应用的 id,再通过jstack id 查看应用的锁的持有情况。

  活锁
  活锁是指任务或者执行者没有被阻塞,由于某些条件没有被满足,导致一直重复尝试-失败-尝试的过程。处于活锁的实体是在不断的改变状态,活锁可以自行解开。

  线程饥饿
  优先级低的线程,总是拿不到资源。

8.4 线程安全的单例模式

  懒汉式
  延迟初始化占位模式(内部类持有外部类):

public class LazySingleton {
    private static LazySingleton4 instance = null;

    public static LazySingleton getInstance() {
        return HungrySingletonHolder.instance;
    }

    private LazySingleton() {
    }
    //静态内部类持有外部类
    static class HungrySingletonHolder {
        static LazySingleton instance = new LazySingleton();
    }
}

  双重检验+volatile模式:

public class LazySingleton {
    /**
     * <p>
     * 在某些情况下jvm会对指令进行重排优化,
     * 可能导致对象在分配内存之后还没有进行初始化,
     * 这时候另外一个线程代码执行到if(instance == null)时,
     * 发现对象已经不为空,直接返回。
     * 此时返回的对象是一个未初始化的对象,就会有问题。
     * </p>
     *  volatile 关键字可以禁止jvm对指令进行重排序
     */
    private static volatile LazySingleton instance = null;

    public static LazySingleton getInstance() {

        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
    //构造函数私有化
    private LazySingleton() {
    }

  饿汉式
  在申明的时候就new这个类的实例,由虚拟机保证线程安全。

public class HungrySingleton {

    private static HungrySingleton instance = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return instance;
    }

    private HungrySingleton() {
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不才不才不不才

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

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

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

打赏作者

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

抵扣说明:

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

余额充值