java 并发——内置锁
坚持学习,总会有一些不一样的东西。
# 一、由单例模式引入
引用一下百度百科的定义——
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
文字定义总是很含糊,举个反例就很清楚了,想起之前总结过单例模式,就从单例模式开始吧。如果不清楚单例模式的新同学,可以看一下这篇总结:
[java中全面的单例模式多种实现方式总结](https://www.cnblogs.com/joy99/p/9859764.html)
单例模式中,懒汉式的实现方案如下:
```
public class Singleton {
private Singleton() {
}
private static Singleton sSingleton;
public static Singleton getInstance() {
if (sSingleton == null) {
sSingleton = new Singleton();
}
return sSingleton;
}
}
该方法在单线程中运行是没有问题的,但是在多线程中,某些情况下,多个线程同时都判断到 `sSingleton == null`,然后又都执行 `sSingleton = new Singleton()`,这样就不能保证单例了,我们说它不是线程安全的。
一种改进方法:
public class Singleton {
private Singleton() {
}
private static Singleton sSingleton;
public synchronized static Singleton getInstance() {
if (sSingleton == null) {
sSingleton = new Singleton();
}
return sSingleton;
}
}
上面这种实现,实际上效率非常低,是完全不推荐使用的。主要是因为加了 `sychronized` 关键字,意为同步的,也就是内置锁。使用 `synchronized` 关键字修饰方法, 是对该方法加锁,这样在同一时刻,只有一个线程能进入该方法,这样保证了线程安全,但是也正因为如此,效率变得很低,因为当对象创建之后,再次调用该方法的时候,直接使用对象就可以了,无需再同步了。于是有了下面改进的实现方式—— DCL(双重检查锁):
public class Singleton {
private Singleton() {
}
/**
* volatile is since JDK5
*/
private static volatile Singleton sSingleton;
public static Singleton getInstance() {
if (sSingleton == null) {
synchronized (Singleton.class) {
// 未初始化,则初始instance变量
if (sSingleton == null) {
sSingleton = new Singleton();
}
}
}
return sSingleton;
}
}
`sSingleton = new Singleton() ` 不是一个原子操作。故须加 volatile 关键字修饰,该关键字在 jdk1.5 之后版本才有。下面就来说说 `synchronized` 和 `volatile` 这两个关键字。
# 二、同步与 synchronized
## synchronized 锁代码块
java 提供了一种一种内置锁,来实现同步代码块,同步代码块包含两个部分:一个作为锁的对象引用,一个由锁保护的代码块。形式如下:
synchronized (lock) {
// 由锁保护的代码块
}
每个 java 对象都可以作为实现同步的锁,java 的内置锁也称为互斥锁。同一时刻只能有一个线程获得该锁,获得该锁的线程才能进入由锁保护的代码块,其它线程只能等待该线程执行完代码块之后,释放该锁后,再去获得该锁。例子:
public class SynchronizedDemo1 {
private Object lock = new Object();
public static void main(String[] args) {
SynchronizedDemo1 demo = new SynchronizedDemo1();
new Thread(() -> {
try {
demo.test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
demo.test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test1() throws InterruptedException {
System.out.println("--- test1 begin - current thread: " + Thread.currentThread().getId());
Thread.sleep(1000);
synchronized (lock) {
System.out.println("--- test1 synchronized - current thread: " + Thread.currentThread().getId());
Thread.sleep(5000);
}
System.out.println("--- test1 end - current thread: " + Thread.currentThread().getId());
}
}
执行结果:
![](https://img2018.cnblogs.com/blog/758949/201810/758949-20181030215303621-302045014.gif)
从结果可以清楚地看到一个线程进入同步代码块之后,另一个线程阻塞了,需要等到前者释放锁之后,它获得锁了才能进入同步代码块。
上面代码中,我们创建的 `lock` 对象作为锁。用 `synchronized` 修饰方法又是什么充当了锁呢?
## synchronized 修饰方法
以关键字 `synchronized` 修饰的方法就是一种横跨整个方法的同步代码块,其中该同步代码块的锁就是调用该方法的对象。
class A {
public synchronized void a(){
System.out.println("hello");
}
}
等价于
class A {
public void a(){
synchronized(this) {
System.out.println("hello");
}
}
}
静态方法用 `类名.方法名` 来调用,以关键字 `synchronized` 修饰的静态方法则以 Class 对象作为锁。
class A {
public static synchronized void a(){
System.out.println("hello");
}
}
等价于
class A {
public static void a(){
synchronized(A.class) {
System.out.println("hello");
}
}
}
写个demo测试一下:
public class A {
public static void main(String[] args) {
A obj_a = new A();
new Thread() {
@Override
public void run() {
try {
obj_a.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
@Override
public void run() {
try {
obj_a.b();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread(){
@Override
public void run() {
try {
A.c();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
try {
A.d();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void a() throws InterruptedException {
System.out.println("--- begin a - current Thread " + Thread.currentThread().getId());
Thread.sleep(8000);
System.out.println("--- end a - current Thread " + Thread.currentThread().getId());
}
public synchronized void b() throws InterruptedException {
System.out.println("--- begin b - current Thread " + Thread.currentThread().getId());
Thread.sleep(8000);
System.out.println("--- end b - current Thread " + Thread.currentThread().getId());
}
public synchronized static void c() throws InterruptedException {
System.out.println("--- begin c - current Thread " + Thread.currentThread().getId());
Thread.sleep(5000);
System.out.println("--- end c - current Thread " + Thread.currentThread().getId());
}
public synchronized static void d() throws InterruptedException {
System.out.println("--- begin d - current Thread " + Thread.currentThread().getId());
Thread.sleep(5000);
System.out.println("--- end d - current Thread " + Thread.currentThread().getId());
}
}
运行结果如下:
![](https://img2018.cnblogs.com/blog/758949/201810/758949-20181030215230799-1214901721.gif)
可以看到,由于方法 a 和 方法 b 是同一个锁 `obj_A`,所以当某个线程执行其中一个方法是,其他线程也不能执行另一个方法。但是方法 c 是由 `A.class` 对象锁住的,执行方法 C 的线程与另外两个线程没有互斥关系。
**对于某个类的某个特定对象来说,该类中,所有 synchronized 修饰的非静态方法共享同一个锁,当在对象上调用其任意 synchronized 方法时,此对象都被加锁,此时,其他线程调用该对象上任意的 synchronized 方法只有等到前一个线程方法调用完毕并释放了锁之后才能被调用。
而对于一个类中,所有 synchronized 修饰的静态方法共享同一个锁。**
## 可重入锁
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,然而,内置锁是可重入的,,如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。这也意味着获取锁的操作粒度是“线程”,而不是“调用”。当线程请求一个未被持有的锁时,jvm 将记下锁的持有者(即哪个线程),并获取该锁的计数值为 1 ,当同一个线程再次获取该锁时, jvm 将计数值递增,当线程退出同步代码块时,计数值将递减,当计数值为 0 时,将释放锁。
public class B {
public static void main(String[] args) {
B obj_B = new B();
obj_B.b();
}
private synchronized void a(){
System.out.println("---a");
}
private synchronized void b(){
System.out.println("---b");
a();
}
}
执行上面这段代码,将输出
---b
---a
假设没有可重入的锁,对于对象 `obj_B` 来说,调用 b 方法时,线程将会持有 `obj_B` 这个锁,在方法 b 中调用方法 a 时,将会一直等待方法 b 释放锁,造成死锁的情况。<br/>
《Java 并发编程实战》 中举的可重入锁的例子:
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
我第一遍看这段代码的时候,在思考,这里父类子类的方法都有synchronized同步,当调用子类LoggingWidget的doSomething()时锁对象肯定是当时调用的那个LoggingWidget实例,可是问题是当执行到super.doSomething()时,要调用父类的同步方法,那此时锁对象是谁?是同一个锁进入了 2 次,还是获得了子类对象和父类对象的 2 个不同的锁?
下面这段代码能给出结论:
public class Test {
public static void main(String[] args) throws InterruptedException {
final TestChild t = new TestChild();
new Thread(new Runnable() {
@Override
public void run() {
t.doSomething();
}
}).start();
Thread.sleep(100);
t.doSomethingElse();
}
public synchronized void doSomething() {
System.out.println("something sleepy!");
try {
Thread.sleep(1000);
System.out.println("woke up!");
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
private static class TestChild extends Test {
public void doSomething() {
super.doSomething();
}
public synchronized void doSomethingElse() {
System.out.println("something else");
}
}
}
这段代码输出了:
something sleepy!
woke up!
something else
而不是
something sleepy!
something else
woke up!
这说明是同一个锁进入了 2 次,即调用子类方法的子类对象。而这也正好符合多态的思想,调用 super.doSomething() 方法时,是子类对象调用父类方法。
# 三、同步关键字 volatile
## 原子性
原子是世界上的最小单位,具有不可分割性。 比如 `a=0` 这个操作不可分割,我们说这是一个原子操作。而 `++i` 就不是一个原子操作。它包含了"读取-修改-写入"的操作。
<br/>
同步代码块,可以视作是一个原子操作。Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。比如:`AtomicBoolean` `AtomicInteger` `AtomicLong` 等原子操作类。具体可查阅JDK源码或者参考《ava并发编程的艺术》第7章。
<br/>
下面说说复合操作与线程安全的问题。
我们知道,java 集合框架中的 Vector 类是线程安全的,查看该类的源码发现,很多关键方法都用了`synchronized` 加以修饰。但是实际使用时候,稍有不慎,你会发现,它可能并不是线程安全的。比如在某个类中拓展一下 Vector 的方法,往 vector 中添加一个元素时,先判断该元素是否存在,如果不存在才添加,该方法大概像下面这样:
public class CJUtil{
public void putElement(Vector vector, E x){
boolean has = vector.contains(x);
if(!has){
vector.add(x);
}
}
}
上面这个代码肯定是线程不安全的,但是为什么呢?不是说好,Vector 类是线程安全的吗?上网搜了一下,居然发现关于 Vector 类是不是线程安全的存在争议,然后我看到有人说它不是线程安全的,给出的理由比如像上面这种先判断再添加,或者先判断再删除,是一种复合操作,然后认真地打开 JDK 的源码看了,发现 Vector 类中 `contains` 方法并没有用 `synchronized` 修饰,然后得出了结论,Vector不是线程安全的...
<br/>
事实到底是怎样的呢?我们假设 Vector 类的 `contains` 也用 `synchronized` 关键字加锁同步了,此时有两个线程 tA 和 tB 同时访问这个方法,tA 调用到 `contains` 方法的时候,tB 阻塞, tA 执行完 `contains` 方法,返回 false 后,释放了锁,在 tA 执行 `add` 之前,tB 抢到了锁,执行了 `contains` 方法,tA 阻塞。对于同一个元素, tb 判断也不包含,后面, tA 和 tB 都向 Vector 添加了这个元素。经过分析,我们发现,对于上述复合操作线程不安全的原因,并非是其中单个操作没有加锁同步造成的。
<br/>
那如何解决这个问题呢?可能马上会想到,给 putElement 方法加上 `synchronized` 同步。
public class CJUtil{
public synchronized void putElement(Vector vector, E x){
boolean has = vector.contains(x);
if(!has){
vector.add(x);
}
}
}
<br/>
这样整个方法视为一个原子操作,只有当 tA 执行完整个方法后,tB 才能进入,也就不存在上面说的问题了。其实,这只是假象。这种在加锁的方法,并不能保证线程安全。我们可以从两个方面来分析一下:
1. 从上文我们知道,给方法加锁,锁对象,是调用该方法的对象。这和我们操作 Vector 方法的锁并不是同一个锁。我们虽然保证了只有一个线程能够进入到 `putElement` 方法去操作 vector,但是我们没法保证其它线程通过其它方法不去操作这个 vector 。
2. 上一条中,只有一个线程能够进入到 `putElement` 方法,是不准确的,因为这个方法不是静态的,如果在两个线程中,分别用 `CJUtil` 的两个不同的实例对象,是可以同时进入到 `putElement` 方法的。
<br/>
正确的做法应该是:
public class CJUtil{
public void putElement(Vector vector, E x){
synchronized(vector){
boolean has = vector.contains(x);
if(!has){
vector.add(x);
}
}
}
}
## 重排序
重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序,分别对应编译时和运行时环境。<br/>
不要假设指令执行的顺序,因为根本无法预知不同线程之间的指令会以何种顺序执行。<br/>
编译器重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能的减少寄存器的读取、存储次数,充分复用寄存器的存储值。
`int a = 5;①` `int b = 10;②` `int c = a + 1;③` 假设用的同一个寄存器
这三条语句,如果按照顺序一致性,执行顺序为①②③寄存器要被读写三次;但为了降低重复读写的开销,编译器会交换第二和第三的位置,即执行顺序为①③②
## 可见性
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
<br/>
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
下面这段代码:
public class A {
private static boolean flag = false;
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
while (!flag) {
}
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
}
看执行结果:
![](https://img2018.cnblogs.com/blog/758949/201810/758949-20181031005413812-1172266332.gif)
可以看到程序并没有像我们所期待的那样,在一秒之后,退出,而是一直处于循环中。
下面给 `flag` 加上 `volatile` 关键修饰:
public class A {
private static volatile boolean flag = false;
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
while (!flag) {
}
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
}
再看结果:
![](https://img2018.cnblogs.com/blog/758949/201810/758949-20181031005514488-1468399450.gif)
<br/>
结果表明,没有用 `volatile` 修饰 `flag` 之前,改变了不具有可见性,一个线程将它的值改变后,另一个线程却 “不知道”,所以程序没有退出。当把变量声明为 `volatile` 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。`volatile` 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
![](https://img2018.cnblogs.com/blog/758949/201810/758949-20181031005534045-1648870158.png)
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
volatile 修饰的遍历具有如下特性:
1. 保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2. 禁止指令重排序优化。
3. 不会阻塞线程。
## synchronized 与可见性
细心的人应该发现了,上面代码中的循环是一个空循环,我试着去掉 `volatile` 关键字,在循环里面加了一条打印信息,如下:
public class A {
private static boolean flag = false;
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
while (!flag) {
System.out.println("---");
}
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
}
结果会是怎样,会一直打印 "---" 吗?看结果:
![](https://img2018.cnblogs.com/blog/758949/201810/758949-20181031005555989-969788166.gif)
奇怪了,为什么没有使用 `volatile` 关键字,一秒之后程序也推出了。点击查看 `System.out.println(String x)` 的源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
我们发现,该方法加锁同步了。
那么问题来了,synchronized 到底干了什么。。
按理说,synchronized 只会保证该同步块中的变量的可见性,发生变化后立即同步到主存,但是,flag 变量并不在同步块中,实际上,JVM对于现代的机器做了最大程度的优化,也就是说,最大程度的保障了线程和主存之间的及时的同步,也就是相当于虚拟机尽可能的帮我们加了个volatile,但是,当CPU被一直占用的时候,同步就会出现不及时,也就出现了后台线程一直不结束的情况。<br/>
参考书籍:
《Java 并发编程实战》
《Java 编程思想 第四版》
java 并发——内置锁的更多相关文章
- JAVA并发-内置锁和ThreadLocal
上一篇博客讲过,当多个线程访问共享的可变变量的时候,可以使用锁来进行线程同步.那么如果线程安全性存在的3个前提条件不同时存在的话,自然就不需要考虑线程安全性了.或者说如果我们能够将某个共享变量变为局部 ...
- java synchronized内置锁的可重入性和分析总结
最近在读<<Java并发编程实践>>,在第二章中线程安全中降到线程锁的重进入(Reentrancy) 当一个线程请求其它的线程已经占有的锁时,请求线程将被阻塞.然而内部锁是可重 ...
- Java 并发:内置锁 Synchronized
摘要: 在多线程编程中,线程安全问题是一个最为关键的问题,其核心概念就在于正确性,即当多个线程訪问某一共享.可变数据时,始终都不会导致数据破坏以及其它不该出现的结果. 而全部的并发模式在解决问题时,採 ...
- Java内置锁和简单用法
一.简单的锁知识 关于内置锁 Java具有通过synchronized关键字实现的内置锁,内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁. jav ...
- 转:【Java并发编程】之一:可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- 【Java并发编程】之一:可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- 《java并发编程实战》读书笔记1--线程安全性,内置锁,重入,状态
什么是线程安全? 当多个线程访问某个类时,不管这些的线程的执行顺序如何,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 哈哈书上的解释,还是翻译过 ...
- java并发编程(一)可重入内置锁
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...
- 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)
多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢? ...
随机推荐
- zabbix服务器搭建
zabbix服务器源码安装 参看官方文档 这里不做过多的翻译,我的系统是centos6.5,安装的时候是base安装,所以要装一些其他依赖包,除此之外,还有一些php插件: extension=bcm ...
- C# 创建XML文件
private void CreateXMLFile(string pathAndFileName) { XmlDocument doc = new XmlDocument(); XmlElement ...
- 如何在ChemDraw中打出符号π
很多人日常使用ChemDraw是一款非常优秀的化学绘图软件,在其绘制化学结构式或者反应式的过程中,常常需要添加各种符号.比如有的用户会需要输入希腊字符π,但是不知道用什么方法添加.本教程就来给大家介绍 ...
- superresolution_v_2.0 Application超分辨率程序文档
SUPERRESOLUTION GRAPHICAL USER INTERFACE DOCUMENTATION Contents 1.- How to use this application. 2.- ...
- 多线程环境下调用 HttpWebRequest 并发连接限制
.net 的 HttpWebRequest 或者 WebClient 在多线程情况下存在并发连接限制,这个限制在桌面操作系统如 windows xp , windows 7 下默认是2,在服务器操作 ...
- XDocument简单入门
[+] 1.什么是XML? 2.XDocument和XmlDocument的区别? 3.XDocument 4.XmlDocument 5.LINQ to XML 6.XML序列化与反序列化 因为 ...
- GitHub Pages站点官方宣布开始使用HTTPS
导读 数百万人依靠GitHub Pages,将其作为他们的网站主机,除此之外,还有数百万人每天访问这些网站.为了更好地保护到GitHub Pages站点的通讯,也为了鼓励在因特网上更广泛地采用HTTP ...
- JavaScript匿名函数的使用
JavaScript匿名函数的使用: http://www.cnblogs.com/skykang/archive/2010/12/03/1895274.html 一.什么是匿名函数? 在Javas ...
- html的table列表根据奇数还是偶数行显示不同颜色
<tr <s:if test="#sts.even"> class="table_1" onMouseOut="this.class ...
- 170317、到底什么时候该使用MQ?
一.缘起 一切脱离业务的架构设计与新技术引入都是耍流氓. 引入一个技术之前,首先应该解答的问题是,这个技术解决什么问题. 就像微服务分层架构之前,应该首先回答,为什么要引入微服务,微服务究竟解决什么问 ...