多线程(四) 线程同步(上)-synchronized,volatile

多线程 专栏收录该内容
21 篇文章 4 订阅

前言

--前言是为了帮助大家能够更好的理解线程通信和线程同步,了解Java内存模型的抽象。
前言部分引用文章地址:http://www.infoq.com/cn/articles/java-memory-model-1?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_articles_clk

  在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

  在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

  同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

Java内存模型的抽象
在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

这里写图片描述

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
(1)首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
(2)然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
这里写图片描述

  如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

同步方式

  同步方式我所了解的有七种,分别如下,篇幅较大,线程同步将分为上中下三章,本章为上。

<一>同步代码块
synchronized(同步监视器){
//需要访问的共享数据
}
同步监视器:俗称“锁”,可以使用任意对象充当,但是确保多个线程持有同一把锁(同一个对象)

<二> 同步方法
public synchronized void show(){} //隐式的锁—this

  为了详细讲述synchronized的使用及锁,列举以下八种情况,希望大家能从示例代码和运行结果中清楚明白。

线程八锁:

//1 标准访问,苹果还是Android
//2 加入Thread.sleep,苹果还是Android
//3 加入Hello方法,苹果还是hello
//4 加入第2部手机,苹果还是Android
//5 两个静态同步方法,同一部手机,苹果还是Android
//6 两个静态同步方法,有2部手机,苹果还是Android
//7 一个静态同步方法,一个普通同步方法,有1部手机,苹果还是Android
//8 一个静态同步方法,一个普通同步方法,有2部手机,苹果还是Android

在这里只列出标准访问代码和所有运行结果,大家自己在标准访问代码的基础上加条件调试:

标准访问代码:

public class Phone {

    public  synchronized void getIOS()throws InterruptedException
    {
        //Thread.sleep(4000);
        System.out.println("-----getIOS");
    }
    public synchronized void getAndroid() throws InterruptedException
    {
        System.out.println("-----getAndroid");
    }

    /*public void getHello() throws InterruptedException
    {
        System.out.println("-----getHello");
    }*/
}

public static void main(String[] args){
        final Phone phone = new Phone();
        //final Phone phone2 = new Phone();

        new Thread(new Runnable(){
            @Override
            public void run(){
                for (int i = 0; i < 1; i++){
                    try{
                        phone.getIOS();
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        },"AA").start();

        new Thread(new Runnable(){
            @Override
            public void run(){
                for (int i = 0; i < 1; i++){
                    try{
                        phone.getAndroid();
                        //phone.getHello();
                        //phone2.getAndroid();
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        },"BB").start();
    }

运行结果分别如下:
//1 标准访问,苹果还是Android

-----getIOS
-----getAndroid

//2 加入Thread.sleep,苹果还是Android(后面别注释掉Thread.sleep)

-----getIOS
-----getAndroid

//3 加入Hello方法,苹果还是hello(后面别注释掉hello方法)

-----getHello
-----getIOS

//4 加入第2部手机,苹果还是Android

-----getAndroid
-----getIOS

//5 两个静态同步方法,同一部手机,苹果还是Android

-----getIOS
-----getAndroid

//6 两个静态同步方法,有2部手机,苹果还是Android

-----getIOS
-----getAndroid

//7 一个静态同步方法,一个普通同步方法,有1部手机,苹果还是Android

-----getAndroid
-----getIOS

//8 一个静态同步方法,一个普通同步方法,有2部手机,苹果还是Android

-----getAndroid
-----getIOS

原因:
  一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法。

  锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法。
  加个普通方法后发现和同步锁无关 。
  换成两个对象后,不是同一把锁了,情况立刻变化。
  都换成静态同步方法后,情况又变化。

  所有的非静态同步方法用的都是同一把锁——实例对象本身,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。


  所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

<三>wait()与notify()
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

详细见:wait、sleep、notify、notifyAll的使用方法

四、使用特殊域变量(volatile)实现线程同步

a.volatile关键字为域变量的访问提供了一种免锁机制, 
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新, 
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值 
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 

  补充解释:通过前言引用的内存知识我们知道线程共享的变量在线程私有的栈中(本地内存),存储的不一定是内存对象的地址。如果是java的对象,则是存放的是地址。如果是基本数据类型,则是一个复制的过程,即将值在栈内存(本地内存)和堆内存之间,来回复制同步。
  volatile所要解决的问题,就是告诉cpu直接访问主存的对象,而不要访问栈中的变量,从而达到了同步的效果。

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。

具体例子及使用场景推荐大家看这篇转载博文:java之用volatile和不用volatile的区别

参考文章:(http://www.cnblogs.com/duanxz/p/3709608.html?utm_source=tuicool&utm_medium=referral);
(http://blog.csdn.net/wuwenxiang91322/article/details/25336905);

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论
请先登录 后发表评论~
©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

淡淡的倔强

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值