zoukankan      html  css  js  c++  java
  • Java中线程同步的理解

    Java中线程同步的理解

    我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。 
    线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。 
    当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。 
    同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。 
    线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
    
    因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。 



    
    




    关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。 

    
    
    同步锁:
    
    前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。 
    线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。 
    线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里? 
    当然是加在共享资源上了。反应快的读者一定会抢先回答。 
    没错,如果可能,我们当然尽量把同步锁加在共享资源上。
    一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。 但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。 读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?
    这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。 于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。
    这一点一定要记住,同步锁不是加在共享资源上,而是加在访问同一份共享资源的不同代码段上的。
    同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。 
    现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型:
    首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。 
    其次,我们要解决的问题是,我们应该在代码段上加什么样的锁?
    这个问题是重点中的重点。这是我们尤其要注意的问题:访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。 这就是说,同步锁本身也一定是多个线程之间的共享对象。
    
    
    这里我们以当前最流行的Java语言为例。Java语言里面用synchronized关键字给代码段加锁。整个语法形式表现为 
    synchronized(同步锁) { 
    // 访问共享资源,需要同步的代码段 
    }
    
    这里尤其要注意的就是,同步锁本身一定要是共享的对象。
    
    … f1() {
    
    Object lock1 = new Object(); // 产生一个同步锁
    
    synchronized(lock1){ 
    // 代码段 A 
    // 访问共享资源 resource1 
    // 需要同步 
    } 
    }
    
    上面这段代码没有任何意义。因为那个同步锁是在函数体内部产生的。每个线程调用这段代码的时候,都会产生一个新的同步锁。那么多个线程之间,使用的是不同的同步锁。根本达不到同步的目的。 
    同步代码一定要写成如下的形式,才有意义。
    
    public static final Object lock1 = new Object();
    
    … f1() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 A 
    // 访问共享资源 resource1 
    // 需要同步 
    }
    
    你不一定要把同步锁声明为static或者public,但是你一定要保证相关的同步代码之间,一定要使用同一个同步锁。 

    讲到这里,你一定会好奇,这个同步锁到底是个什么东西。为什么随便声明一个Object对象,就可以作为同步锁?
    因为确实任何一个Object Reference都可以作为同步锁。
    我们可以把Object Reference理解为对象在内存分配系统中的内存地址。
    因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。
    这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。
    ...

    信号量:
    
    同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。 
    有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。 
    信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。 
    (1)等待某个信号量的通知 
    public static final Object signal = new Object();
    
    … f1() { 
    synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁
    
    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码 
    signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列
    
    // 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了
    
    // 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面 
    // 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。 
    // 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。 
    … 
    } 
    }

    (2)发出某个信号量的通知 
    … f2() { 
    synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。
    
    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码 
    signal.notify(); // 这里,我们通知signal的待召队列中的某个线程。
    
    // 如果某个线程等到了这个通知,那个线程就会转到就绪队列中 
    // 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行 
    // 嘿嘿,虽然本线程好心通知其他线程, 
    // 但是,本线程可没有那么高风亮节,放弃到手的同步锁 
    // 本线程继续执行下面的代码 
    … 
    } 
    }

    ...
    完整版:http://blog.csdn.net/u012179540/article/details/40685207



  • 相关阅读:
    C# Winform下一个热插拔的MIS/MRP/ERP框架(通用控件)
    Xamarin如何使用终端设备的NFC功能传递卡号等信息给Web页面(Android)
    C# Winform下一个热插拔的MIS/MRP/ERP框架(简介)
    C# Winform中自定义筛选及自带统计行的Datagridview控件
    C#下Excel的普通处理和报表设计
    JVM 垃圾回收算法
    谈数据库的性能优化
    RedisService
    bean 与 map 互转.
    RedisService
  • 原文地址:https://www.cnblogs.com/zedosu/p/6636099.html
Copyright © 2011-2022 走看看