Java并发编程(十一)-- Java中的锁详解
上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁。
synchronized锁
synchronized锁是什么?
synchronized是Java的一个关键字,它能够将代码块(方法)锁起来
- 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~
public synchronized void test() {
....
}
synchronized是一种互斥锁
- 一次只能允许一个线程进入被锁住的代码块
- synchronized是一种内置锁/监视器锁
- Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用**对象的内置锁(监视器)**来将代码块(方法)锁定的!
synchronized用处是什么?
- synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
- synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)
Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。
synchronized的原理
我们首先来看一段synchronized修饰方法和代码块的代码:
public class Main {
//修饰方法
public synchronized void test1(){ } public void test2(){
// 修饰代码块
synchronized (this){ }
}
}
同步代码块:
- monitorenter和monitorexit指令实现的
同步方法(在这看不出来需要看JVM底层实现)
- 方法修饰符上的ACC_SYNCHRONIZED实现。
synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
synchronized如何使用
synchronized一般我们用来修饰三种东西:
- 修饰普通方法
- 修饰代码块
- 修饰静态方法
修饰普通方法:
用的锁是对象(内置锁)
public class Test1{ // 修饰普通方法
public synchronized void test() {
// doSomething
}
}
修饰代码块:
用的锁是对象(内置锁)--->this
public class Test2 { public void test() {
// 修饰代码块
synchronized (this){
// doSomething
}
}
}
当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)
修饰静态方法
获取到的是类锁(类的字节码文件对象):
public class Test3 { // 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)
public synchronized void test() {
// doSomething
}
}
类锁与对象锁
synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。
- 它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!
public class SynchoronizedDemo { //synchronized修饰非静态方法
public synchronized void function() throws InterruptedException {
for (int i = 0; i <3; i++) {
Thread.sleep(1000);
System.out.println("function running...");
}
}
//synchronized修饰静态方法
public static synchronized void staticFunction()
throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Static function running...");
}
} public static void main(String[] args) {
final SynchoronizedDemo demo = new SynchoronizedDemo(); // 创建线程执行静态方法
Thread t1 = new Thread(() -> {
try {
staticFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 创建线程执行实例方法
Thread t2 = new Thread(() -> {
try {
demo.function();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动
t1.start();
t2.start();
}
}
结果证明:类锁和对象锁是不会冲突的!
重入锁
我们来看下面的代码:
public class Widget { // 锁住了
public synchronized void doSomething() {
...
}
} public class LoggingWidget extends Widget { // 锁住了
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
- 当线程A进入到LoggingWidget的
doSomething()
方法时,此时拿到了LoggingWidget实例对象的锁。 - 随后在方法上又调用了父类Widget的
doSomething()
方法,它又是被synchronized修饰。 - 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的
doSomething()
方法还需要一把锁吗?
不需要的!
因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续**“开锁”**进去的!
这就是内置锁的可重入性。
释放锁的时机
- 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
- 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
- 不会由于异常导致出现死锁现象~
Lock显式锁
Lock显式锁简介
Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~
Lock显式锁是一个接口,我们来看看:
随便翻译一下他的顶部注释,看看是干嘛用的:
简单概括一下:
- Lock方式来获取锁支持中断、超时不获取、是非阻塞的
- 提高了语义化,哪里加锁,哪里解锁都得写出来
- Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
- 支持Condition条件对象
- 允许多个读线程同时访问共享资源
synchronized锁和Lock锁使用哪个
前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??
必须是有的!!Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化
所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)
所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~
总得来说:
- synchronized好用,简单,性能不差
- 没有使用到Lock显式锁的特性就不要使用Lock锁了。
示例
有一个购买行为事务,需要更新数据库,通常的sql语句是:
update item set amount = amount - 1 where item_id = 1;
然而当amount只有1个的时候,同时有两个顾客进入了事务进行购买行为会如何,最后amount=-1,两个顾客都获得了这个商品,这显然不合理,那么该如何解决这一问题呢?
网上查了一些方法,发现了两个我认为比较不错的方法:乐观锁和悲观锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
下面来简单介绍一下他们的执行原理。
1. 乐观锁
1) 概念: 在执行修改操作时不判断是否存在冲突,而是到了操作完成后再判断是否存在冲突,如有冲突则回滚
2) 适用情况: 一般适用于回滚代价低,且冲突较少的情况.
3) 优点: 执行操作时不会造成阻塞
4) 缺点: 如果冲突较多,将造成较多的回滚操作
5) 实现: 一般使用版本控制的方式实现
6) 实现例子:
begin;
select @cur_amount := amount from item where item_id = 1;
update item set amount = amount - 1 where amount = @cur_amount and item_id = 1;
commit;
// 最后根据对数据更新行数是否为1来告诉用户是否购买成功。
这是使用乐观锁来更新的例子, @cur_amount是本次事务A获得的数量, 例如为 1, 而另外一个事务B假如于事务A之前执行完了跟新操作, 那么此时数据库中的amount将变为0.
那么事务A的 update 中语句 amount = @cur_amount 将是 0 = 1 而不成立, 所以此次购买会失败
我这里只是一个简单的例子,实际操作中,可能这个事务中间会进行大量操作,而最后会因为amount != @cur_amount 而回滚
2. 悲观锁
1) 概念: 在执行操作前就进行是否需要锁的判断,若有操作正在执行,则需要等待锁释放。
2) 适用情况: 冲突较多的情况
3) 优点: 不需要进行回滚
4) 缺点: 需要串行执行, 会造成阻塞
5) 实现: 使用排他锁
6) 实现例子:
begin;
select amount from item where item_id = 1 for update;
// 通过amount来做出一些行为,例如告诉用户库存不足,购买失败,然后只有amount > 1才进入更新库存操作
update item set amount = amount - 1 where item_id = 1;
commit;
由于是串行执行,其他事务的for update必须等该当前事务的for update语句执行,所以我们不必担心我们获得的amount被修改过,因为它永远是最新的
事实上,对于这种购买行为的最佳解决方案是
update item set amount = amount - 1 where item_id = 1 and amount > 0;
由于update操作在repeatable-read中是串行执行的,所以我们大可以不加锁,直接这么一句就解决了,当然这是一种特例。
参考资料:
- 《Java核心技术卷一》
- 《Java并发编程实战》
- 《Java并发编程的艺术》
Java并发编程(十一)-- Java中的锁详解的更多相关文章
- Java并发编程:线程封闭和ThreadLocal详解
转载请标明出处: http://blog.csdn.net/forezp/article/details/77620769 本文出自方志朋的博客 什么是线程封闭 当访问共享变量时,往往需要加锁来保证数 ...
- Java并发编程(3) JUC中的锁
一 前言 前面已经说到JUC中的锁主要是基于AQS实现,而AQS(AQS的内部结构 .AQS的设计与实现)在前面已经简单介绍过了.今天记录下JUC包下的锁是怎么基于AQS上实现的 二 同步锁 同步锁不 ...
- Java并发编程3-抽象同步队列AQS详解
AQS是AtractQueuedSynchronizer(队列同步器)的简写,是用来构建锁或其他同步组件的基础框架.主要通过一个int类型的state来表示同步状态,内部有一个FIFO的同步队列来实现 ...
- Java并发编程系列-(4) 显式锁与AQS
4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...
- Java并发编程:Java的四种线程池的使用,以及自定义线程工厂
目录 引言 四种线程池 newCachedThreadPool:可缓存的线程池 newFixedThreadPool:定长线程池 newSingleThreadExecutor:单线程线程池 newS ...
- “全栈2019”Java多线程第十七章:同步锁详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第十一章:线程优先级详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- [转载] java并发编程:Lock(线程锁)
作者:海子 原文链接: http://www.cnblogs.com/dolphin0520/p/3923167.html 出处:http://www.cnblogs.com/dolphin0520/ ...
- Java并发编程(1)-Java内存模型
本文主要是学习Java内存模型的笔记以及加上自己的一些案例分享,如有错误之处请指出. 一 Java内存模型的基础 1.并发编程模型的两个问题 在并发编程中,需要了解并会处理这两个关键问题: 1.1.线 ...
随机推荐
- cf1131f 构造+并查集
#include<bits/stdc++.h> using namespace std; #define maxn 150005 int f[maxn],n; vector<int& ...
- 刚发了两个关于极光推送的网上Demo,再次自己结合官网总结一下,以便加深印象
简单源码如下: //Map<String, String> parm是我自己传过来的参数,同学们可以自定义参数public static void jpushAndroid(Map< ...
- SpringMVC 框架完成图片上传到项目路径操作
/** * 保存添加 * * @return */ @RequestMapping(value = "taizhang/add.action", method = { Reques ...
- 饮冰三年-人工智能-Python-13Python基础之运算符与数据类型
1:算数运算符 + - * / ** % // 2: 成员运算符 in not in name = """张三""" if "张& ...
- 如何查看响应端口号被个程序占用(Windows)
我们以80端口为例,在dos输入命令“ netstat -aon|findstr "80" 后按回车显示如下,可以看到占用80端口对应的程序的PID号为1752 ...
- OpenCV-Python教程8-图像混合
一.图片相加 要叠加两张图片,使用cv2.add(),相加两幅图片的形状(高度.宽度.通道数)必须相同.numpy中可以直接用res = img1 + img2相加.但是两者的结果并不相同 impor ...
- 支持向量机-SMO算法简化版
SMO:序列最小优化 SMO算法:将大优化问题分解为多个小优化问题来求解 SMO算法的目标是求出一系列的alpha和b,一旦求出这些alpha,就很容易计算出权重向量w,并得到分隔超平面 工作原理:每 ...
- android开发默认图标怎么换?
首先要在资源文件放入想换的图标图片拖到drawable-XX文件夹下, 然后打开AndroidManifest.xml这个配置清单文件找 到application标签里的这句android:icon= ...
- C#矩阵求逆
来源:http://zhidao.baidu.com/link?url=DiqAbq9YUYn3z7QjxGGoF0PLZwN-Y9ecqKB7Gy38JWRD1riMIYukVKXKq88pxtWL ...
- java传值和传引用区别
1. 在java中所有的参数都是传值的,引用符号&的传递是C++中才有的:2. 在java传参中,基本类型(byte--short--int--long--float--double--boo ...