天天看点

并发实战——并发相关知识点overview

本章内容:

  • 总结下并发有关问题(以某公司并发编程讲座为参考资料)

1、java的并发模型和操作系统有关么?

操作系统的不同会影响java中并发模型的实现方式。

Java在JVM层面抽象了一套自己的线程机制(里面的规则和操作系统层面的有一定相似性),用以映射不同的操作系统的任务调度。

譬如早期不支持线程的操作系统,jvm在操作系统上面是一个进程,当这个进程被操作系统调度到后,jvm内部实现的线程库再调度java线程。

之后,当操作系统开始支持多线程(如windows从一开始就支持线程),这样java线程就和操作系统线程一一对应或多多对应了,这个时候,如果是一一对应,那么线程的调度完全交给了操作系统内核,当然jvm还保留一些策略足以影响到其内部的线程调度。

比如线程模型中很多的实现都是native方法,native关键字的函数都是操作系统实现的, java只能调用。

如果一个机器上只有一个逻辑CPU,此时系统调度器通过轮换时间片的方式来调度任务,所以不存在真正的并行,只是并发;只有多个逻辑CPU的情形下,才会出现并行。

http://blog.csdn.net/gatieme/article/details/51481863

2、并发相关特性和解决办法

Visibility:通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到。volatile

Ordering:通过java提供的同步机制或volatile关键字(也可以是实现Visibility), 来保证内存的访问顺序。

Cache coherency :它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。

Happens-before :synchronized,volatile,final,java.util.concurrent.lock,atomic

Java存储模型有一个happens-before原则,就是如果动作B要看到动作A的执行结果(无论A/B是否在同一个线程里面执行),那么A/B就需要满足happens-before关系。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

开始接触happens-before规则和指令重排序的时候,着实乱了以阵子,尤其是happens-before规则的第一条,明明说了同一个线程中每个动作都happens-before其后一个动作,而指令重排序又会对没有依赖的两个操作进行重排序,这不是相互矛盾的么?

经过网上翻阅了一些资料以后,我的理解是,happens-before规则是用来判断一个动作对另一个动作是否可见的法则,他只是用来判断可见性的,而不是决定执行顺序的,就是说动作A和动作B 的执行顺序是可以通过指令重排发生变化的,而如果你要保证A和B的可见性关系,就必须采用其他控制手段来保证AB的执行顺序不被打乱,这样就能用happens-before规则来判断AB两个动作的可见性。

3、分离锁

并发实战——并发相关知识点overview

分离锁负面作用:对容器加锁,进行独占访问更加困难,并且更加昂贵了。

内存使用的问题:sina就曾经因为在Action层使用ConcurrentHashMap而导致内存使用过大,修改array后竟然单台服务器节省2G。

https://www.ibm.com/developerworks/cn/java/j-lo-lock/(如何更聪明地使用锁)

4、volatile关键字:

volatile关键字:

1:简化实现或者同步策略验证的时候来使用它;

2:确保引用对象的可见性;

3:标示重要的生命周期的事件,例如:开始或者关闭。

脆弱的volatile的使用条件:

1:写入变量不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值;

2:变量不需要和其他变量共同参与不变约束;

3:访问变量时不需要其他原因需要加锁。

Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件即变量真正独立于其他变量和自己以前的值 ,在某些情况下可以使用 volatile 代替 synchronized 来简化代码。

http://blog.csdn.net/lazyer_dog/article/details/52345914

5、几个建议

对共享可变数据同步访问;

避免过多的同步;

永远不要在循环外面调用wait;

不要依赖于线程调度器;

线程安全的文档化;

避免使用线程组。

6、JUC包

并发实战——并发相关知识点overview
Executors

Executor

ExecutorService

ScheduledExecutorService

Callable

Future

ScheduledFuture

Delayed

CompletionService

ThreadPoolExecutor

ScheduledThreadPoolExecutor

AbstractExecutorService

Executors

FutureTask

ExecutorCompletionService

Queues

BlockingQueue

ConcurrentLinkedQueue

LinkedBlockingQueue

ArrayBlockingQueue

SynchronousQueue

PriorityBlockingQueue

DelayQueue

Concurrent Collections

ConcurrentMap

ConcurrentHashMap

CopyOnWriteArray{List,Set}

Synchronizers

CountDownLatch

Semaphore

Exchanger

CyclicBarrier

Timing

TimeUnit

Locks

Lock

Condition

ReadWriteLock

AbstractQueuedSynchronizer

LockSupport

ReentrantLock

ReentrantReadWriteLock

Atomics

Atomic[Type], Atomic[Type]Array

Atomic[Type]FieldUpdater

Atomic{Markable,Stampable}Reference

7、线程池

1:线程池的大小最好是设定好,因为JDK的管理内存毕竟是有限的;

2:使用结束,需要关闭线程池;

3: Runtime.getRuntime().addShutdownHook(hook); 对不能正常关闭的

线程做好相关的记录。

8、任务池

任务池: ScheduledExecutorService

9、阻塞队列:BlockingQueue

生产者消费者模型中,会用到阻塞队列。

各种阻塞队列:

ArrayBlockingQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。

Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素.

LinkedBlockingDeque一个基于已链接节点的、任选范围的阻塞双端队列。

LinkedBlockingQueue一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素

PriorityBlockingQueue一个无界阻塞队列,它使用与类 PriorityQueue 相同的顺序规则,并且提供了阻塞获取操作。

SynchronousQueue一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。

10、ConcurrentHashMap

并发实战——并发相关知识点overview

ConcurrentHashMap主要的知识点:锁分离,hash算法

11、CopyOnWriteArray{List,Set}

当读操作远远大于写操作的时候,考虑用这个并发集合。例如:维护监听器的集合。注意:其频繁写的效率可能低的惊人。

12、四大同步器

同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger

并发实战——并发相关知识点overview
并发实战——并发相关知识点overview
并发实战——并发相关知识点overview

13、互斥锁: ReentrantLock

Lock 更加灵活,性能更好

interface Lock {
        void lock();
        void lockInterruptibly() throws InterruptedException;
        boolean tryLock();
        boolean tryLock(long timeout, TimeUnit unit)
                    throws InterruptedException;
        void unlock();
        Condition newCondition();
    }
           

支持多个Condition

可以不以代码块的方式上锁

可以使用tryLock,并指定等待上锁的超时时间

调试时可以看到内部的owner thread,方便排除死锁

RenntrantLock支持fair和unfair两种模式

14、读写锁: ReadWriteLock

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

15、AbstractQueuedSynchronizer

  • 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架;
  • 基于JDK底层来操控线程调度,LockSupport.park和LockSupport.unpark;
  • 此类支持默认的独占 模式和共享 模式之一,或者二者都支持;
  • 如果想玩玩它,请看CountDownLatch的源码。
  • 当然,也可以去FutureTask这个类看看。
  • -

16、CAS

乐观锁

处理器指令,全称Compare and swap,原子化的读-改-写指令

处理竞争策略:单个线程胜出,其他线程失败,但不会挂起

目前的多处理器系统基本都支持原子指令,典型模式:首先从V中读取值B,由A生成新值B,然后使用CAS原子化地把V的值由A修改成B,并且期间不能有其他线程修改V的值,CAS能够发现来自其他线程的干扰,所以即使不使用锁,也能解决原子化地读-写-改的问题

非阻塞算法

  • 一个线程的失败或挂起不影响其他线程
  • J.U.C的非阻塞算法依赖Atomic
  • 算法里一般采用回退机制处理Atomic的CAS竞争
  • 对死锁免疫的同步机制
  • 目标:相对阻塞算法,减少线程切换开销、减少锁的竞争等。
  • 也是Lock-free,即无锁编程

编程实践

使用更优的锁

缩小锁的范围

  • 减少线程把持锁的时间
  • 避免在临界区进行耗时计算
  • 常见做法1:缩小同步快
  • 常见做法2:把一个同步块分拆成多个
  • 需要保证业务逻辑正确为前提

避免热点域

  • 热点域:每次操作,都需要访问修改的fields
  • eg. ConcurrentHashMap的size计算问题

使用不变和Thread Local 的数据

  • 不变数据,即Immutable类型数据、在其生命周期中始终保持不变,可以安全地在每个线程中复制一份以便快速读取。
  • ThreadLocal数据,只被线程本身使用,因此不存在不同线程之间的共享数据的问题。

使用高并发容器

高速缓存计算结果

安全发布

利用成熟的框架

ThreadLocal

http://blog.csdn.net/youyou1543724847/article/details/52460852

继续阅读