Java 并发问题的产生
Java内存模型(JMM)规定所有的变量都是存在主存当中(类似物理内存),每个线程都有自己的工作内存(类似于高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存,当多个线程同时向主存读取和写入的时候,便会出现并发问题
并发编程中要保证的三个原则
原子性:线程对于内存中的数据的--读取和写入保持原子性
可见性:A线程改变物理内存的值,需要让B线程知道
有序性:代码的执行顺序(因为CUP为了提高代码的执行顺序,会打乱代码的执行顺序,只会保证单线程的最终一致性)
JAVA保证并发编程中的三个概念的机制
-
原子性:使用Lock, synchronized关键字来保证操作的原子性
-
可见性: 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存, 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
-
有序性:在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
happens-before原则
除了上面所说的可以使用volicate和加锁来保证有序性,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序,下面就来具体介绍下happens-before原则(先行发生原则)
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
synchronized:
- 实现原理:(synchronized(object)时)
- 每个对象有一个监视器锁(monitor),线程通过monitorenter和monitorexit命令,获得锁
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,线程已经占有该monitor,只是重新进入则进入的同时monitor的进入数加1
- 如果线程调用monitorexit时,mointor进入数减一,当mointor进入数为0时,其他线程可以进入
详细介绍:https://www.jianshu.com/p/d40eb583f5c4
volatile关键字:
- 使用volatile关键字会强制将修改的值立即写入主存,并让其他线程的运行时内存的该变量值无效(使用时重新读取主存)
- 只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性(写入一致不能保证)
详细介绍:https://www.jianshu.com/p/91f0c6dd2551
Lock:
- 相对与synchronized好处:Lock可以等待一段时间后自动释放锁(例如IO操作时间过长时)
- 相对与synchronized好处:Lock可以知道线程是否获得锁
- 相对与synchronized好处:Lock可以使用读写锁来增加效率
- ReentrantLock的实现原理就是根据AQS的机制实现的
AQS 机制:
- 假设线程A要获取同步状态(这里想象成锁,方便理解),初始状态下state=0,所以线程A可以顺利获取锁,A获取锁后将state置为1。在A没有释放锁期间,线程B也来获取锁,此时因为state=1,表示锁被占用,所以将B的线程信息和等待状态等信息构成出一个Node节点对象,放入同步队列,head和tail分别指向队列的头部和尾部(此时队列中有一个空的Node节点作为头点,head指向这个空节点,空Node的后继节点是B对应的Node节点,tail指向它),同时阻塞线程B(这里的阻塞使用的是LockSupport.park()方法)。后续如果再有线程要获取锁,都会加入队列尾部并阻塞。
CAS的实现原理:
- CAS(比较与设置)主要应用在concurrent包中的AtomicXXX的实现
- CAS通过调用JNI的代码实现的( JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言)
- 所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
- CAS还是会存在ABA问题,这是就需要通过加入版本号来解决该问题
详细介绍:https://www.jianshu.com/p/9a4b9e846ae4
Atomic工具包
- java 在 1.5 版本中提供了 java.util.concurrent.atomic 包,该包下所有的类都是原子操作,其内部使用的原理就是cas算法
并发工具类--- ThreadLocal
- JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序
- 用于线程间的数据隔离。所以ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问
详细地址:https://www.jianshu.com/p/1f61faa88861
并发容器类--- ConcurrentHashMap
- 使用锁分段技术实现线程安全,并且访问高效的HashMap
详细地址:https://www.jianshu.com/p/56fe02f47887
并发容器类--- CopyOnWriteArrayList
- 使用写入时复制的思想实现高性能的ArrayList数据结构
详细地址:https://www.jianshu.com/p/757cb5154319
并发工具类---ThreadPoolExecutors(线程池)
线程池工具类,固定大小线程池,单任务线程池,可变线程池
-
核心组件:
- corePoolSize:核心线程池的大小
- maximumPoolSize:最大线程池的大小
- keepAliveTime:线程没有任务时的存活时间
- workQueue:用于存储任务的,阻塞队列
- threadFactory:线程工厂,用于创建线程
- handler:线程的处理策略()
-
饱和策略:
- AbortPolicy:中止,executor抛出未检查RejectedExecutionException,调用者捕获这个异常,然后自己编写能满足自己需求的处理代码。
- DiscardRunsPolicy:遗弃最旧的,选择丢弃的任务,是本应接下来就执行的任务。
- DiscardPolicy:遗弃会默认放弃最新提交的任务(这个任务不能进入队列等待执行时)
- CallerRunsPolicy:调用者运行,既不会丢弃哪个任务,也不会抛出任何异常,把一些任务推回到调用者那里,以此减缓新任务流。它不会在池线程中执行最新提交的任务,但它会在一个调用了execute的线程中执行。
并发工具类---Semaphore(信号量)
- 主要用于控制多线程对共同资源库访问的限制
- 例如 地下车位,要有空余才能放行, 开会的例子:会议室里等与会人员到齐了会议才能开始
并发工具类---CountDownLatch( 计数器)
- 比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了
并发工具类--- CyclicBarrier
- 类似CountDownLatch,CyclicBarrier可以多次使用,CountDownLatch只能用一次(为0后不可变)
网友评论