synchronized关键字

提到synchronized关键字就会和同步、线程安全挂钩。什么线程?说到线程先提一下进程,进程是由系统进行资源分配合调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

先说下什么是同步和和线程安全。

举个栗子:
同步:公司有4个厕所,恰好坏了3个就只有一个是好的,老王速度快,抢到了这个坑,其他的同事去的晚就只能在外面排队。
异步:在老王蹲坑的时候,扫地阿姨正在里面打扫卫生,各干各的。这就是异步。上厕所有人看着,这就把”隐私”暴露在外面了, 你说尴尬不?所以也就也导致了线程的安全问题。

所以线程的安全问题也就是因为异步操作引起的,所以就得解决这个问题,把异步操作变成同步操作。也就是同步锁。

1. synchronized 同步锁

synchronized关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。(也就是上面说的没坏的那个厕所同时只能有一个人可以用 /滑稽)

先做个小测试

public class Test {

    public static void main(String[] str) {

        Test test = new Test();

        Thread t1 = new Thread() {
            public void run() {
                test.printNum();
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                test.printNum();
            }
        };

        t1.start();
        t2.start();
    }
    public void printNum() {
        for (int i = 1; i <= 10; i++) {
            System.out.print(i + " ");
        }
    }
}
//打印结果
1 2 1 2 3 4 5 6 3 4 5 6 7 7 8 8 9 10 9 10 

开启两个线程同时执行打印,可以看出打印出两组从1到10的是混乱的。两个线程都玩各的,产生了不安全性。那怎么才能使其打印的正常呢。下面把代码改动一下:

public class Test {

    //省略..
   
    //第一种方式
    public void printNum() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.print(i + " ");
            }
        }
    }

    //第二种方式 (打印结果和第一种方式一致)
    public synchronized void printNum() {
        for (int i = 1; i <= 10; i++) {
            System.out.print(i + " ");
        }
    }
}

//打印结果
1 2 3 4 5 6 7 8 9 10   1 2 3 4 5 6 7 8 9 10 

上面可以看到在printNum的代码块中添加了synchronized (this),保证了线程的安全,当t1线程执行完后t2线程才会执行,打印顺序恢复正常。第一种方式被称为同步代码块, 该方式是在方法内部使用大括号包裹使得一个代码块得到同步,当这个代码块正在被一个线程执行下,下一个线程等待。第二种方式被称为同步方法, 是在方法中添加synchronized关键字。还有一种写法和第一种方式类似synchronized (非this对象)。

说完对象锁再继续往下看两个示例:

//省略..

// 第一种方式:给方法添加static和synchronized关键字
public static synchronized void printNum() {
    for (int i = 1; i <= 10; i++) {
       System.out.print(i + " ");
    }
}

// 第二种方式:把this改为Test.class
public void printNum() {
    synchronized (Test.class) {
        for (int i = 1; i <= 10; i++) {
            System.out.print(i + " ");
        }
    }
}

//打印结果
1 2 3 4 5 6 7 8 9 10   1 2 3 4 5 6 7 8 9 10 

这个示例中,两种方式和第一种打印的结果一样,为什么要拆开测试呢。 这就说到了同步锁。

上面两个示例包括最后说的非this对象在同步锁中属于对象锁
下面两个示例我们叫做类锁

2. 对象锁 和 类锁

引用一篇博文里的相关资料. 原创连接没找到。

1.一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,
2.在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁);
3.如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。
4.取到锁后,他就开始执行同步代码(被synchronized修饰的代码);
5.线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。
6.这样就保证了同步代码在同一时刻只有一个线程在执行。

先说下锁的概念,以便能更方面的理解。
在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调,而这两类数据是被所有线程共享的

① 保存在堆中的实例变量
② 保存在方法区中的类变量

image.png

Start---
方法区 (Method Area)
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

堆(Heap)
堆是JVM中一块可自由分配给对象的区域。当我们谈论垃圾回收(garbage collection)时,我们主要回收堆(heap)的空间。Java的普通对象存活在堆中。与栈不同,堆的空间不会随着方法调用结束而清空。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在于堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存空间将最终消耗殆尽。

栈(Stacks)
在Java中,JVM中的栈记录了线程的方法调用。每个线程拥有一个栈。在某个线程的运行过程中,如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。
Java的参数和局部变量只能是基本类型的变量(比如int),或者对象的引用(reference)。因此,在栈中,只保存有基本类型的变量和对象引用。引用所指向的对象保存在堆中。(引用可能为Null值,即不指向任何对象)。当被调用方法运行结束时,该方法对应的帧将被删除,参数和局部变量所占据的空间也随之释放。线程回到原方法,继续执行。当所有的栈都清空时,程序也随之运行结束。
End---

在Java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。 也可以说任意一个对象都拥有自己的监视器。
对于对象来说,相关联的监视器保护 对象的实例变量
对于类来说,监视器保护 类的类变量
当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法。为了实现监视器的排他性监视能力,Java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。

类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。Synchronized先天具有重入性, 即在同一锁程中,线程不需要再次获取同一把锁(一个线程可以多次对同一个对象上锁)。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一

小小的总结一下:Synchronized代码执行前会进行抢锁的动作,抢到锁的线程执行代码块,没有抢到锁的线程等待(阻塞)其线程执行完后, 再进行抢锁操作

  1. 并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 比较关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞,阻塞的意思就是不仅这个方法不能执行,这之后的方法也不能执行。
  4. 这三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码
  5. 以上规则对其它对象锁同样适用

最后看一下对象锁和类锁的区别:

区别.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341