Java多线程与并发 学习

2月28 Java多线程与并发

Java并发-理论基础


为什么需要多线程

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,
平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

需要了解的类

ExecutorService

java中对线程池定义的接口

Executors工厂类

1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

备注:Executors只是一个工厂类,它所有的方法返回的都是ThreadPoolExecutorScheduledThreadPoolExecutor这两个类的实例。

newCachedThreadPool返回ThreadPoolExecutor,它继承自AbstractExecutorService,前者实现ExecutorService接口

ExecutorService使用

ExecutorService executorService = Executors.newFixedThreadPool(10);

executorService.execute(new Runnable() {
    public void run() {
        System.out.println("Asynchronous task");
    }
});

executorService.shutdown();

ExecutorService的执行

ExecutorService有如下几个执行方法:

- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(...)
- invokeAll(...)

execute(Runnable)

异步的执行,但是无法获取方法的返回值

ExecutorService的关闭

  • executorService.shutdown(): 所有线程执行完关闭
  • executorService.shutdownNow(): 强制关闭

其他请参考
https://blog.csdn.net/suifeng3051/article/details/49443835

CountDownLatch

CountDownLatch概述

  1. CountDownLatch一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。

  2. 有一点要说明的是CountDownLatch初始化后计数器值递减到0的时候,不能再复原的,这一点区别于Semaphore,Semaphore是可以通过release操作恢复信号量的。

CountDownLatch使用原理

  1. 创建CountDownLatch并设置计数器值。
  2. 启动多线程并且调用CountDownLatch实例的countDown()方法。
  3. 主线程调用 await() 方法,这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务,count值为0,停止阻塞,主线程继续执行。

CountDownLatch常用方法

  • public void await() throws InterruptedException:调用await()方法的线程会被挂起,等待直到count值为0再继续执行。
  • public boolean await(long timeout, TimeUnit unit) throws InterruptedException:同await(),若等待timeout时长后,count值还是没有变为0,不再等待,继续执行。时间单位如下常用的毫秒、天、小时、微秒、分钟、纳秒、秒。
  • public void countDown(): count值递减1
  • public long getCount():获取当前count值。
  • public String toString():重写了toString()方法,多打印了count值,具体参考源码。

CountDownLatch使用场景

一个程序中有N个任务在执行,我们可以创建值为N的CountDownLatch,当每个任务完成后,调用一下countDown()方法进行递减count值,再在主线程中使用 await() 方法等待任务执行完成,主线程继续执行。

https://www.cnblogs.com/Andya/p/12925634.html

pool-1-thread-1 do something!
pool-1-thread-2 do something!
pool-1-thread-2 do something!
pool-1-thread-2 do something!
pool-1-thread-1 do something!
main thread do something-1
pool-1-thread-3 do something!
pool-1-thread-3 do something!
pool-1-thread-3 do something!
pool-1-thread-3 do something!
pool-1-thread-3 do something!
countDownLatch: java.util.concurrent.CountDownLatch@68f7aae2[Count = 0]
main thread do something-2

并发问题出现的根源: 并发三要素

可见性: CPU缓存引起

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

一个线程修改的值还没有存入主存中时,另一个线程就从主存中调用
这个值了

原子性: 分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

i+=1这条程序CPU执行分三个步骤
1. 将变量 i 从内存读取到 CPU寄存器;
2. 在CPU寄存器中执行 i + 1 操作;
3. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

由于CPU分时复用,在多个线程中这三个步骤可能会被打断执行

有序性: 重排序引起

有序性:即程序执行的顺序按照代码的先后顺序执行。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

* 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
* 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
* 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

对于会导致多线程程序出现内存可见性问题的重排序,对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
处理器重排序有 “内存屏障“指令

JAVA是怎么解决并发问题的: JMM(Java内存模型)

java内存模型

理解的第一个维度:核心知识点

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。

理解的第二个维度:可见性,有序性,原子性

  • 原子性
    只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

    Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。

  • 可见性
    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    通过synchronized和Lock也能够保证可见性

  • 有序性
    可以通过volatile关键字来保证一定的“有序性”,另外可以通过synchronized和Lock来保证有序性.
    当然JMM是通过Happens-Before 规则来保证有序性的。

关键字: volatile、synchronized 和 final

Happens-Before 规则

单一线程原则

Single Thread Rule

在一个线程内,在程序前面的操作先行发生于后面的操作。

管程锁定规则

Monitor Lock Rule

一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

volatile 变量规则

Volatile Variable Rule

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。(两个线程同时要对volatile变量操作的话)

线程启动规则

Thread Start Rule

Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

线程加入规则

Thread Join Rule

Thread 对象的结束先行发生于 join() 方法返回。

线程中断规则

对象终结规则

传递性

Transitivity

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

线程安全: 不是一个非真即假的命题

一个类在可以被多个线程安全调用时就是线程安全的

线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

不可变

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。

多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

不可变的类型

  • final 关键字修饰的基本数据类型
  • String
  • 枚举类型
  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);

绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施。

相对线程安全

相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。

在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。

线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。

线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

线程安全的实现方式

互斥同步

阻塞同步

synchronized 和 ReentrantLock。

非阻塞同步

无同步方案

要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性

(一)栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的

https://pdai.tech/md/java/thread/java-thread-x-theorty.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值