线程同步机制synchronized中锁的判断以及锁的作用范围

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题,Java中提供了**同步机制(synchronized)**来解决。

在这里插入图片描述

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制

同步代码块

  • 同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
     需要同步操作的代码
}
  • 同步锁:
    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
    锁对象 可以是任意类型。
    多个线程对象,要使用同一把锁。
    注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void method(){
    可能会产生线程安全问题的代码
}

同步方法的锁对象:
(1)静态方法:当前类的Class对象
(2)非静态方法:this

Lock锁(JUC中的锁机制)

  • java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
  • Lock锁也称同步锁,加锁与释放锁方法,如下:
    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。

接着我们通过八个问题以及八个实例来理解一下多线程锁synchronized的作用范围。

锁的8个问题

首先模拟手机发短信和发邮件,创建Phone类,类中声明两个同步方法,其中一个同步方法发送短信,另一个同步方法发送邮件。

class Phone{  
    //发送短信的同步方法
    public synchronized void sendSMS() throws Exception{     
        System.out.println("------sendSMS");  
    }
    //发送邮件的同步方法
    public synchronized void sendEmail() throws Exception{   			
    	System.out.println("------sendEmail");    
 	}   
}
public class Lock_8 {
    public static void main(String[] args) throws Exception{
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        //发送短信的线程
        new Thread(()->{
            try {
                phone.sendSMS();
            }catch(Exception e){
                e.printStackTrace();
            }
        },"AA").start();        
        //让主线程sleep0.2秒已区分上下两个线程的执行顺序        
        Thread.sleep(200);        
        //发送email的线程        
        new Thread(()->{
            try {
                phone.sendEmail();
                //phone.getHello();
                //phone1.sendEmail();
            }catch(Exception e){
                e.printStackTrace();
            }
        },"BB").start();
    }
}

① 标准访问,先打印短信还是邮件

测试结果:先输出sendSMS 后输出sendEmail
在这里插入图片描述

分析原因:

由于Phone类中无论发短信还是发邮件两个方法均为synchronized非静态同步方法,则此时的锁对象就是this即主方法中new的Phone对象,因此在主方法中无论有多少个线程由于竞争的都为同一把锁每次都只能由先抢到锁并获取到cpu资源的线程进入Phone类中执行相应非静态同步方法,而其他线程无论多久都得先在Phone类外等待。本例中通过Thread.sleep(200); 让主线程休眠0.2秒使“AA”线程先于“BB”线程获取到cpu资源,因此“AA”线程先获取锁并执行相应方法,执行完毕后将锁释放,此时“BB”线程才能进入Phone类中执行相应非静态方法。

② 停4秒在短信方法内,先打印短信还是邮件

修改Phone类:

在这里插入图片描述

测试:隔4秒后先输出了sendSMS 后输出sendEmail

在这里插入图片描述

分析原因:

在发短信同步方法中使其sleep四秒钟,由于sleep并不会释放锁以及cpu资源,因此并不影响获取锁以及线程执行的先后顺序,过程和①一致。

③ 普通的hello方法,是先打短信还是hello

修改Phone类:
在这里插入图片描述

修改main:

在这里插入图片描述

测试结果:先输出Hello world!隔4秒后输出sendSMS

在这里插入图片描述

分析原因:

synchronized同步机制只对标有它的同步方法或者同步代码块有效,对于普通的getHello方法并没有同步锁的限制,因此“AA”线程获取同步锁并得到CPU资源进入Phone资源类中的同步方法休眠4秒,而与此同时主线程sleep0.2秒后“BB”线程执行没有synchronized同步机制限制的getHello方法,0.2秒后先输出hello world!“AA”线程休眠4秒后输出sendSMS并释放资源。

④ 现在有两部手机,先打印短信还是邮件

修改main:

在这里插入图片描述

测试结果:先输出sendEmail 隔4秒后输出sendSMS

在这里插入图片描述

原因分析:

首先明确,资源类Phone中发送短信和发送邮件均为非静态同步方法,锁对象为this即当前对象,而主方法中new了两部手机phone和phone1,因此此时对于线程“AA”与线程“BB”来说,它们竞争的已经不是同一把锁,所以对于锁的争抢互不影响,“AA”获取phone当前对象锁,而“BB”则获取phone1对象锁,因此“AA”在优于”BB“先获取到CPU资源后携带着phone锁进入到资源类中执行相应方法且休眠4秒,而”BB“线程在0.2秒后也获取到CPU资源,携带着phone1锁进入到资源类执行sendEmail方法,因此执行顺序为先输出sendEmail 隔4秒后输出sendSMS。

⑤ 两个静态同步方法,1部手机,先打印短信还是邮件

修改Phone类:

在这里插入图片描述

修改main:

在这里插入图片描述

测试结果:隔4秒后先输出了sendSMS 后输出sendEmail

在这里插入图片描述

原因分析:

首先明确资源类中两个同步方法由非静态变为了静态同步方法,因此此时的锁对象已经由原来的this当前实例对象变为了当前类的Class对象。因此此时的锁对象与new出的phone无关,而是造出phone的工厂Class Phone这个Class的类对象。所以整体流程和①的流程相似,因为让主线程休眠0.2秒的原因,线程“AA”会先获取到CPU资源并携带着锁进入资源类Phone执行相应静态同步方法4秒后输出sendSMS,随后释放锁和CPU资源,“BB”线程再获取锁以及CPU资源结束在资源类Phone外的等待,进入执行sendEmail方法。

⑥ 两个静态同步方法,2部手机,先打印短信还是邮件

修改main:

在这里插入图片描述

测试结果:隔4秒后先输出了sendSMS 后输出sendEmail

在这里插入图片描述

分析原因:

与⑤原因相同,由于静态同步方法,锁对象已经由当前实例对象this变为生产实例对象的工厂Class Phone,即当前Phone类的Class对象,线程“AA”和线程“BB”都要竞争同一把Class对象锁,而与new出的实例无关,因此不管有几部手机,不管new出几个phone都不影响输出结果,因为phone不是锁对象。

⑦ 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件

修改Phone类:

在这里插入图片描述

修改main:
在这里插入图片描述

测试结果:先输出sendEmail 隔4秒后输出sendSMS
在这里插入图片描述

原因分析:

首先明确资源类中sendSMS依旧为静态同步方法,锁对象为当前类的Class对象。而sendEmail方法由静态同步方法变为了非静态同步方法,因此锁对象为当前实例对象this。“AA”线程需要的锁对象为Class对象,而“BB”线程需要的锁对象为当前实例对象phone,因此两个线程之间不存在锁的竞争,只存在CPU资源获取的先后顺序,而由于主线程休眠的0.2秒,"AA线程"先获取到CPU资源,携带着Class对象锁进入到资源类中执行senSMS静态同步方法并休眠4秒,而“BB”线程在0.2秒后获取到CPU资源,携带着当前phone对象锁进入到资源类中执行sendEmail方法,因此先输出sendEmail 隔4秒后输出sendSMS。

⑧ 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件

修改main:

在这里插入图片描述

测试结果:先输出sendEmail 隔4秒后输出sendSMS
在这里插入图片描述

原因分析:

首先明确,静态同步方法与非静态同步方法与⑦相同,没有改变,因此线程“AA”与线程“BB“竞争的锁资源并非同一个,所以有多个实例phone、phone1由于锁的不同并不会影响两个线程进入资源类执行相应同步方法的先后顺序。因此流程依旧和⑦相同。

总结:

1.synchronized实现同步的基础:Java中的每一个对象都可以作为锁,具体表现为以下3种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象

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

3.当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁。可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

4.所有的非静态同步方法用的是同一把锁——当前实例对象(this),所有的静态同步方法用的也是同一把锁——类对象本身,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。

5.普通方法与同步锁无关,不影响线程的执行。
综上所述,在上述8个问题中,想要判断程序输出的先后顺序,一定要先搞清楚当前线程所竞争的锁资源是什么,以及线程所竞争的锁资源是不是同一个,搞清楚这一点后,问题就迎刃而解了,而对于锁的理解,就可以将其理解为非静态同步方法中this当前实例对象或者是静态同步方法中的当前类的Class对象。

此时再去理解同步代码块中同步锁的概念就简单的多了:

synchronized(同步锁){
   需要同步操作的代码
}
  • 同步锁:
    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

    锁对象 可以是任意类型。(this或者是Phone.Class

​ 多个线程对象,要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值