Java并发_1.Java对并发编程都提供了哪些支持

175 阅读4分钟

AQS、JMM、JUC、CAS关于Java并发编程的概念许许多多,本章起抱着解决问题的角度,从头开始建立一个知识框架,是时候熟练掌握Java并发了!出发!

1. 并发导致的线程安全问题

我们都已经知道了并发在实际应用和面试中的重要性了,但是并发会产生线程安全问题。分为安全性问题、活跃性问题和性能问题。

image.png

1.1 安全性问题

安全就是永远不会发生糟糕的事情。多个线程访问某个类不管怎么访问都能表现出正确行为,那么称这个类为线程安全的。方法、属性也都是如此。

不管怎么访问这里包括环境、线程、调用者的改变都不会影响执行结果:

  1. 不管环境的调度方式都能表现出正确行为。
  2. 不管线程如何交替执行都能表现出正确行为。
  3. 不需要调用代码中额外的同步操作都能表现出正确行为。

1.1.1 相关概念

  • 竞态问题/竞态条件Race Condition

多个线程访问同一个资源,如果对资源的访问顺序敏感,那么就会存在竞态条件。

  • 内存可见性

一个线程对共享变量的修改可能对其他线程不可见。

  • 指令重排序

代码执行顺序与预期不符,进而引发线程安全性问题。

1.2 活跃性问题

活跃性就是某件正确的事情最终会发生。我们常见的死锁、活锁、饥饿都属于活跃性问题。

1.2.1 相关概念

  • 死锁

两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁。

  • 活锁

线程会一直重复执行某个相同的操作,并且一直失败重试。

  • 饥饿

就是线程因无法访问所需资源而无法执行下去的情况。

1.3 性能问题

之后再提。

2. Java拯救并发

Java对于并发提供了JUC工具包和JMM内存模型。针对解决以上提及的问题,先简单介绍一下解决问题思路,之后会对具体问题解决方案展开分析。

保证线程安全的常见方案有:

1. 单线程

例如Redis收到客户端请求后都会交给一个线程来读写数据。

2. 互斥锁

synchronized关键字和ReentrantLock都提供了互斥锁的实现。

3. 读写分离

是一种读多写少场景下的优化策略,在Java中的CopyOnWriteArrayList就是一个典型的实现,写操作时会提供原始数据的副本,在副本上执行更新操作,修改完后将副本替换原有数据。复制和替换时仍然需要互斥锁来保证线程安全。读操作无需加锁。

image.png

4. 原子操作类

Java提供了AtomicInteger等原子累保证线程安全,底层使用了CAS。

5. 不可变类

不可变类是指一旦创建就不能被修改的类,String和不可变类型的封装类都将class和成员变量声明成了final来保证线程安全,修改方法例如String的replace方法其实已经创建了一个新的对象,而不是在原有对象上修改。

6. 创建独立副本ThreadLocal

ThreadLocal存储变量的副本,每个线程都拥有自己独立的副本,一个线程的修改不会影响其他线程的副本,使用ThreadLocal操作的都是数据的副本,所以保证了数据安全的同时也牺牲了数据共享的灵活性。常用于保存请求上下文和数据库的连接池场景中。

常见安全问题解决方案

2.1 安全性问题/竞态问题解决方案

解决竞态问题的关键是保证访问顺序,因此我们可以通过使用锁、信号量、互斥量等同步机制来保证共享资源的访问顺序。

2.2 死锁解决方案

死锁的解决分为预防死锁、避免死锁、检测死锁、解除死锁。可以设置超时时间等。

2.3 活锁解决方案

活锁如果是因为2个线程重复的获取释放锁,可以通过给予随机时间获取锁的方式,让2个线程可以公平的获取锁,或者可以让获取失败的线程加入另一个队列进行处理。不是重复之前操作原先队列。

2.4 饥饿解决方案

使用优先级队列的资源调度算法或遵循轮训机制模式等,总之需要提供使用资源的机会。

2.5 相关概念

  • JUC

全称java.util.concurrent,为了更好支持并发让开发者方便开发提供的工具包。

  • JMM

全称Java Memory Model内存模型。可以把JMM看作是Java定义的并发编程相关的一组规范,该内存模型屏蔽系统和硬件的差异,实现了在Java中编写一次随处运行。

参考:

  1. 《Java并发编程实战》