Review:并发编程篇

https://yanglinwei.blog.csdn.net/article/details/103760834

多线程的运行状态

在这里插入图片描述


线程分类:分为用户线程和守护线程(区别在于主线程停止,这两个线程也是否会跟着停止),setDeamon(true);


线程的几种创建方式:继承Thread、实现Runnable(推荐),使用匿名内部类。


线程方法

  • join:谁join就让给谁。线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。
  • yield:暂停当前正在执行的线程,并执行其它线程(可能会没效果)。

线程安全:当多个线程同时共享全局变量或静态变量,做写操作时,可能会发生数据冲突的过程,也就是线程安全的问题。但是做读操作是不会发生数据冲突问题。


线程安全问题解决方式:使用同步synchronized或使用锁(lock)。


内置锁:每一个java对象都可以用作一个实现同步的锁,成为内置锁(synchronized)。synchronized关键字有两种用法:

  • 修饰方法:修饰普通的方法,使用的是this锁,修饰静态(static)的方法,使用的是当前类的字节码文件对象。
  • 修饰代码块:锁粒度更小,锁对象不一定是this。

多线程死锁:同步嵌套同步,导致锁无法释放。


ThreadLocal:ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。


多线程的3大特性原子性(要么全部执行,要么全部不执行)、可见性(某线程修改了一个变量,另外一个线程也能看到)、有序性(一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的)


Volatile: 解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值。

  • 特点:保证此变量对所有的线程的可见性(可见性)、禁止指令重排序优化(有序性)
  • 与synchronized区别:读性能几乎没区别,写耗时。volatile保证可见性,不保证原子性。

重排序:编译器和处理器可能会对操作做重排序,它们在重排序时,会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。

  • 在单线程程序中: 对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因)
  • 在多线程程序中: 对存在控制依赖的操作重排序,可能会改变程序的执行结果。

共享内存模型(JMM):定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题,如下图:

在这里插入图片描述


多线程之间通讯:指多个线程在操作同一个资源,但是操作的动作不同(举例:生产和消费线程)

  • wait(属于Object类:必须暂定当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行;
  • notify/notifyall(属于Object类): 唤醒锁池中的线程,使之运行。wait、notify一定要在synchronized里面进行使用;一般使用时,必须是同一个锁资源;
  • sleep(属于Thread类):程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
  • Lock(jdk1.5之后新增):提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。Lock 接口在指定的截止时间之前获取锁,如果截止时间到了依旧无法获取锁,则返回。
  • Condition:类似于在传统的线程技术中的,Object.wait()(condition.await())和Object.notify()(condition.signal())的功能。

并发包相关类

  • CountDownLatch(计数器):一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了;

  • CyclicBarrier(屏障):可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍;

  • Semaphore(计数信号量):设定一个阈值,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞;

  • Queue(队列):在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能非阻塞队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue。

  • ConcurrentLinkedQueue(非阻塞队列):适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue.它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。

  • BlockingQueue(阻塞队列):在队列为空时,获取元素的线程会等待队列变为非空,当队列满时,存储元素的线程会等待队列可用。分为:

     【ArrayBlockingQueue】:一个有边界的阻塞队列,它的内部实现是一个数组 
    
     【LinkedBlockingQueue】:大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为`Integer.MAX_VALUE`的容量 。它的内部实现是一个链表)
    
      【PriorityBlockingQueue】:是一个没有边界的队列
    
      【SynchronousQueue】:内部仅允许容纳一个元素,当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
    

线程池:为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。


线程池分类

  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newSingleThreadExecutor**:**创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newScheduleThreadPool**:**创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadScheduledExecutor**:**创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

前面几种线程池分类的创建方式:Executors.newXXX。Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。


线程池处理流程

在这里插入图片描述


CPU密集:任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。


IO密集:任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。


合理配置线程池:CPU密集型,因为全速运行,线程CPU所占时间长,所以需要越少的线程。IO密集型,因为IO阻塞,CPU运算能力都在等待,所以需要越多的线程。(总结就是:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。)

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 ) * CPU数目


Callable和Future:与使用Thread和Runnable创建线程的区别是,Callable和Future来实现获取任务结果的操作。常用方法如下:

  • 能够中断执行中的任务:boolean cancel(boolean mayInterruptRunning)
  • 判断任务是否执行完成:boolean isDone()
  • 获取任务执行完成后的结果:V get()

锁分类(同一JVM)

  • 重入锁:分为Synchronized(内置锁、重量级锁)和ReentrantLock(显示锁、轻量级锁);
  • 读写锁:读-读能共存,读-写不能共存,写-写不能共存。使用方式:① ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); ②Lock r = rwl.readLock(); ③ Lock w = rwl.writeLock();
  • 悲观锁:悲观的认为每一次操作都会造成更新丢失问题,在每次查询时加上排他锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • 乐观锁:认为在操作数据时(更新操作),默认别的线程并不会操作数据发生冲突,所以在每次操作时并不会上锁,只是在更新时判断别的线程在此期间是否有做修改。(通常使用方式,添加version字段,更新前,先查出version,更新时加version校验是否已经改变)。
  • CAS无锁机制:在高并发的情况下,它比有锁的程序拥有更好的性能,它天生就是死锁免疫的。算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。调用Native方法compareAndSet,执行CAS操作
  • 自旋锁:采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

锁分类(不同JVM):如果想在不同的jvm中保证数据同步,使用分布式锁技术,分类如下:

  • 数据库实现: 使用排它锁的技术(很少用到);
  • Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁;
  • Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功;
  • Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的;
  • Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

原子类:支持在单个变量上解除锁的线程安全编程,相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作,用了无锁的概念,有的地方直接使用CAS操作的线程安全的类型。常见的有:AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference。


Disruptor框架:一个高性能的异步处理框架,或者可以认为是最快的消息框架(轻量的JMS),也可以认为是一个观察者模式的实现,或者事件监听模式的实现。利用无锁的算法,所有内存的可见性和正确性都是利用内存屏障或者CAS操作来实现低延迟。

猜你喜欢

转载自blog.csdn.net/qq_20042935/article/details/134569646