精华内容
下载资源
问答
  • 每一个运行程序可能包括个独立运行的线程(Thread)。 线程(Thread)是一份独立运行程序,有自己专用运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。 当线程同时读写同一份...
    这是javaeye上非常经典的关于线程的帖子,写的非常通俗易懂的,适合任何读计算机的同学. 
    线程同步
    
    我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。 
    线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。 
    当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。 
    同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。 
    线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
    
    因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。 
    关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。 
    关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。 
    关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。
    
    为了加深理解,下面举几个例子。 
    有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤: 
    (1)到市场上去,寻找并购买有潜力的样品。 
    (2)回到公司,写报告。 
    这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。 
    这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。
    
    下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。 
    这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。
    
    下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。 
    如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。 
    如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。 
    上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。
    
    同步锁
    
    前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。 
    线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。 
    生活中,我们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每个储物箱都有一把锁,一把钥匙。人们可以使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,然后把钥匙拿走。这样,该储物箱就被锁住了,其他人不能再访问这个储物箱。(当然,真实的储物箱钥匙是可以被人拿走复制的,所以不要把贵重物品放在超市的储物箱里面。于是很多超市都采用了电子密码锁。) 
    线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里? 
    当然是加在共享资源上了。反应快的读者一定会抢先回答。 
    没错,如果可能,我们当然尽量把同步锁加在共享资源上。一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。 
    但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。 
    读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。 
    于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点一定要记住,同步锁是加在代码段上的。 
    同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。 
    现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型。 
    首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。 
    其次,我们要解决的问题是,我们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是我们尤其要注意的问题:访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。 
    这就是说,同步锁本身也一定是多个线程之间的共享对象。
    
    Java语言的synchronized关键字
    
    为了加深理解,举几个代码段同步的例子。 
    不同语言的同步锁模型都是一样的。只是表达方式有些不同。这里我们以当前最流行的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对象,就可以作为同步锁? 
    在Java里面,同步锁的概念就是这样的。任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。 
    一些求知欲强的读者可能想要继续深入了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你可以在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter, … monitor exit之类的指令对。Monitor就是实际上的同步锁。每一个Object Reference在概念上都对应一个monitor。 
    这些实现细节问题,并不是理解同步锁模型的关键。我们继续看几个例子,加深对同步锁模型的理解。
    
    public static final Object lock1 = new Object();
    
    … f1() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 A 
    // 访问共享资源 resource1 
    // 需要同步 
    } 
    }
    
    … f2() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 B 
    // 访问共享资源 resource1 
    // 需要同步 
    } 
    }
    
    上述的代码中,代码段A和代码段B就是同步的。因为它们使用的是同一个同步锁lock1。 
    如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。 
    这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列。 
    每一个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1 - ready queue。每个队列里面都可能有多个暂停运行的线程。 
    注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。 
    成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。 
    因此,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围。同步的代码段范围越小越好。我们用一个名词“同步粒度”来表示同步代码段的范围。 
    同步粒度 
    在Java语言里面,我们可以直接把synchronized关键字直接加在函数的定义上。 
    比如。 
    … synchronized … f1() { 
    // f1 代码段 
    }
    
    这段代码就等价于 
    … f1() { 
    synchronized(this){ // 同步锁就是对象本身 
    // f1 代码段 
    } 
    }
    
    同样的原则适用于静态(static)函数 
    比如。 
    … static synchronized … f1() { 
    // f1 代码段 
    }
    
    这段代码就等价于 
    …static … f1() { 
    synchronized(Class.forName(…)){ // 同步锁是类定义本身 
    // f1 代码段 
    } 
    }
    
    但是,我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。 
    我们不仅要在缩小同步代码段的长度上下功夫,我们同时还要注意细分同步锁。 
    比如,下面的代码
    
    public static final Object lock1 = new Object();
    
    … f1() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 A 
    // 访问共享资源 resource1 
    // 需要同步 
    } 
    }
    
    … f2() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 B 
    // 访问共享资源 resource1 
    // 需要同步 
    } 
    }
    
    … f3() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 C 
    // 访问共享资源 resource2 
    // 需要同步 
    } 
    }
    
    … f4() {
    
    synchronized(lock1){ // lock1 是公用同步锁 
    // 代码段 D 
    // 访问共享资源 resource2 
    // 需要同步 
    } 
    }
    
    上述的4段同步代码,使用同一个同步锁lock1。所有调用4段代码中任何一段代码的线程,都需要竞争同一个同步锁lock1。 
    我们仔细分析一下,发现这是没有必要的。 
    因为f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。我们可以增加一个同步锁lock2。f3()和f4()的代码可以修改为: 
    public static final Object lock2 = new Object();
    
    … f3() {
    
    synchronized(lock2){ // lock2 是公用同步锁 
    // 代码段 C 
    // 访问共享资源 resource2 
    // 需要同步 
    } 
    }
    
    … f4() {
    
    synchronized(lock2){ // lock2 是公用同步锁 
    // 代码段 D 
    // 访问共享资源 resource2 
    // 需要同步 
    } 
    }
    
    这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就可以大大较少同步锁竞争的概率,从而减少系统的开销。
    
    信号量
    
    同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。 
    有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。 
    信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。 
    很多语言里面,同步锁都由专门的对象表示,对象名通常叫Monitor。 
    同样,在很多语言中,信号量通常也有专门的对象名来表示,比如,Mutex,Semphore。 
    信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至可以跨进程进行同步。另外一些信号量甚至还有计数功能,能够控制同时运行的线程数。 
    我们没有必要考虑那么复杂的模型。所有那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。 
    我们还是以Java语言为例。Java语言里面的同步锁和信号量概念都非常模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。 
    这种模糊虽然导致概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。我们不必执着于名词之争,可以专注于理解实际的运行原理。 
    在Java语言里面,任何一个Object Reference都可以作为同步锁。同样的道理,任何一个Object Reference也可以作为信号量。 
    Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。 
    具体调用方法为 
    (1)等待某个信号量的通知 
    public static final Object signal = new Object();
    
    … f1() { 
    synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁
    
    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码 
    signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列
    
    // 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了
    
    // 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面 
    // 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。 
    // 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。 
    … 
    } 
    }
    
    需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。
    
    (2)发出某个信号量的通知 
    … f2() { 
    synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。
    
    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码 
    signal.notify(); // 这里,我们通知signal的待召队列中的某个线程。
    
    // 如果某个线程等到了这个通知,那个线程就会转到就绪队列中 
    // 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行 
    // 嘿嘿,虽然本线程好心通知其他线程, 
    // 但是,本线程可没有那么高风亮节,放弃到手的同步锁 
    // 本线程继续执行下面的代码 
    … 
    } 
    }
    
    需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。
    
    以上就是Object的wait()和notify()的基本用法。 
    实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。 
    另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。 
    这些细节问题,并不对大局产生影响。
    
    绿色线程
    
    绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。 
    操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的 
    绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。 
    当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。 
    难道说,绿色线程要比操作系统线程要慢吗?当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。线程调度器并不是很成熟。 
    目前,线程的流行实现模型就是绿色线程。比如,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。 
    另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。 
    ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和进程混淆起来。这里要注意区分一下。 
    ErLang Process之间根本就不需要同步。因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。因此根本就不需要同步。 
    final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情。
    展开全文
  • java线程加锁

    2016-08-16 10:45:33
    13.8 线程加锁的设置,所谓线程就是为了防止个线程为同一个动作进行操作。 synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种: 1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用...

    13.8 线程加锁的设置,所谓线程就是为了防止多个线程为同一个动作进行操作。

    synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
    2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
    4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    1. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
      每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
      C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    13.9 对方法进行加锁:

    代码如下:

    package com.eduask.thread.base;


    import com.sun.swing.internal.plaf.synth.resources.synth;


    //测试线程加锁的现象;

    public class TestThreadSync {

    public static void main(String[] args) throws InterruptedException {

    MySnc1 mySnc1=new MySnc1();

    Thread thread1=new Thread(mySnc1);

    thread1.start();

    thread1.sleep(1000);

    mySnc1.m1();

    }

    }

    class MySnc1 implements Runnable{

    Integer money=100;

    @Override

    public synchronized void run() {

    money=10;

    System.out.println(money);

    }

    public void m1() {

    System.out.println(money+"m1");

    }

    }


    13.10 对对象进行加载代码如下;

    package com.eduask.thread.base;


    //对对象进行加锁;

    public class ObjectSync {

    public static void main(String[] args) {

    A a = new A();

    Thread thread = new Thread(a);

    thread.start();

    }

    }


    class A implements Runnable {

    public void run() {

    synchronized (this) {

    System.out.println("对对象进行加锁");

    }

    }


    }

    展开全文
  • 每一个运行程序可能包括个独立运行的线程(Thread)。 线程(Thread)是一份独立运行程序,有自己专用运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。 当线程同时读写同一份...
    这是javaeye上非常经典的关于线程的帖子,写的非常通俗易懂的,适合任何读计算机的同学. 
    线程同步

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

    因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
    关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
    关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
    关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

    为了加深理解,下面举几个例子。
    有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:
    (1)到市场上去,寻找并购买有潜力的样品。
    (2)回到公司,写报告。
    这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。
    这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。

    下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。
    这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。

    下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。
    如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。
    如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。
    上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。

    同步锁

    前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。
    线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
    生活中,我们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每个储物箱都有一把锁,一把钥匙。人们可以使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,然后把钥匙拿走。这样,该储物箱就被锁住了,其他人不能再访问这个储物箱。(当然,真实的储物箱钥匙是可以被人拿走复制的,所以不要把贵重物品放在超市的储物箱里面。于是很多超市都采用了电子密码锁。)
    线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里?
    当然是加在共享资源上了。反应快的读者一定会抢先回答。
    没错,如果可能,我们当然尽量把同步锁加在共享资源上。一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。
    但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。
    读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。
    于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点一定要记住,同步锁是加在代码段上的。
    同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。
    现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型。
    首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。
    其次,我们要解决的问题是,我们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是我们尤其要注意的问题:访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。
    这就是说,同步锁本身也一定是多个线程之间的共享对象。

    Java语言的synchronized关键字

    为了加深理解,举几个代码段同步的例子。
    不同语言的同步锁模型都是一样的。只是表达方式有些不同。这里我们以当前最流行的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对象,就可以作为同步锁?
    在Java里面,同步锁的概念就是这样的。任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。
    一些求知欲强的读者可能想要继续深入了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你可以在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter, … monitor exit之类的指令对。Monitor就是实际上的同步锁。每一个Object Reference在概念上都对应一个monitor。
    这些实现细节问题,并不是理解同步锁模型的关键。我们继续看几个例子,加深对同步锁模型的理解。

    public static final Object lock1 = new Object();

    … f1() {

    synchronized(lock1){ // lock1 是公用同步锁
    // 代码段 A
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    … f2() {

    synchronized(lock1){ // lock1 是公用同步锁
    // 代码段 B
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    上述的代码中,代码段A和代码段B就是同步的。因为它们使用的是同一个同步锁lock1。
    如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。
    这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列。
    每一个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1 - ready queue。每个队列里面都可能有多个暂停运行的线程。
    注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。
    成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。
    因此,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围。同步的代码段范围越小越好。我们用一个名词“同步粒度”来表示同步代码段的范围。
    同步粒度
    在Java语言里面,我们可以直接把synchronized关键字直接加在函数的定义上。
    比如。
    … synchronized … f1() {
    // f1 代码段
    }

    这段代码就等价于
    … f1() {
    synchronized(this){ // 同步锁就是对象本身
    // f1 代码段
    }
    }

    同样的原则适用于静态(static)函数
    比如。
    … static synchronized … f1() {
    // f1 代码段
    }

    这段代码就等价于
    …static … f1() {
    synchronized(Class.forName(…)){ // 同步锁是类定义本身
    // f1 代码段
    }
    }

    但是,我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。
    我们不仅要在缩小同步代码段的长度上下功夫,我们同时还要注意细分同步锁。
    比如,下面的代码

    public static final Object lock1 = new Object();

    … f1() {

    synchronized(lock1){ // lock1 是公用同步锁
    // 代码段 A
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    … f2() {

    synchronized(lock1){ // lock1 是公用同步锁
    // 代码段 B
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    … f3() {

    synchronized(lock1){ // lock1 是公用同步锁
    // 代码段 C
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    … f4() {

    synchronized(lock1){ // lock1 是公用同步锁
    // 代码段 D
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    上述的4段同步代码,使用同一个同步锁lock1。所有调用4段代码中任何一段代码的线程,都需要竞争同一个同步锁lock1。
    我们仔细分析一下,发现这是没有必要的。
    因为f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。我们可以增加一个同步锁lock2。f3()和f4()的代码可以修改为:
    public static final Object lock2 = new Object();

    … f3() {

    synchronized(lock2){ // lock2 是公用同步锁
    // 代码段 C
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    … f4() {

    synchronized(lock2){ // lock2 是公用同步锁
    // 代码段 D
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就可以大大较少同步锁竞争的概率,从而减少系统的开销。

    信号量

    同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。
    有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。
    信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。
    很多语言里面,同步锁都由专门的对象表示,对象名通常叫Monitor。
    同样,在很多语言中,信号量通常也有专门的对象名来表示,比如,Mutex,Semphore。
    信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至可以跨进程进行同步。另外一些信号量甚至还有计数功能,能够控制同时运行的线程数。
    我们没有必要考虑那么复杂的模型。所有那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。
    我们还是以Java语言为例。Java语言里面的同步锁和信号量概念都非常模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。
    这种模糊虽然导致概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。我们不必执着于名词之争,可以专注于理解实际的运行原理。
    在Java语言里面,任何一个Object Reference都可以作为同步锁。同样的道理,任何一个Object Reference也可以作为信号量。
    Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。
    具体调用方法为
    (1)等待某个信号量的通知
    public static final Object signal = new Object();

    … f1() {
    synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁

    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
    signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列

    // 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了

    // 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面
    // 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。
    // 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。

    }
    }

    需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。

    (2)发出某个信号量的通知
    … f2() {
    synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。

    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
    signal.notify(); // 这里,我们通知signal的待召队列中的某个线程。

    // 如果某个线程等到了这个通知,那个线程就会转到就绪队列中
    // 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行
    // 嘿嘿,虽然本线程好心通知其他线程,
    // 但是,本线程可没有那么高风亮节,放弃到手的同步锁
    // 本线程继续执行下面的代码

    }
    }

    需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。

    以上就是Object的wait()和notify()的基本用法。
    实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。
    另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。
    这些细节问题,并不对大局产生影响。

    绿色线程

    绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。
    操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的
    绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。
    当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。
    难道说,绿色线程要比操作系统线程要慢吗?当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。线程调度器并不是很成熟。
    目前,线程的流行实现模型就是绿色线程。比如,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。
    另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。
    ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和进程混淆起来。这里要注意区分一下。
    ErLang Process之间根本就不需要同步。因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。因此根本就不需要同步。
    final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情。
    展开全文
  • 多线程访问共享资源访问 1.尝试写出需求。 2.分析出现问题。 三个线程同时执行run方法,CPU执行资源随机分配。 线程在执行方法过程中,随时能进入受阻塞状态。 所以可以使用:假设线程停止位置,来分析问题(找...

    一.加锁方式

    1.解决需求的步骤:
    需求:100张票,利用多线程进行卖票,方式有:窗口, 黄牛 ,网购。

    • 多线程访问共享资源的访问
    • 1.尝试写出需求。
    • 2.分析出现的问题。
      • 三个线程同时执行run方法,CPU执行资源随机分配。
      • 线程在执行方法过程中,随时能进入受阻塞状态。
      • 所以可以使用:假设线程停止的位置,来分析问题(找极限位置)。
    • 3.想解决方案:
      • 一个线程执行完买票操作,另一个线程才能买票。
      • 这样来保证共享数据的安全。
    • 4.找出解决方法:
      • 方式一:同步代码块(同步锁)
      • 当线程进入同步代码块,会把锁拿走,执行代码块中的代码。
      • 当代码执行完毕后,会把锁还回去。
      • 如果线程遇到同步代码块,发现没有锁,将进入等待(有锁才能进)。
    • 在找问题时,可以使用多线程的睡眠方法,放大问题。
      • 写法:
    synchronized(锁){
    	   	上锁的代码
     	  }
    
    • 锁注意:
      • 所有线程使用的都是同一把锁
      • 锁可以使用任意一个对象(同一个对象就可以)
      • 让线程让出CPU的执行资源的方法(增加让出的几率)[需要放在同步代码块外]
    Thread.yield();
    

    完成卖票需求的代码:(使用同步代码块完成)

    public class Kll {
    	public static void main(String[] args) {
    		// 利用接口实现类 创建三个线程出来
    		Tickets tickets = new Tickets();
    		// 创建三个线程,并写出线程名字
    		Thread t1 = new Thread(tickets, "黄牛");
    		Thread t2 = new Thread(tickets, "窗口");
    		Thread t3 = new Thread(tickets, "网购");
    		// 开启线程
    		t1.start();
    		t2.start();
    		t3.start();
    	}
    
    }
    
    // 利用接口方法来保证 访问的共享资源
    class Tickets implements Runnable {
    	// 声明票的总数
    	private int tickets = 50;
    	// 声明锁对象(保证锁也是线程共享的即唯一性)
    	private final Object obj = new Object();
    
    	@Override
    	public void run() {
    		// 利用循环,保证票都能卖出去
    		while (true) {
    			synchronized (obj) {
    				// 休眠(放大问题)
    				try {
    					Thread.sleep(100);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    
    				// 判断票
    				if (tickets > 0) {
    					// 可以卖
    					System.out.println(Thread.currentThread().getName() + "--" + tickets);
    					// 卖票
    					tickets--;
    				} else {
    					// 卖完了
    					break;
    				}
    			}
    			// 让线程让出CPU的执行资源(增加让出的几率)[需要放在同步代码块外]
    			Thread.yield();
    		}
    	}
    }
    
    • 方式二:同步方法
      • 封装一个方法
      • 使用同步方法
      • 写法:在方法上 使用synchronized关键词修饰方法
      • 同步方法有没有锁?是什么?为什么?
      • 有,原理和同步代码块一样
      • 成员方法,使用的对象锁是this
      • 同步方法能不能是静态的?可以(成员变量也要是静态的)
      • 如果可以,那么静态的同步方法用的锁是this吗?
      • 不是this(因为静态方法不能用this)
      • 静态方法,使用的锁的是 类锁(本类.class)
    public class Kll {
    	public static void main(String[] args) {
    		// 利用接口实现类 创建三个线程出来
    		Ticket tickets = new Ticket();
    		// 创建三个线程
    		Thread t1 = new Thread(tickets, "黄牛");
    		Thread t2 = new Thread(tickets, "窗口");
    		Thread t3 = new Thread(tickets, "网购");
    		// 开启线程
    		t1.start();
    		t2.start();
    		t3.start();
    	}
    
    }
    
    // 利用接口方法来保证 访问的共享资源
    
    class Ticket implements Runnable {
    	// 声明票的总数
    	private static int tickets = 50;
    	// 声明锁对象(保证锁也是线程共享的即唯一性)
    	private final Object obj = new Object();
    
    	@Override
    	public void run() {
    		// 利用循环,保证票都能卖出去
    		while (true) {
    			
    			if (sellTickets()) {
    				break;
    			}
    		
    			// 让线程让出CPU的执行资源(增加让出的几率)[需要放在同步代码块外]
    			Thread.yield();
    		}
    	}
    
    	public static synchronized boolean sellTickets() {
    		if (tickets > 0) {
    			System.out.println(Thread.currentThread().getName() + "--" + tickets);
    			tickets--;
    			return false;
    		}else {
    			return true;
    		}
    	}
    }
    

    方式三:利用锁的接口Lock

    • jdk1.5 锁的接口 Lock
    • lock();加锁的方法
    • unlock();释放锁方法
    • 保证出现异常时也能把锁关闭(释放了)finally
    • 写法:
          lock();
          try{
              加锁的代码
          }finally{
          	   释放锁
          unlock(); 
           }
    

    案例代码:

    public class Kll {
    	public static void main(String[] args) {
    		// 利用接口实现类 创建三个线程出来
    		Tickets1 tickets = new Tickets1();
    		// 创建三个线程
    		Thread t1 = new Thread(tickets, "黄牛");
    		Thread t2 = new Thread(tickets, "窗口");
    		Thread t3 = new Thread(tickets, "网购");
    		// 开启线程
    		t1.start();
    		t2.start();
    		t3.start();
    	}
    
    }
    
    // 利用接口方法来保证 访问的共享资源
    
    class Tickets1 implements Runnable {
    	// 声明票的总数
    	private int tickets = 50;
    	// 声明锁对象(保证锁也是线程共享的即唯一性)
    	private final Object obj = new Object();
    	// 声明lock锁 
    	// 参数 true 可以尽量让线程公平进入锁
    	private final ReentrantLock lock = new ReentrantLock(true);
    
    	@Override
    	public void run() {
    		// 利用循环,保证票都能卖出去
    		while (true) {
    			// 使用lock锁
    			lock.lock();
    			try {
    				// 判断票
    				if (tickets > 0) {
    					// 可以卖
    					System.out.println(Thread.currentThread().getName() + "--" + tickets);
    					// 卖票
    					tickets--;
    				} else {
    					// 卖完了
    					break;
    				}
    			} finally {
    				lock.unlock();
    			}
    		// 让线程让出CPU的执行资源(增加让出的几率)[需要放在同步代码块外]
    		Thread.yield();
    		}
    	}
    }
    

    需求:公司年会,进入公司有两个门(前门和后门)。进门的时候,每位人都能获取一张彩票(7位数),公司有100个员工。利用多线程模拟进门过程,统计每个入口入场的人数 ,每个人拿到的彩票的号码( 要求7位数字 7个数不能重复)。
    打印格式:
    编号为: 1 的员工 从后门 入场! 拿到的双色球彩票号码是: [17, 24, 29, 30, 31, 32, 07]
    编号为: 2 的员工 从后门 入场! 拿到的双色球彩票号码是: [06, 11, 14, 22, 29, 32, 15]
    ……
    从后门 入场的员工总共: 45 位员工
    从前门 入场的员工总共: 55 位员工
    保证总人数100即可
    案例代码:

    import java.util.ArrayList;
    
    public class Kll {
    	public static void main(String[] args) {
    		// 创建表示前后门的线程
    		Person person = new Person();
    		Thread t1 = new Thread(person, "前门");
    		Thread t2 = new Thread(person, "后门");
    		// 开启线程
    		t1.start();
    		t2.start();
    	}
    
    }
    
    
    class Person implements Runnable{
    	// 声明总人数
    	private int sum = 100;
    	// 声明记录前后门人数
    	private int frontNum = 0;
    	private int backNum = 0;
    	
    	// 打印彩票的方法
    	public ArrayList<Integer> lottery() {
    		// 随机7个数[1,100]放入ArrayList
    		ArrayList<Integer> list = new ArrayList<>();
    		while (list.size() < 7) {
    			int random = (int)(Math.random() * 100 + 1);
    			// 判断是否重复
    			if (!list.contains(random)) {
    				list.add(random);
    			}
    		}
    		return list;
    	}
    
    	@Override
    	public void run() {
    		
    		// 循环,保证所有人都进来
    		while (true) {
    			// -----
    			synchronized (this) {
    				// 接收集合
    				ArrayList<Integer> list = lottery();
    				// 获取当前线程的名字
    				String name = Thread.currentThread().getName();
    				// 循环停止条件
    				// 判断人数
    				if (sum <= 0) {
    					break;
    				} else {
    					// 判断从哪儿进来的
    					if (name.equals("前门")) {
    						frontNum++;
    						System.out.println("编号为:" + (101 - sum) + " 的员工 从" + name +  " 入场! " + "拿到的双色球彩票号码是: " + list);
    						sum--;
    					}else if (name.equals("后门")) {
    						backNum++;
    						System.out.println("编号为:" + (101 - sum) + " 的员工 从" + name +  " 入场! " + "拿到的双色球彩票号码是: " + list);
    						sum--;
    					}
    				}
    				if (sum <= 0) {
    					System.out.println("从前门 入场的员工总共: "+ frontNum +" 位员工");
    					System.out.println("从后门 入场的员工总共: "+ backNum +" 位员工");
    				}
    			}
    			// 让出资源
    			Thread.yield();
    		}
    	}
    }
    
    为什么不把最后的if(sum == 0)判断和循环结束的条件放在一起?
    因为两个线程都在synchronized (this)同步代码块外,循环内等着结束。
    1.如果结束条件和最后的if判断放在一起,那么,只有,一个线程会结束,此时,sum刚好等于0,另外一个线程进入执行完if(sum == 0)之前的语句之后,sum=-1,就永远结束不了程序,进入死循环。同理,若把判断条件sum=0改为sum<=0,那么最另外一个线程执行完if(sum<=0)之前的语句,已经使人数多了1个,比需求多了一个人。
    2.若是把最后的判断条件和最开始的结束条件合二为一放在最开始,那么两个线程都会走一遍这个结束语句,就会把最终的(从后门 入场的员工总共: 45 位员工 ; 从前门 入场的员工总共: 55 位员工)结果各打印两遍

    二.死锁

    前提:

    • 1.至少两个线程。
    • 2.锁的嵌套(同步代码块的嵌套)。
      • 线程1和线程2同时访问有嵌套的同步代码块,且有两个锁A和B:
      • 线程1拿到了A 向进入下一个代码块需要B锁。
      • 线程2拿到了B 向进入下一个代码块需要A锁。
      • 这时谁也进不去 线程进入相互等待的状态 导致程序卡主。
        一定几率出现死锁,,出现时的图如下:在这里插入图片描述

    简单死锁出现的代码:

    public class Kll {
    	public static void main(String[] args) {
    		DieLock dieLock = new DieLock();
    		Thread t1 = new Thread(dieLock);
    		Thread t2 = new Thread(dieLock);
    		t1.start();
    		t2.start();
    		
    	}
    }
    // 声明锁
    class LockA {
    	// 私有化构造方法
    	private LockA() {
    	}
    	// 创建锁对象(声明一个常量)
    	public static final LockA A = new LockA();
    }
    class LockB {
    	// 私有化构造方法
    	private LockB() {
    	}
    	// 创建锁对象(声明一个常量)
    	public static final LockB B = new LockB();
    }
    // 线程
    class DieLock implements Runnable{
    	// 利用标记控制先A-->B或先B-->
    	boolean isTrue = false;
    	@Override
    	public void run() {
    		// 利用死循环 增加死锁几率
    		while (true) {
    			// 不断让两个线程先进A锁再进B锁
    			// 下一次从B锁进A锁
    			if (!isTrue) {
    				synchronized (LockA.A) {
    					System.out.println(Thread.currentThread().getName() + " if中A锁");
    					synchronized (LockB.B) {
    						System.out.println(Thread.currentThread().getName() + " if中B锁");
    					}
    				}
    			}else {
    				synchronized (LockB.B) {
    					System.out.println(Thread.currentThread().getName() + " else中B锁");
    					synchronized (LockA.A) {
    						System.out.println(Thread.currentThread().getName() + " else中A锁");
    					}
    				}
    			}
    			// 改变一下标记
    			isTrue = !isTrue;
    		}
    	}
    }
    

    三.线程停止

    如何让线程停止

    • 调用stop()方法 已过时,不推荐
    • interrupt() 不能中断线程
    • 其作用:
      • 1.改变中断状态(就是个布尔值,初值false–>true)
      • 2.当你这个线程中使用了sleep,wait或join方法时,会抛出异常InterruptedException
        • 中断状态将被清除 这时interrupted()的值还是false
        • 中断状态被清除指的是从休眠状态转为运行状态或者受阻塞状态
    • 如何停止线程?
      • 正确方式:使用标记停止线程

    方式一:在线程内没有sleep,wait,或join方法出现时,用interrupt()方法改变线程状态中断。
    代码如下:

    public class Kll {
    	public static void main(String[] args) throws InterruptedException {
    		INter1 iNter = new INter1();
    		Thread t1 = new Thread(iNter);
    		t1.start();
    		// 休眠几秒 给线程运行时间
    		t1.sleep(1000);
    		// 中断线程
    		t1.interrupt();
    		// 利用标记停止线程
    		//iNter.isTrue = true;
    		System.out.println("线程中断");
    		// 让主线程运行一会儿
    		Thread.sleep(1000);
    		System.out.println("主线程结束");
    	}
    }
    
    class INter1 implements Runnable{
    	@Override
    	public void run() {
    		// 打印中断状态
    		System.out.println(Thread.currentThread().isInterrupted());
    		// 为真时循环
    		// 默认的中断状态是false
    		while (!Thread.currentThread().isInterrupted()) {
    			System.out.println(Thread.currentThread().getName() + "run方法");
    		}
    	}
    }
    

    方法二:用标记法

    • 一.线程内有sleep方法时(同时测试interrupt方法对sleep方法的作用)
      代码如下:
    public class Kll {
    		public static void main(String[] args) throws InterruptedException {
    			INter1 iNter = new INter1();
    			Thread t1 = new Thread(iNter);
    			t1.start();
    			// 休眠几秒 给线程运行时间
    			t1.sleep(1000);
    			// 中断线程
    			// 利用标记停止线程
    			iNter.isTrue = true;
    			System.out.println("线程中断");
    			// 让主线程运行一会儿
    			Thread.sleep(1000);
    			System.out.println("主线程结束");
    			
    		}
    	}
    
    	class INter1 implements Runnable{
    		// 声明标记 控制线程的停止
    		public boolean isTrue = false;
    
    		@Override
    		public void run() {
    			// 打印中断状态
    			System.out.println(Thread.currentThread().isInterrupted());
    			while (!isTrue) {
    				// 线程休眠
    				try {
    					// 中断异常 InterruptedException
    					// 中断状态被清除指的是
    					// 从休眠状态-->运行状态(受阻塞状态)
    					Thread.sleep(1000);
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    				System.out.println(Thread.currentThread().getName() + " run方法");
    			}
    		}
    	}
    
    • 二.测试interrupt方法对wait作用
    • 注意:wait方法需要锁对象调用
      代码如下:
    public class Kll {
    	public static void main(String[] args) throws InterruptedException {
    		WaitRunnable runnable = new WaitRunnable();
    		Thread t1 = new Thread(runnable);
    		t1.start();
    		
    		//Thread.sleep(1000);
    		for (int i = 0; i < 10; i++) {
    			if (i == 5) {
    				// interrupt 方法
    				//t1.interrupt();
    				// 让线程停止
    				runnable.isTrue = true;
    			}
    		}
    		Thread.sleep(1000);
    		System.out.println("主线程停止");
    	}
    
    }
    
    class WaitRunnable implements Runnable{
    	// 生声明标记停止线程
    	public boolean isTrue = false;
    
    	@Override
    	public void run() {
    		System.out.println(Thread.interrupted());
    		synchronized (this) {
    			while (!isTrue) {
    	
    				// 使用线程等待
    				try {
    					// wait方法需要锁对象调用
    					// InterruptedException抛出一个异常
    					// 将线程从等待状态转换为-->运行状态(或受阻塞状态)  清除了原有状态
    					this.wait();
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				System.out.println(Thread.currentThread().getName());
    			}
    		}
    		
    	}
    	
    }
    

    注意:wait方法让线程进入中断状态被清除(指的是从休眠状态转为运行状态或者受阻塞状态)。

    • 在主线程内使用sleep方法会线程的这种中断状态被清除的效果更明显。

    主线程立即接收到子线程修改状态的方法:
    从子线程中修改状态,查看主线程中是否能够立即接收到

    • 不能,解决方法是:
      • 当从子线程中修改状态时
      • 主线程不能立即接收到这个状态的改变
      • 使用关键词 volatile 来标识你改变的状态的变量
      • 效果:可以让主线程立即接收到改变的值
    public class Kll {
    	public static void main(String[] args) {
    		ChangeState state = new ChangeState();
    		Thread t1 = new Thread(state);
    		t1.start();
    		
    		// 利用线程标记 卡住主线程
    		while (!state.isTrue) {
    		}
    		System.out.println(Thread.currentThread().getName() + "main结束" + state.isTrue);
    	}
    }
    
    class ChangeState implements Runnable{
    	// 标记线程状态
    	public volatile boolean isTrue = false;
    	// 记录循环次数
    	public int num = 0;
    
    	@Override
    	public void run() {
    		while (!isTrue) {
    			num++;
    			try {
    				Thread.sleep(1000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			if (num == 5) {
    				// 修改状态
    				isTrue = true;
    			}
    			System.out.println(Thread.currentThread().getName() + " " + num);
    		}
    	}
    }
    
    展开全文
  • 这里是Python7编程挑战-多线程专题!每天学习3个问题,包括初级,中级,高级问题各1个。今天是第5天!一起来呀,就7天!每日3题是麦叔面试系列专题之一,每天包括初级,中级,高级难度题目各一道。每日3题,100天...
  • 这段话是从网上copy过来的,总结了一下原子操作的作用。但是文中提到的osbase.h文件找不到。可能是因为版本升级我的lib中没有这个文件。 iOS平台下的原子操作函数都以OSAtomic开头,使用时需要包含头文件。不同...
  • 这段话是从网上copy过来的,总结了一下原子操作的作用。但是文中提到的osbase.h文件找不到。可能是因为版本升级我的lib中没有这个文件。 iOS平台下的原子操作函数都以OSAtomic开头,使用时需要包含头文件。不同线程...
  • 多线程读写同一Socket话基本上是会出问题 想要不出问题话,除非你多个线程作用是一样 在TCP/TP卷2中有这样结构体 struct sockbuf{ short sb_flags; .......... }so_recv,so_...
  • synchronized是对类当前实例进行加锁,防止其他线程同时访问该类该实例所有synchronized块,注意这里是“类当前实例”,类两个不同实例就没有这种约束了。 那么static synchronized恰好就是要控制类...
  • 1. 相关概念1.1 解释器Python 解释器主要作用是将我们在 .py 文件中写好代码交给机器去执行,比较常见解释器包括如下几种:CPython:官方解释器,我们从官网下载安装后获得就是这个解释器,它使用 C 语言...
  • Java中每个对象都有一个内置锁,当程序运行到非静态synchronized同步方法上时,自动获得与正在执行代码类当前实例(this实例)...所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(...
  •  多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!  so,不使用线程锁, 可能导致错误 分布式锁,进程锁,线程锁到底是什么 在分布式集群系统开发中,线程锁...
  • C++ 加锁的原则

    2020-07-26 18:51:02
    规则10.1 多线程、进程并行访问共享资源时,一定要加锁保护 说明:共享资源包括全局变量,静态变量,共享内存,文件等。 建议封装像智能指针一样对象对锁进行管理,比如我们就封装了一个auto_lock,在构造时申请 ...
  • 多个线程同时对同一个对象进行读写操作,很容易会出现一些难以预料的问题。...这里我想总结一下java多线程中的各种锁的作用和用法,还有容易踩的坑。 这篇文章里面有很多的文字和代码都来自于《实战Java高...
  • 1.保证进程情况下变量可见性 2.禁止 instance = new Singleton();指令重排 注意: instance = new Singleton() 分解为: 1.分配对象内存空间 2.初始化对象 3.将instance指向分配内存空间 若不加 volatile;2和3...
  • 让复用变得容易,拒绝重复...将被保护数据暴露到互斥量作用域之外 被保护数据访问接口本身就存在竞态条件(条件竞争) 不要暴露你数据 来看下面例子: struct protected_data { char data[100]; } class ...
  • Sqlite的加锁机制

    千次阅读 2016-08-09 15:24:41
    作用:Sqlite的加锁毫无疑问是保证数据安全,当多个账户操作同一个数据库时,会避免数据非安全现象,类似于多线程的迸发机制,用syncronized同步代码块来保证线程安全。如 LOCK TABLES `t_region` WRITE; /*!...
  • 以下代码为不加锁的时候的代码,毫无疑问是乱序的,因为两个线程同时操作了全局变量 #include <thread> #include <iostream> #include <mutex> #include <list> // 全局变量 std::mutex my_...
  • 本文重点探究以下几个问题:多线程内存共享线程类获取线程结果方法:join/setDaemon对线程退出的作用线程无法利用多核,不是真正的并发多线程内存共享多个线程访问同一内存变量时,需要通过锁机制来实现,未使用锁的...
  • 嵌入式实时系统线程的作用

    千次阅读 2011-03-14 20:58:00
    <br />具有优先级抢占式任务系统由于响应实时性在嵌入式广泛使用,除了合理安排各个线程的优先级外,处理好线程之间关系极大影响系统性能和实时性,是程序员在实际进行系统设计时工作重点。...
  • ConcurrentHashMap是线程安全的HashMap,我就不废话了,实现的思想是使用的分段加锁,使用分段加锁的作用当然就是可以有效提升并发量,可以对比一下所有操作都加锁的HashTable,就能明白分段加锁的好处了 ...
  • 多线程死锁

    2018-06-28 00:16:52
    一、死锁:所谓死锁是指线程因竞争资源而造成一种僵局(互相等待),若无外力作用,这些进程都无法向前推进。二、如何避免死锁1、加锁顺序(线程按照一定顺序加锁)---------本例就是采用这种方法2、加锁...
  • 在java中要实现多线程之间同步操作主要有如下两种方式:synchronized关键字和对象wait()和notify()/notifyAll(); 首先来看一下synchronized关键使用 作用范围: 给指定对象加锁,在进入同步代码块前...
  • 如果一个多线程程序某个线程调用了fork()函数,那么新创建子进程是否将自动创建和父进程相同数量线程呢? 答案是否定,正确解释是子进程只拥有一个执行线程,他是调用fork()那个线程完整复制。并且子...
  • 规则10.1 多线程、进程并行访问共享资源时,一定要加锁保护  说明:共享资源包括全局变量,静态变量,共享内存,文件等。 建议封装像智能指针一样对象对锁进行管理,比如我们就封装了一个auto_lock,在构造时申请...
  • 多线程的锁机制

    2020-06-09 09:55:15
    1、synchronized对象同步锁:synchronized是对对象加锁,可作用于对象、方法(相当于对this对象加锁)、静态方法(相当于对Class实例对象加锁,锁住该类所有对象)以保证并发环境的线程安全。同一时刻只有一个...
  • 1.Java中提供一种最简单的加锁机制(同步机制) 同步:两个线程之间不再是孤独了,需要互相考虑对方情况。 synchronized(同步) 1、作用:就是一把锁,实现两个线程之间互斥 2、语法用法: (1)、作为方法修饰...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 489
精华内容 195
关键字:

多线程加锁的作用