开始


类有一个特性叫封装,如果一个类,所有的field都是private的,而且没有任何的method,那么这个类就像是四面围墙+天罗地网,没有门。看起来就是一个封闭的箱子,外面的进不来,里面的出不去,一般来说,这样的类是没用的。

现在为这个类定义一个public的method,这个method能够修改这个类的field,相当于为这个箱子开了一个门。门有了,然后访问者就有了,当一个时间段,有多个访问者进来,就可能会发生并发问题。
 
并发问题是个什么问题?最经典的例子就是转账,一个访问者从账户A扣取一部分金额,加到账户B上。在A账户扣取之后,B账户转入之前,数据处于不一致的状态,另一个访问者如果在这个时候访问B账户,获取的数据就是有问题的。这就是并发问题,导致这个问题的出现基于2个条件:1.访问者的操作导致数据在一段时间内是不一致的;2.可以有多个访问者同时操作。如果能够破坏其中一个条件,就可以解决并发问题了。我们的关注点是在第2个条件上。
 
回到那个箱子,回到那个门。我们设想为这个门加一把锁,一个访问者进了这个门,就上锁,期间其他访问者不能再进来;等进去的访问者出来,锁打开,允许另一个访问者进去。

1. 给一个代码块上锁

synchronized可以上锁、解锁。但是它本身并不是锁,它使用的锁来自于一个对象:任何对象实例都有一把内部锁,只有一把。synchronized不仅仅可以对整个method上锁,还可以对method内的某个代码块上锁。
比如下面这种用法:

synchronized(obj){
// some code...
}

这个用法就是使用了obj的锁,来锁定一个代码块。

对整个方法上锁,如:

 publicsynchronizedvoid aMethod(){
// some code...
}

这个时候它使用的是当前实例this的锁,相当于下面的模式:

publicvoid aMethod(){
synchronized(this){
// some code...
}
}

2. 两个代码块的互斥

一个代码块,被上了锁,就无法同时接纳多个线程的访问。如果是2个不同的代码块,都被上了锁,它们之间是否会有影响呢?请看下面的代码:

 class SyncData {
public void do1() {
synchronized(this) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
} } public void do2() {
synchronized(this) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do2-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

创建1个SyncData的实例,开启2个线程,一个线程调用实例的do1方法,另一个线程调用实例的do2方法,你会看到他们之间是互斥的——即使2个线程访问的是实例的不同的方法,依然不能同时访问。因为决定是否可以同时访问的不再是门,而是锁。只要使用的是相同的对象锁,就会互斥访问

上文中关于门的比喻已经不合适了,因为在代码中你可以发现两个门(do1、do2)使用了同一把锁(this),而这和我们的常识经验是相违背的,下文也不会再出现“门”。

3. 锁的识别

可以使用任何对象的锁,比如你可以专门创建一个对象,只提供锁的功能:

 class SyncData {
private Object lock = new byte[0]; public void do1() {
synchronized(lock) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

思考下面的代码是否能起到互斥访问的作用:

 class SyncData {
public void do1() {
Object lock = new byte[0];
synchronized(lock) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

这个是不能起到互斥作用的,因为每一次调用,局部变量lock都是不同的实例。也就是说,synchronized使用的锁总是变化的。所以我们再补充一点:只有使用相同的对象锁,才能互斥访问。所以识别所使用的锁,是很重要的。

 
下面再看一段代码:

 class SyncData {
public void do1() {
synchronized(this) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
} }
}

创建2个实例,分别交给2个线程中的1个去访问,能互斥吗?

不可以,因为每一个实例使用的都是自身的锁,相互之间是不同的锁,所以不能互斥。如果把代码改成这样呢:

class SyncData {
public void do1() {
synchronized(this.getClass()) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
} }
}

可以互斥,不管一个类有多少个实例,它们调用getClass()返回的结果都是同一个实例。

讨论这个问题,是因为可以在static的method上使用synchronized,而其本质,就是使用了上面那种实例的锁,所以不同的synchronized static方法之间,也是互斥的。

总结


总结一下我们的结论:
  1. 任何对象实例都有一把内部锁,只有一把。
  2. 相同的对象锁是互斥访问的充要条件。
这2个结论已经够了,重要的是识别使用的对象的锁是不是相同的。
 
多线程设计,考虑同步问题,我有几点想法:
  1. 一个类的实例,可能被多个线程并发访问,才考虑同步控制。
  2. 在1的前提下,只有会导致数据状态出现一段时间的不一致,相关的代码片段才需要同步控制。
  3. 在2的前提下,只有两块代码会相互干扰时,才必须使用同一把对象锁,来实现互斥;如果相互之间没有影响,建议使用不同的对象锁,以保持并发性能。
当然,在判断“数据状态是否会不一致”、“两块代码是否有干扰”的时候,是比较困难的,所以再补充2点:
  1. 在不能确认数据状态是否会不一致的情况下,按照会不一致的情况考虑
  2. 在不能确认两块代码是否有干扰的情况下,按照会有干扰的情况考虑
我们的讨论到此结束。
 

参考


  1. Java中Synchronized的用法
    介绍了使用synchronized的几种方式,以及相互的区别,写的很好,建议也看一下,相互印证。

Java synchronized的原理解析的更多相关文章

  1. java线程池原理解析

    五一假期大雄看了一本<java并发编程艺术>,了解了线程池的基本工作流程,竟然发现线程池工作原理和互联网公司运作模式十分相似. 线程池处理流程 原理解析 互联网公司与线程池的关系 这里用一 ...

  2. 【Java并发编程】24、Synchronized实现原理解析

    一.概述 我们知道在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它. 不过,随着后续Java版本更新对 ...

  3. Java Synchronized的原理

    我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的: public class SynchronizedDemo{ public void method(){ syn ...

  4. Java synchronized实现原理总结和偏量锁、轻量锁、重量锁、自旋锁

    synchronized实现同步的基础:Java中的每一个对象都可以作为锁.具体表现为以下3种形式. 对于普通同步方法,锁是当前实例对象(this). 对于静态同步方法,锁是当前类的Class对象. ...

  5. java synchronized 的原理。

    synchronized的作用大概分为三种: 1.确保多线程互斥的访问多线程代码.2.保证变量的可见性.3.防止指令重排序. 那么synchronized 是如何实现这些功能的. public cla ...

  6. java synchronized 关键字原理

    Synchronized 关键字是解决并发问题常用解决方案,有以下三种使用方式: 同步普通方法,锁的是当前对象.同步静态方法,锁的是当前 Class 对象.同步块,锁的是 {} 中的对象. 实现原理: ...

  7. Java反序列化漏洞原理解析(案例未完善后续补充)

    序列化与反序列化 序列化用途:方便于对象在网络中的传输和存储 java的反序列化 序列化就是将对象转换为流,利于储存和传输的格式 反序列化与序列化相反,将流转换为对象 例如:json序列化.XML序列 ...

  8. Java并发之synchronized关键字深度解析(二)

    前言 本文继续[Java并发之synchronized关键字深度解析(一)]一文而来,着重介绍synchronized几种锁的特性. 一.对象头结构及锁状态标识 synchronized关键字是如何实 ...

  9. Java并发包JUC核心原理解析

    CS-LogN思维导图:记录CS基础 面试题 开源地址:https://github.com/FISHers6/CS-LogN JUC 分类 线程管理 线程池相关类 Executor.Executor ...

随机推荐

  1. 课程设计——利用信号量实现读-写者问题(JAVA)

    package cn.Douzi.ReadWriter; import java.util.Scanner; public class ReadWrite { static public int co ...

  2. 【BZOJ】1834 [ZJOI2010]network 网络扩容

    [算法]网络流-最大流+最小费用最大流(费用流) [题解] 第一问跑最大流. 第二问: 原始边相当于费用为0的边,再原图(跑过最大流的图)基础上添加带费用的边,容量为k(相当于inf). 第一问最大流 ...

  3. 【HDU】6110 路径交(2017百度之星) 线段树+RMQ-LCA+树链的交

    [题目]2017"百度之星"程序设计大赛 - 初赛(A) [题意]给定n个点的带边权树,m条编号1~m的路径,Q次询问编号区间[L,R]所有链的交集的长度.n<=500000 ...

  4. 【BZOJ】3771: Triple FTT+生成函数

    [题意]给定n个物品,价值为$a_i$,物品价格互不相同,求选一个或两个或三个的价值为x的方案数,输出所有存在的x和对应方案数.$ai<=40000$. [算法]生成函数+FFT [题解]要求价 ...

  5. 基本控件文档-UITextField属性---iOS-Apple苹果官方文档翻译

    本系列所有开发文档翻译链接地址:iOS7开发-Apple苹果iPhone开发Xcode官方文档翻译PDF下载地址 //转载请注明出处--本文永久链接:http://www.cnblogs.com/Ch ...

  6. c语言学习笔记.关键字.存储类型关键字等

    关键字const 1.修饰变量. 修饰的对象为常量,只读. 2.修饰指针. const 也可以和指针变量一起使用,这样可以限制指针变量本身,也可以限制指针指向的数据. const 离变量名近就是用来修 ...

  7. JS设计模式——3.封装与信息隐藏

    封装.信息隐藏与接口的关系 信息隐藏是目的,封装是手段. 接口提供了一份记载着可供公共访问的方法的契约.它定义了两个对象间可以具有的关系.只要接口不变,这个关系的双方都是可以替换的. 一个理想的软件系 ...

  8. Warning: Permanently added the RSA host key for IP address '192.30.253.113' to the list of known hosts. Permission denied (publickey). fatal: Could not read from remote repository. Please make sure y

    这个应该是很多github新手经常出错的问题,这个就是没有在你github上添加一个公钥. 下面就直接说步骤: 1 可以用 ssh -T git@github.com去测试一下 图上可以明显看出缺少了 ...

  9. 【Eclipse】Elipse自定义library库并导入项目

    1.定义像JRE System Library之类的库 (1)点击UserLibrary (2)如果没有就点击new新建一个user library,否则进行4  (3)向user library添加 ...

  10. php中的转义函数

    <?php parse_url 解析URL, 返回各组成部分 urlencode/urldecode url编码/解码 htmlentities 将字符串转化为html实体 htmlentiti ...