并发编程(三):深入分析synchronized

一、synchronized简介

Java提供了强制性的锁机制:synchronized,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。

包括两种用法:synchronized方法和 synchronized 块。

二、synchronized的使用

2.1 synchronized方法

public static synchronized void inc() {  
    ....  
}  

synchronized方法控制多个线程对该方法内的成员的并发访问,我们将inc方法申明为synchronized,所以同一时间只有一个线程可以访问inc方法。当第一个玩家进来后,会对inc方法加一把锁不让别的玩家访问,玩家二如果也想访问inc方法只能排队等候直到玩家一访问结束释放锁后,效果如下图。 虽然它现在是线程安全的了,但是这种方法过于极端,它的性能非常差。因为有时候我们需要共享的只是方法内的部分数据,其它数据是可以自由访问的,那么这个时候我们应该在项目中使用synchronized块。

image

2.2 synchronized代码块

synchronized代码块控制线程访问的数据在synchronized(obj)或synchronized(this){}里面,同一时间也只能有一个线程可以访问,别的请求线程将被阻塞在 synchronized(obj){}外边,这样可以不影响别的线程访问不需要共享的数据。比如:inc() 玩家等级小于30 ,不满足条件程序直接return不用让线程也阻塞在synchronized代码块外边。

public void inc(Object obj) {  
    if(obj == null)  
    {  
       return;  
    }  
    synchronized (obj) {  
        count++;  
    }  
} 
public void inc(int lvl) {  
       if(lvl < 30)//玩家等级小于30 返回  
    {  
        return;  
    }  
    synchronized (this) {  
        count++;  
    }  
}  

2.3 对synchronized(this)的理解

  • 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

  • 然而,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

  • 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

  • 第三个例子同样适用其它同步代码块,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

  • 以上规则对其它对象锁同样适用。

    以上是摘自百度百科对synchronized的理解,详细使用参考这篇文章:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html

三、内部锁的重进入

当一个线程请求其它线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。重进入意味着锁的请求是基于“每线程”,而不是基于“每调用”的。重进入的实现是通过每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁时未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器达到0时,锁被释放。【摘自JAVA并发编程实战】

3.1代码示例

package com.game.lll.syn;  
  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  
  
  
public class UnsafeCount {  
    public static int count = 0;  
    static LoggingWidget loggingWidget = new LoggingWidget();  
    public static void inc() {  
        loggingWidget.doSomething();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
  
        ExecutorService service=Executors.newFixedThreadPool(Integer.MAX_VALUE);  
  
        for (int i = 0; i < 10; i++) {  
            service.execute(new Runnable() {  
                @Override  
                public void run() {  
                    UnsafeCount.inc();  
                }  
            });  
        }  
  
        service.shutdown();  
        //避免出现main主线程先跑完而子线程还没结束,在这里给予一个关闭时间  
        service.awaitTermination(3000,TimeUnit.SECONDS);  
        System.out.println("运行结果:UnsafeCount.count=" + UnsafeCount.count);  
    }  
}  
package com.game.lll.syn;  
  
public class LoggingWidget extends Widget{  
    public synchronized void doSomething()  
    {  
        System.out.println("LoggingWidget"+UnsafeCount.count++);  
        super.doSomething();  
    }  
}  
package com.game.lll.syn;  
public class Widget {  
   public synchronized void doSomething()  
   {  
       System.out.println("Widget"+UnsafeCount.count++);  
   }  
}  

控制台输出:

LoggingWidget0
Widget1
LoggingWidget2
Widget3
LoggingWidget4
Widget5
LoggingWidget6
Widget7
LoggingWidget8
Widget9
LoggingWidget10
Widget11
LoggingWidget12
Widget13
LoggingWidget14
Widget15
LoggingWidget16
Widget17
LoggingWidget18
Widget19
运行结果:UnsafeCount.count=20

3.2代码分析

重进入方便了锁行为的封装,因此简化了面向对象并发代码的开发。上面代码子类覆写了父类的synchronized类型的方法,并调用父类中的方法。如果没有可重入的锁,这段代码将会产生死锁。因为Weight和loggingWeight中的soSomething方法都是synchronized类型的,都会在处理前试图获得weight的锁。倘若内部锁不是可重入的,super.doSomething的调用者就永远无法得到weight的锁,因为锁已经被占有,导致线程会永久的延迟,等待着一个永远无法获得的锁。

四、锁的三大特性

4.1 原子性

原子性是指在同一时刻只有一个线程对它进行读写操作,避免多个线程在更改共享数据时出现数据的不准确。

在Java中提供了原子操作的关键字synchronized。我在上一篇文章中写过原子性Atomic(一)

4.2 可见性

可见性是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个值的修改。在Java中,除了synchronized,volatile和final也是可见性的。synchronized可以确保线程能预见另一个线程对某一个值或状态的更改,就像下图一样。当线程一执行一个同步块时,线程二也随后进入了同一个锁的同步块中,这时可以保证,在释放锁M之前线程一变量的值count对线程二是可见的。换句话说就是有一个玻璃透明的房间,虽然线程一进入后将房间锁住了,但是线程二在门口还是可以透过玻璃看见房间内的一切事物。

image
package com.game.lll.syn;  
  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  
  
  
public class SafeCount implements Runnable{  
    public  volatile static int count = 0;  
  
    public synchronized static void inc()  
    {  
        count++;  
    }  
      
    public static void main(String[] args) throws InterruptedException {  
  
         SafeCount t1 = new SafeCount();    
         Thread ta = new Thread(t1, "线程一");    
         Thread tb = new Thread(t1, "线程二");    
         ta.start();    
         tb.start();    
        System.out.println("UnsafeCount.count=" + SafeCount.count);  
    }  
  
    @Override  
    public void run() {  
        System.out.println(Thread.currentThread().getName()+"---执行前--count:"+count);    
        inc();  
        System.out.println(Thread.currentThread().getName()+"---执行后--count:"+count);    
    }  
      
}  

控制台输出:

UnsafeCount.count=0
线程二---执行前--count:0
线程一---执行前--count:0
线程二---执行后--count:1
线程一---执行后--count:2

锁不仅仅是关于同步与互斥,也是关于内存可见的。为了保证所有线程看到共享的、可变变量的最新值,读写和写入线程必须使用公共的锁进行同步。摘自--《Java并发编程实战》

4.3 有序性

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

1.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。

2.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。。

3.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。

4.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

5.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。

6.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。

7.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。

8.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

详细请参考这篇文章:深入理解Java虚拟机笔记---原子性、可见性、有序性

作者:小毛驴,一个Java游戏服务器开发者 原文地址:https://liulongling.github.io/

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

推荐阅读更多精彩内容