java并发编程之多线程小结

今天这里总的概诉一下多线程,把我们之前学习的串行起来。

多线程基础

        线程和进行的区别: 线程是一条执行路径。多线程是多条独立的执行路径,他们与进程的区别是,进程可以看作是计算机的一个独立的应用,而线程只是一条执行路径,一个进行会包含多个线程。

        创建线程的方式:1.继承Thread类,重写run方法  2.实现Runnable接口,实现run方法 总的来说就这两种 话可以写成匿名内部类方式。

        使用多线程的好处:合理的使用多线程可以提高程序效率,多线程会充分利用CPU,所以会压榨CPU为我们执行任务。但是需要合理配置线程,超出CPU承受范围,会适得其反。

        线程的五个状态:创建,就绪,运行,阻塞,销毁。其中启动线程是 start()方法。销毁分为两种情况,正常执行完毕和执行期间发生异常。

        详细了解请点击我:java并发编程之多线程基础。

        另外在--java并发编程之多线程基础--中补充一点-------->在主线程中创建线程,这时候的线程是一个全新的执行路径,不会受到主线程的影响,哪怕是主线程执行完毕,子线程还是会继续执行,这个时候的子线程就相当于一条全新的执行路径。如果多线程中涉及到事务,那么这个时候的事务都是独立的?切记是独立的,不会因为一个地方出错而回滚。如果想要主线程销毁,子线程也销毁可以设置子线程为守护线程。--------> setDaemon(true)

多线程线程安全问题

        什么是线程安全问题:当多线程在操作同一个共享变量的时候,因为线程不是直接操作值,而是操作的本地内存(JMM),当本地内存刷新到主内存中肯定是存在时间的,那么如果这个时候其他线程也对主内存进行了操作,读取不会发生线程安全问题,只有操作才会发生线程安全问题。也就是当线程操作同一个共享变量的时候其他线程是不可见的。其他线程不知道你修改了值。

 线程的三大特性:原子性,可见性,有序性

  1. 原子性:对共享变量读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
  2. 可见性:一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  3. 有序性:即代码执行顺序都是重上至下串行执行。

所以,线程安全问题就是在线程修改了共享变量的时候,就是JMM和线程可见性的原因,通俗说就是当线程修改了共享变量的时候其他线程不知道。

        重排序:线程为了提高执行效率,会把没有依赖关系的代码进行重新排序,就违背了有序性特性,那么就很有可能会影响到最终的执行效果。

扫描二维码关注公众号,回复: 8957457 查看本文章

        volatile:该关键字就是用于保证可见性,和有序性的,他可以把线程修改的值马上刷新到主内存,保证可见性,而且可以禁止从排序,但是却不能解决线程安全问题。因为普通的共享变量被修改之后,会在什么时候把修改值写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证线程安全问题。

        如何避免线程安全问题:可以使用锁机制,因为锁机制可以保证可见性(只允许一个线程执行)。

锁总的来说分为两类:

  1. 悲观所:是悲观的,总认为会发生线程安全问题,所以就加锁。
  2. 乐观锁:是乐观的,它不会认为会发生线程安全问题,所以就不会加锁。

多线程锁的深入

1.悲观锁

悲观锁代表:Lock(轻量级锁,递归锁,显示锁,互斥锁),synchronized关键字(重量级锁,递归锁,互斥锁)

1-1:Lock接口主要常用实现类

  • ReentrantLock:可以使用 lock() 灵活的手动上锁,但是不要忘记 ulock() 释放锁资源,与synchronized功能相同。
  • ReadLock:读锁,一次只允许一个线程进行读取。
  • WriteLock:写锁,一次只允许一个线程进行写入。

Lock锁需要结合try进行使用,在finally中手动释放锁资源。

1-2:synchronized关键字

  • 可以是代码块,可以是修饰方法,可以是修饰静态方法。
  • 如果是修饰方法,使用的是this锁对象,如果是静态方法,则使用的是当前类的字节码文件。
  • 代码块,需要指定一个所对象,是一个对象,常用基本数据类型的包装类型都可以作为synchronized锁对象。

1-3:wait,notify

    可以实现生产者消费者,当wait的时候就会被阻塞,当执行了notify才会被释放,必须结合synchronized使用

1-4:Condition中的await和signal

    Condition也可以达到synchronized搭配wait和notify的功能,但是Condition可以指定阻塞的时间,即到达了时间还没执行signal放行指令,这个时候就会自动放行。

1-5:CountDownLatch计数器

    在创建该类的时候,需要指定一个int类型的初始值,该值就是执行countDown()的次数。使用改类的await()方法会阻塞线程,只有当调用了指定次数的countDown()方法,阻塞线程才会被释放。

1-6:CyclicBarrier屏障

    同样会在创建的时候指定初始值,调用await()方法会阻塞线程,当之后达到指定初始值数量的线程数才会被释放,可以理解为只有当指定数量线程被创建就绪之后会被统一同时释放。

1-7:Semaphore信号量

    同样要指定初始值,作用为指定方法只允许指定数量的线程同时进行执行,其余的线程会竞争拿到许可证,有了许可证才可以执行被上锁的方法,执行完毕之后需要归还许可证,acquire()获取许可证,release()释放许可证。

CountDownLatch,CyclicBarrier,Semaphore详细使用方法请点击我:java并发编程之队列

使用锁,尽量避免锁里面嵌套锁,否则容易发生死锁现象。

2.乐观锁

CAS无锁机制:Compare and Swap,即比较再交换。我们可以理解CAS无锁机制为一下模式

    CAS会存在三个参数,即为:CAS(V,E,N)

  • V(Variable):需要更新的变量
  • E(Expect):预期值
  • N(New):需要更新的值

        CAS无锁机制白话文解释:就是当我们要更新变量(Variable)的时候,会将预期值(Expect)和我们需要修改的新值(New)进行比较,如果说Expect和New值相等,那么就表示没有其它线程对Variable进行修改,这个时候就会把New赋值给Variable。

乐观锁代表类:原子类---->java.util.concurrent.atomic包下面

这里列出了所有的原子操作类,这里对常用的原子类做一些介绍

原子更新基本数据类型

  • AtomicInteger:对Integer类型的原子操作。
  • AtomicBoolean:对Booleran类型原子操作。
  • AtomicLong:对Long类型的原子操作。

原子更新数组

  • AtomicIntegerArray:原子更新整形数组里面的元素
  • AtomicLongArray:原子更新长整形数组里面的元素
  • AtomicReferenceArray:原子更新引用类型数组里面的元素

        另外如果使用构造函数创建上面几个对象,并且通过构造函数传递需要操作的数组,那么会赋值一份新的数组,原子不会操作之前的数组,而是操作的复制的数组。

原子更新引用类型

  • AtomicReference:原子更新引用类型
  • AtomicReferenceFieldUpdater:原子更新引用类型的字段
  • AtomicMarkableReference:原子更新带有标记位的引用类型,会带有版本号

原子更新字段

  • AtomicIntegerFieldUpdater:原子更新整形的字段
  • AtomicLongFieldUpdater:原子更新长整形的字段
  • AtomicStampedReference:原子更新带有版本号的引用类型,可用于原子的更新数据和数据的版本号。
原子基本数据类型常用方法
方法名称 作用
set(int newValue) 设置一个值,这里是AtomicInteger,所以参数也是int类型
get() 返回当前值 什么原子类就是返回什么值
getAndSet(int newValue) 更新并返回更新之前的值,参数为需要更新的值
boolean compareAndSet(int expect, int update) 更新原子操作,expect预期值,update更新的值,如果更新失败返回false。
getAndIncrement() 自增,返回更新前的值 
getAndDecrement() 自减,返回更新前的值
incrementAndGet() 自增,返回现在的新值
decrementAndGet() 自减,返回现在的新值

黑色部分是12个原子类通用的方法。

12个原子类总结

        我们可以看到12个原子类,有两个是红色的,为什么会这样呢?因为其余10个原子类也不是绝对安全的,他们有可能会发生ABA问题。

        为什么会发生ABA问题:前面我们说到了CAS是循环进行比较判断,然后会把预期值和需要更新的值进行比较判断,如果预期值等于更新的值,那么就会进行赋值操作,那如果有一个线程修改了值,然后第二个线程又把值修改回去了。那这个时候预期值就没有发生改变了,但是已经被修改了,这就是所谓的ABA问题。

        为什么AtomicStampedReferenceAtomicMarkableReference没有ABA问题呢?因为他在传统的CAS(V,E,N)上增加了一个版本号,即CAS(V,E,N,V,VE),这样只要修改了,那么版本号都会进行改变,下次进行操作的时候就还会对版本,一级版本预期值进行判断,如果版本不等于版本预期值,或者更新的值不等于预期值,那么都会不允许操作。

多线程之队列

队列:队列遵循LIFO原则,即先进先出,在java中队列(Queue)跟list和set是同一个级别的,他们都是继承于Collection接口

队列三大类型:

  • 阻塞队列:当队列超中指定最大容量元素超出之后入队会阻塞,当队列中不存在元素时,出队也会阻塞
  • 非阻塞队列:顾名思义,即不会被阻塞
  • 双端队列:一般队列原理为FIFO,即先进先出原则,但是双端队列,头和尾可以同时进行出队和入队

如果想详细了解队列,请点击我:java并发编程之队列

拓展:多线程如何正确中断线程

  1. 抛出异常法 例如:int i = 1 / 0
  2. interrupt 搭配阻塞线程awit()

详细了解请点击我:interrupt() 配合阻塞中断线程

多线程之线程池

    线程池:线程池相当于是我们多线程的一个管理者,在高并发情况下,如果频繁的创建线程和销毁线程,那么会对性能有很大的影响,而且这样线程无法管理,当线程多了之后,我们的CPU往往会吃不消,而我们的线程池就是充当一个管理者和调度者,线程池会存在一些空闲的线程,如果当前有任务过来,这个时候就不需要创建线程,直接把闲置的线程拿来使用即可,如果一下来了很多的任务,那么这个时候线程池就会创建新的线程,如果超过了我们设置的最大线程数,那么线程池就会把任务保存到队列中,而且线程池中的线程在执行完毕任务之后不会被马上销毁,如果还有任务,那么线程就会被复用。如果任务被处理完毕之后,被创建的线程也不会一直闲置,而是当指定时间之后会被销毁。使用多线程,推荐使用线程池

线程池特点总结:

  1. 很友好的管理多线程。
  2. 存在闲置线程,可以达到快速响应的效果(少量请求,直接使用闲置线程,不必创建线程,当然更快)
  3. 有最大线程数量,不会存在无休止创建线程,导致程序崩溃(线程太多也不好,所以要合理配置线程
  4. 线程闲置销毁时间,没有使用的线程到达指定时间,那么就会被回收销毁(避免闲置线程消耗系统资源
  5. 有缓存队列,超额任务可进行缓存,避免创建过多线程,和流量削锋,避免程序崩溃
  6. 当然还可以提升小于效率咯(多线程特性
  7. 有返回值,可抛出异常

多线程两大创建方式:

  1. 创建ThreadPoolExecutor类,调用execute方法,或者submit方法。
  2. Executors类中的抽象方法。

其中ThreadPoolExecutor类需要我们自己指定参数,进行配置线程池,Executors中的抽象方法,封装好的,可以直接使用。

ThreadPoolExecutor

  这是ThreadPoolExecutor参数最多的一个构造函数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
  1. corePoolSize:核心线程数,核心线程数就相当于是一直存活的空闲线程
  2. maximumPoolSize:最大线程数,允许创建最大的线程数量
  3. keepAliveTime:空闲线程存活时间
  4. unit:keepAliveTime 时间单位
  5. workQueue:当超出最大线程数之后的任务,需要被缓存的阻塞队列
  6. threadFactory:线程创建策略
  7. handler:拒绝策略,当任务数量大于核心线程数+队列最大容量数,就会执行的拒绝策略。

如果自定义线程池,前面五个是比较核心的参数。

Executors

  • newCachedThreadPool:一个最大线程数接近无限大的一个线程池,空余线程回收时间为1分钟,如果使用这个基本上就不会发生我们上面自定义实现的 ThreadPoolExecutor 出现拒绝策略,因为我们最大线程数才8,而这个线程池为 Integer.MAX_VALUE 这么大
  • newFixedThreadPool:该线程池创建时要指定最大线程数,他的最大线程数和核心线程数都是一样的,并且如果有空余线程会立马被回收。但是他有一个LinkedBlockingQueue队列,基本上也算是一个无限大的一个线程池了。
  • newScheduledThreadPool:核心线程数为指定的线程数,但是最大线程数也是Integer.MAX_VALUE,并且支持定时执行。
  • newSingleThreadExecutor:改线程的核心线程数和最大线程数都是1,空余线程会被立马回收,但是队列是LinkedBlockingQueue。

上面四个就是常用的Executors提供的四个线程池,当然Executors中的线程池远远不止这几个。

execute和submit的区别

        execute:使用一个实现Runnable接口的线程作为参数,进行执行该任务,但是没有返回值,无法抛出异常。

        submit:使用一个实现Runnable接口或者Callable<T>接口的线程类,可以有返回值,可以抛出异常。如果接口实现Callable<T>接口,那么就会返回一个Future接口类型,然后调用该接口中的get()方法或者get(long timeout, TimeUnit unit)方法即可拿到返回值,调用get()方法会阻塞线程,后者的区别就是当到达指定时间,如果还没有拿到返回值,那么就会抛出异常。

合理配置线程池

合理配置线程池非常重要,一般分为CPU密集型,和IO密集行,有兴趣的小伙伴可百度。

好了到了这里,就大概的简述了一下java中多线程。

往期回顾:

java并发编程之正确使用 interrupt 中断线程

java并发编程之线程池

java并发编程之队列

java并发编程之线程之间通讯

java并发编程之内存模型&多线程三大特性

java并发编程之线程安全问题

java并发编程之多线程基础

发布了25 篇原创文章 · 获赞 9 · 访问量 3050

猜你喜欢

转载自blog.csdn.net/qq_40053836/article/details/100189113