多线程总结

当多个线程访问某一个类(对象或者方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或者方法)就是线程安全的。

Synchronized

  1. 如果方法使用的synchronized关键字方法修饰时,当多个线程访问该方法时,获取到的是对象级别的锁,也就是以当前对象为锁,若不是同一个对象,就可以互不干扰的执行;如果同一个类中有多个方法被synchronized修饰,且被被修饰的方法同时被多线程访问,那么同时只可以有一个线程被执行,因为这几个方法的锁都是对象锁,只有一个
  2. 如果方法使用的static与synchronized关键字修饰后,当多个线程同时访问该方法时,获取到的锁是会是类级别的锁,也就是以当前类为锁,只可以有一个线程可以执行,其它线程需要等待执行
  3. synchronized修饰方法在指定锁时,可指定当前对象或其它对象或任意类;指定对象为锁时,对象中属性的变更不会影响锁;指定字符串为锁时,可以使用String对象,但不要使用字符串常量,使用String对象时,内容不能修改,会导致锁变更
  4. 锁重入:正在执行的线程在获取到锁时,如果去执行其它使用了同一把锁的方法时,是可以直接执行的

Volatile

  1. volatile关键字的主要作用是使变量在多个线程间可见
  2. 线程中引用的变量是一个副本,如果变量被volatile修饰,则当变量改变时,会强制线程执行引擎去主内存中读取
  3. volatile关键字虽然拥有多个线程间的可见性,但是不具备同步性(既原子性),只可以算是一个轻量级的synchronized,性能也比synchronized强很多,不会造成堵塞(很多开源框架都在用,比如Netty底层就大量使用的volatile)
  4. volatile一般用于多线程间的可见变量,并不能代替synchronized同步功能

notify()和wait()

都是Object类中的方法,所以所有对象都会有这两个方法,wait()会释放锁,notify()不会释放锁

同步类容器

同步类容器都是线程安全的,如Vector、HashTable等,虽然实现了线程安全,但严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量

并发类容器

JDK5.0以后提供了多种并发类容器来代替同步类容器从而改善性能,专门针对并发设计,如使用ConcurrentHashMap来代替传统的HashTable,CopyOnWriteArrayList代替Voctor,以及并发的Queue等

ConcurrentHashMap

内部使用段(Segment)来表示不同的部分,每个段其实就是一个小的HashMap,每个都自己的锁,只要多个修改操作发行在不同的段上,它们就可以并发执行,如果把一个整体分成了16个段,也就是最高支持16个线程的并发修改操作,这也是在多线程场景时减少锁的粒度从而降低锁竞争的一种方案。并且在代码中大多共享使用了Volatile关键字声明,目的是第一时间获取修改的内容,所以性能非常好。

CopyOnWrite容器

CopyOnWrit即写时复制的容器,有两种:CopyOnWriteArrayList和CopyOnWriteArraySet。通俗的理解就是当我们往一个容器里添加元素时,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器中添加元素,添加完元素后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器,适用于读多写少的场景

Queue

主要包含两类实现,ConcurrentLikedQueue和BlockingQueue

  1. ConcurrentLikedQueue是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发下的高性能
  2. BlockingQueue是一个基于链接节点的无界线程安全的阻塞队列,有子类:ArrayBlockingQueue(有界阻塞队列)、LinkedBlockingQueue(无界阻塞队列)、SynchronousQueue(没有缓冲,生产即直接消费的队列)、PriorityBlockingQueue(基于优先级的阻塞队列)、DelayQueue(带有延迟时间的队列)等

多线程中的设计模式

  1. Future模式:在有比较耗时的加载数据业务时,先返回一个包装数据对象,然后异步去加载数据,再将加载到的真实数据添加到包装数据对象中
  2. Master-Worker模式:常用的并行计算模式。它的核心思想是系统由两类进程协作工作:Master进程和Worker进程。Master负责接收和分配任务,Worker负责处理子任务。当各个Worker子进程处理完成后,会将结果返回给Master,由Master做归纳和总结。其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。
  3. 生产者-消费者模式:由负责生产数据的线程去生产数据,将数据存放在一个合集中,再由负责消费数据的线程从合集中取出数据,去做相应的业务逻辑,即可以提高执行效率,也可以将生产端和消费端解耦,提高灵活性。

Executor框架

为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效地进行线程控制。它们都在java.util.concurrent包中,是JDK并发包的核心。其中有一个比较重要的类:Executors,它扮演着线程工厂的角色,通过它可以创建特定功能的线程池。分别有以下几个常用方法:

  1. newFixedThreadPool()方法,创建一个固定数量的线程池,线程数量始终不变,当有任务提交时,若有线程池中有空闲线程,则立即执行,若没有,则会被暂缓在一个任务队列中等待执行
  2. newSingleThreadExecutor()方法,创建只有一个线程的线程池,若线程空闲则执行任务,否则将任务暂缓在任务队列中
  3. newCachedThreadPool()方法,创建一个可根据实际情况调整线程个数的线程池,不限制最大线程数量(最大数量为Integer.MAX_VALUE),当有任务提交时,若有空闲的线程则执行任务,否则创建新的线程去执行,无任务的空闲线程会在60秒后自动回收
  4. newScheduledThreadPool()方法,创建一个可指定数量的线程池,返回一个ScheduledExecutorService对象,可以进行任务调度
  5. 当常用方法不能满足需求时,还可以自定义。上述所有方法都是新建了ThreadPoolExecutor或其子类对象,自定义时根据需要来新建ThreadPoolExecutor对象即可,ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler),参数含义分别为(初始化时的核心线程数,最大线程数,线程的空闲存活时间,时间单位,任务队列,任务队列满时的拒绝处理方法),不同的参数,不同的任务队列,会产生不同策略的线程池
  6. 在使用有界队列,如ArrayBlockingQueue时,当有新的任务需要执行,若线程池实际线程数小于corePoolSize时,则优先创建线程,若大于corePoolSize,则会将任务加入队列中,如果队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新线程,若线程数大于maximumPoolSize,则执行拒绝策略,或其它自定义方式
  7. 在使用无界队列,如LinkedBlockingQueue时,除非系统资源耗尽,否则无界的任务队列不存在任务入列失败的情况,当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务,当达到corePoolSize后,就不会再新建,若仍有新的任务加入,而没有空闲线程资源,则任务直接放入列表中等待。所以,对无界队列的线程池来说,是无视maximumPoolSize参数的
posted @ 2021-10-26 11:28  lixuelong  阅读(63)  评论(0编辑  收藏  举报