Synchronized锁的是什么?
Synchronized锁的是什么?
临界区与锁
并发编程中不可避免的会出现多个线程共享同一个资源的情况,为了防止出现数据不一致情况的发生,人们引入了临界区的概念。临界区是一个用来访问共享资源的代码块,同一时间内只运行一个线程进入。
那么如何实现这个临界区呢?这就用到我们的锁了,当进程想要访问一个临界区时,它先会去看看是否已经有其他线程进入了,也就是看是否能获得锁。如果没有其他线程进入,那么它就进入临界区,其他线程就无法进入,相当于加锁。反之,则会被挂起,处于等待状态,直到其他线程离开临界区,且本线程被JVM选中才可进入(因为可能有其他线程也在等待)。
利用Synchronized解决并发问题
Synchronize是一个重量级锁,它会降低程序性能,因此如果对数据一致性没有要求,就不要使用它。如果方法被Synchronize关键字声明,那么该方法的代码块被视为临界区。当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
下面我们将创建两个线程A,B来同时访问一个对象:A从账户里取钱,B从账户里存钱。首先是不使用Synchronized关键字。
创建账户类
它拥有一个私有变量balance表示金额,addAmount和subtractAmount分别对金额执行加减操作。
public class Account {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void addAmount(double amount){
System.out.println("addAmount start");
double temp=balance;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
temp+=amount;
balance=temp;
System.out.println("addAmount end");
}
public void subtractAmount(double amount){
System.out.println("subtractAmount start");
double temp=balance;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
temp-=amount;
balance=temp;
System.out.println("subtractAmount end");
}
}
创建A,B俩线程,分别对账户存钱和取钱。
public class A implements Runnable {
private Account account;
public A(Account account){
this.account=account;
}
@Override
public void run() {
for(int i=0;i<10;i++){
account.addAmount(1000);
}
}
}
public class B implements Runnable {
private Account account;
public B(Account account){
this.account=account;
}
@Override
public void run() {
for(int i=0;i<10;i++){
account.subtractAmount(1000);
}
}
}
最后在main里面测试
public class Main {
public static void main(String[] args) {
Account account=new Account();
account.setBalance(1000);
A a=new A(account);
Thread ThreadA=new Thread(a);
B b=new B(account);
Thread ThreadB=new Thread(b);
System.out.println("Account Balance:"+account.getBalance());
ThreadA.start();
ThreadB.start();
try {
ThreadA.join();
ThreadB.join();
System.out.println("Account Balance:"+account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadA往账户中执行了10次存入操作,每次存入1000元,ThreadB则是以同样的金额执行了10次取出操作。那么按照我们的推测,最后账户的金额应该维持不变,但程序的结果却不是我们想要的数字。这是为什么呢?因为我们在对数据进行操作的时候,另外一个线程可能也在进行操作,逻辑上应该先后执行的方法变成了同时执行,所以出现了错误。
现在我们给addAmount和subtractAmount加上synchronized关键字,保证数据一致性,这样程序就不会出问题了。
如果是使用synchronize保护代码块,则需要将对象引用作为参数传入。一般来说传入this关键字作为引用执行方法的对象就可以了。
锁的到底是什么?
或许在上面的例子你因为粗心只为其中一个方法加了关键字,那么你会看到这样的现象:

保护代码块要将对象传入,那应该锁的是对象呀。你可能会想:我执行subtractAmout,按道理应该等我执行完addAmount才能执行,它都没有account这个对象的锁,不应该在中间插这么一段呀。但是,只有加了锁的方法,线程执行该方法时才会去尝试获得锁,看看是否有线程进入临界区。访问非同步方法无需获得锁,你把synchronized去掉跟你只加一个的情况是一样的,同步方法与非同步遵循的是不同的规则。也就是说你可以在调用该对象的加了synchronized方法的同时,调用其他的非同步方法。
两个线程怎么同时访问了同一个对象的两个synchronized方法?
你可能在捣鼓这个关键字的时候,惊讶的发现静态方法的与众不同。如果一个对象中的静态方法用synchronized修饰,那么其他线程可以在该静态方法被访问的同时,访问该对象中的非静态方法(当然,该静态方法同一时间只能被一个线程访问)。换句话说,两个线程可以同时访问一个对象中的两个synchronized方法。
等等,不是说锁对象吗?到底锁的是什么?锁的确实是对象,但对于静态方法我们说的是T.class(T 为类名),非静态方法锁的是this ,也就是类的实例对象,两者是不同的。
class T {
// 修饰非静态方法
public synchronized void a() {
// 临界区
}
// 修饰静态方法
public synchronized static void b() {
// 临界区
}
}
上面那段代码相当于:
class T {
// 修饰非静态方法
public synchronized(this) void a() {
// 临界区
}
// 修饰静态方法
public synchronized(T.class) static void b() {
// 临界区
}
}
实际上加锁本质就是在锁对象的对象头中写入当前线程id。我们可以通过下面的代码验证,每次都传入new Object()。
class Account {
private double balance;
public synchronized void addAmount(double amount){
synchronized (new Object()){
System.out.println("addAmount start");
double temp=balance;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
temp+=amount;
balance=temp;
System.out.println("addAmount end");
}
}
public void subtractAmount(double amount){
synchronized (new Object()){
System.out.println("subtractAmount start");
double temp=balance;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
temp-=amount;
balance=temp;
System.out.println("subtractAmount end");
}
}
}
因为线程每次调用方法锁的都是新new的对象,所以加锁无效。甚至编译器可能会将synchronized给优化掉,因为这相当于多把锁保护同一个资源,编译器一看,每个人都弄把锁就进来了,那我还不如不加,反正都一个样。
另外需要注意的是,synchronized是可重入锁。也就是说当线程访问对象的同步方法时,在调用其他同步方法时无需再去获取其访问权。因为我们实际上锁的是对象,对象头里面纪录的都是当前线程的ID。
总结
- 修饰函数,锁的是当前类的实例化对象
- 修饰静态方法,锁的是当前类的Class对象
- 修饰同步代码块,锁的是括号里的对象
加锁实际上就是在锁对象的对象头中写入当前线程id,每个线程要想调用这个同步方法,都会先去锁对象的对象头看看当前线程id是不是自己的。
参考
Synchronized锁的是什么?的更多相关文章
- synchronized锁重入
package synLockIn_1; /* synchronized锁重入,当一个线程得到一个对象锁且还未释放锁时,再次请求此对象锁时可以再次得到该对象的锁 * 此例中线程1进入Service类的 ...
- Java多线程4:synchronized锁机制
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...
- synchronized锁自旋
http://www.jianshu.com/p/5dbb07c8d5d5 原理 通常说的synchronized在方法或块上加锁,这里的锁就是对象锁(当然也可以在类上面),或者叫重量锁,在JVM中又 ...
- 记一次synchronized锁字符串引发的坑兼再谈Java字符串
问题描述 业务有一个需求,我把问题描述一下: 通过代理IP访问国外某网站N,每个IP对应一个固定的网站N的COOKIE,COOKIE有失效时间.并发下,取IP是有一定策略的,取到IP之后拿IP对应的C ...
- 记一次 synchronized 锁字符串引发的坑兼再谈 Java 字符串
业务有一个需求,我把问题描述一下: 通过代理IP访问国外某网站N,每个IP对应一个固定的网站N的COOKIE,COOKIE有失效时间. 并发下,取IP是有一定策略的,取到IP之后拿IP对应的COOKI ...
- Synchronized锁在Spring事务管理下,为啥还线程不安全?
前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 大年初二,朋友问了我一个技术的问题(朋友实在是好学, ...
- Synchronized锁性能优化偏向锁轻量级锁升级 多线程中篇(五)
不止一次的提到过,synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差,但是随着版本的升级,synchro ...
- 多线程之Synchronized锁的基本介绍
基本介绍 synchronized是Java实现同步的一种机制,它属于Java中关键字,是一种jvm级别的锁.synchronized锁的创建和释放是此关键字控制的代码的开始和结束位置,锁是有jvm控 ...
- Java多线程6:Synchronized锁代码块(this和任意对象)
一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...
- Java多线程5:Synchronized锁机制
一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...
随机推荐
- PAT 跟奥巴马一起编程
美国总统奥巴马不仅呼吁所有人都学习编程,甚至以身作则编写代码,成为美国历史上首位编写计算机代码的总统.2014 年底,为庆祝“计算机科学教育周”正式启动,奥巴马编写了很简单的计算机代码:在屏幕上画一个 ...
- pi-star镜像 下载地址
Pi-Star_NanoPi_Air_V3.4.17_09-Jan-2019.zip nanopi air点这里 Pi-Star_NanoPi_V3.4.17_09-Jan-2019.zip nan ...
- el-upload配合vue-cropper实现上传图片前裁剪
需求背景 上传一个封面图,在上传之前需要对图片进行裁剪,上传裁剪之后的图片,类似微信的上传头像. 技术方案 上传肯定是用element的 el-upload 组件实现上传,非常方便,各种钩子函数. 裁 ...
- Centos6.5--svn搭建
0x01 配置好镜像源安装svn yum install subversion -y 0x02 安装完成之后在/opt下面新建一个svn的目录,当然也可以在其他的地方建立svn目录,这个看个人爱好. ...
- tensorflow2.0学习笔记第二章第二节
2.2复杂度和学习率 指数衰减学习率可以先用较大的学习率,快速得到较优解,然后逐步减少学习率,使得模型在训练后期稳定指数衰减学习率 = 初始学习率 * 学习率衰减率^(当前轮数/多少轮衰减一次) 空间 ...
- opencv 移植
1.ubunut系统搭建opencv+python开发环境 1.1.ubuntu系统安装pip3工具 sudo apt-get install python3-pip //安装python模块安装工具 ...
- char 型变量中能不能存贮一个中文汉字?为什么?
在c语言中,char类型占一个字节,而汉字占两个字节,所以不能存储. 在java语言中,char类型占两个字节,而java默认采用Unicode码是16位,所以一个Unicode码占两个字节,java ...
- Arduino控制超声波检测与0.96OLED及串口显示
Arduino控制超声波检测与0.96OLED及串口显示代码使用库共享(包括超声波检测与U8glib): 使用元件: 0.96寸 12864 I2C OLED 128x64规格 超声波检测模块 湿度模 ...
- JS遍历对象的几种方法
几天前一个小伙伴问我 Object.getOwnPropertyNames() 是干什么用的 平时还真没有使用到这个方法,一时不知如何回答 从方法名称来分析,应该是返回的是对象自身属性名组成的数组 那 ...
- ca12a_c++顺序容器的操作5_访问容器中的数据元素
ca12a_c++顺序容器的操作5_访问容器中的数据元素访问元素:c.back()..显示最后一个数据c.front() ..显示第一个数据c[n],n就是下标,适合vector与dequec.at( ...