java线程:锁
锁,是一种很常见的东西。生活中的锁,是一把铁锁,牢牢锁住自己的东西。而线程上的锁呢?
线程问题
线程安全问题主要表现为:
- 原子性
- 可见性
- 有序性
这几个方面相互有区别,又相互有联系。
- 原子性的保障能够消除静态。
- 可见性描述了一个线程对共享变量的更新对于另一个线程而言是否可见
- 原子性和可见性一桶得以暴涨了一个线程能够共享变量的相对新值
- 有序性描述了一个处理器上运行的一个线程对共享变量所做的更新,在另一个处理器上运行的其他线程看来,这些线程是以什么样的顺序观察到这些更新的问题。
总而言之,可见性是有序性的基础,而有序性有可能影响可见性
线程问题下面又有一个调度问题,总结来说调度分为两种调度策略
- 公平调度策略 -> 吞吐率较小 -> 不会导致饥饿
- 非公平调度策略 -> 吞吐率较大 -> 可能会导致饥饿
线程安全性问题的发生,是因为缺少了一样东西线程同步机制
。
线程同步机制
是一套用于协调线程见的数据访问及活动的机制。
而锁,正是其中一个机制。上面讲述了基本的一些问题,下面进入主题
概述
多个线程对共享变量进行访问的时候,很容易引起线程性的问题,也就是线程并发访问共享变量
。一句话,共享变量
这个东西,确实是一个很麻烦的东西。我们要解决线程问题,那就必须解决共享变量的问题
举个例子:有一份共享变量,有多个线程在访问他。由于每个线程都是争夺式访问共享变量,那么可能有一个线程获取了该共享变量,然后需要拿去修改,正想要刷新数据,这个时候第二个线程又抢到了共享变量,在第一个线程更改了变量但是没返回更改后的值前读取了共享变量,这个时候,这个线程读取到的是一个脏的数据。因此,这就导致了线程的问题。
对于这个问题,我们是否可以解决?很容易,我们能够想到,当我们在共享变量外加个锁,就可以了。一个粗俗的例子就是,你在做事情的时候,突然有一种不太好的感觉。这个时候你就想去上厕所了。厕所是公共厕所,你进去了,在门那里加把锁。当你使用完了厕所之后,把锁去掉,出去了,继续干你的事情。这个例子中,厕所可以理解为共享变量,上厕所的人可以理解为线程。
换句话说,也就是将多个线程对共享变量数据的并发方位转换为串行访问。这也是锁的最简单是思路了。
根据 JAVA 虚拟机对锁对实现方式划分,可以划分为:
- 内部锁:synchronized
- 显式锁:java.concurrent.locks.ReentrantLock 类
锁的作用
简单来说:锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性
原子性如何保障?
一段代码如果加了锁,那么这一段代码每一次的访问就只能被一个线程所访问,一个线程只有获得了锁之后,才能对这段代码所访问。
比如这段代码
LOCK
a=b+2;
flag=true;
c=3
UNLOCK
当我们有线程想要访问这三个值的时候,当线程读取到flag=true
的时候,其他两个也必然成立,a 永远是比 b 大 2,c 永远是等于 3。在 LOCK 和 UNLOCK 这个区域内,称为临界区。对于其他线程来说,这个临界区是具有原子性的。也就是在读线程看来,这些变量就像是在同一时刻被更新的,如果是其他的代码,那就能够表明这些执行的操作都可以被简化为一个操作。
如何保障可见性?
可见性的保障是通过写线程冲刷处理器缓存
和读线程刷新处理器缓存
这两个动作实现的
锁的释放隐含着冲刷处理器缓存这个动作。也就能够保障可见性
如何保障有序性?
还是依照上面原子性的那个例子。因为写线程对那些共享变量的更新会同时对读线程课件,也就是在读线程看来,这些变量更像是在同一时刻被更新的。这也就意味着,读线程可以认为写线程是按照源代码顺序更新上述共享变量的。因此也就使得有序性得到保障。
LOCK
a=b+2;
flag=true;
c=3
UNLOCK
即使能够保障有序性,但是也没法保障代码区内的内存操作不被重排序。对于上面的例子,有可能是先执行
a=b+2
,也可能先执行flag=true
或者c=3
。但是由于锁让代码在临界区内保证了原子性,所以这个重排序就被视而不见了。
条件
锁对于保障原子性,可见性,有序性是有两个条件的
- 线程访问
同一个共享资源
的时候必须使用的是同一个
锁 - 线程对共享资源无论是读还是写,都必须持有该锁
对于锁保障的三个特性,也必须是在同一个锁上面才能够执行的,否则不成立。
锁对几个概念
- 可重入性
- 锁对争用与调度
- 锁对粒度
借鉴《java多线程编程实战指南》
出自
如需转载,请标明出处