【Java】synchronized使用和原理

前言
为什么用synchronized?

在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile(这个关键字也很重要,可以关注下)

为何使用同步?

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(增删改查),将会导致数据的不准确,相互之间产生冲突。类似于在atm取钱,银行数据确没有变,这是不行的,要存在于一个事务中。因此加入了同步锁,以避免在该线程没有结束前,调用其他线程。从而保证了变量的唯一性,准确性。

实现原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
(1)synchronized同步代码块:synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象的锁,如果这个锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取对象锁一直失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
(2)同步方法:方法级的同步是隐式的,无须通过字节码指令来控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用的时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论是正常完成还是非正常完成)时释放monitor对象。在方法执行期间,执行线程持有了管程,其他线程都无法再次获取同一个管程。

怎么使用synchronized?

synchronized可以用于修饰方法、代码块(在方法体内的代码块),但是不能用于修饰类、构造器或者非方法体内的代码块,具体的例子如下:

1 使用synchronized修饰方法,同步方法默认用this或者当前类class对象作为锁。

   //使用synchronized修饰
   public synchronized void printStr1(){
        System.out.println("PrintStr1 start...");
        printInfo("str1",3000);
    }
    //不使用synchronized修饰
    public void printStr3(){
        System.out.println("PrintStr3 start");
        printInfo("str3 ",1000);
    }
    //重用方法
    public void printInfo(String info,long time){
        System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
        try {
            Thread.sleep(time);
            System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
    }
    public static void main(String[] args){
        Account ac=new Account("2",1000.);
        new Thread(ac::printStr1).start();
        new Thread(ac::printStr3).start();
        }

可以看到结果中printStr3由于没有加synchronized,虽然printStr1先取得了锁,但是由于printStr1中线程sleep了3秒,所以printStr3先进行执行了,即名字为 Thread-1,即没有添加synchronized的方法在多线程时,执行不会受到限制,因此是线程不安全的,容易使操作的数据出现脏读的情况。
在这里插入图片描述
2 两个方法都用 synchronized 修饰,但是不同线程作用于同一个对象

 public synchronized void printStr(){
        System.out.println("PrintStr start ...");
        printInfo("str",1000);
    }
    public synchronized void printStr1(){
        System.out.println("PrintStr1 start...");
        printInfo("str1",3000);
    }
    public void printInfo(String info,long time){
        System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
        try {
            Thread.sleep(time);
            System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
    }
    public static void main(String[] args){
    //只声明了一个对象
        Account ac=new Account("2",1000.);
        new Thread(ac::printStr).start();
        new Thread(ac::printStr1).start();
        }

可以从结果看出,不同的线程作用于同一个对象,则synchronized的锁是相同的,所以如果printStr先获得了线程,就会一直执行,知道释放锁,然后printStr1才开始执行
在这里插入图片描述
3 两个方法都用 synchronized 修饰,但是不同线程作用于不同对象,不同对象调用的方法不一样

 public synchronized void printStr(){
        System.out.println("PrintStr start ...");
        printInfo("str",1000);
    }
    public synchronized void printStr1(){
        System.out.println("PrintStr1 start...");
        printInfo("str1",3000);
    }
    public void printInfo(String info,long time){
        System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
        try {
            Thread.sleep(time);
            System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
    }
 public static void main(String[] args){
        Account ac=new Account("2",1000.);
        Account ac1=new Account("2",1000.);
        new Thread(ac::printStr).start();
        new Thread(ac1::printStr1).start();
        }

从这个结果可以看出,当两个线程作用域不同对象的时候,他们的锁也不一样,所以printStr和printStr1是一起执行的,其中printStr线程名称为 Thread-0, printStr1为线程Thread-1,由于printStr1 阻塞(sleep)时间为3秒,而printStr阻塞时间为1秒,所以Thread-0先执行结束释放锁,但是这个锁释放后就闲置了,没有被printStr1使用,因为两者属于不同的锁。
在这里插入图片描述
4 两个方法都用 synchronized 修饰,但是不同线程作用于不同对象,不同对象调用的方法一样

 public synchronized void printStr(){
        System.out.println("PrintStr start ...");
        printInfo("str",1000);
    }
    public synchronized void printStr1(){
        System.out.println("PrintStr1 start...");
        printInfo("str1",3000);
    }
    public void printInfo(String info,long time){
        System.out.println(info+"==>"+Thread.currentThread().getName()+" create");
        try {
            Thread.sleep(time);
            System.out.println(info+"==>"+Thread.currentThread().getName()+" execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(info+"==>"+Thread.currentThread().getName()+" end");
    }
 public static void main(String[] args){
        Account ac=new Account("2",1000.);
        Account ac1=new Account("2",1000.);
        new Thread(ac::printStr1).start();
        new Thread(ac1::printStr1).start();
        }

可以看到,不同对象由于具有不同的锁,所以当不同对象调用相同的方法时,会出现以下的形式:
在这里插入图片描述

【注】以上的情况,也说明当分布式情况下,synchronized就失效了,因为不同的对象调用相同的方法,最终是多个线程一起调用,会导致操作的数据出现问题。那么分布式情况下怎么保证线程安全呢,一般有三种方法:

1 队列:将所有要执行的任务放入队列中,然后一个一个消费,从而避免并发问题
2 悲观锁:将数据记录加版本号,如果版本号不一致就不更新,这种方式同Java的CAS理念类似。
3 分布式锁:
3.1 基于数据库实现的分布式锁
3.2 基于Zookeeper实现分布式锁
3.3 基于缓存(redis)来实现分布式锁

5 synchronized作用于静态方法,这个时候,使用synchronized修饰的方法在类加载器中只有一个,无论是几个不同的对象调用,只要某个对象正在调用此静态方法,且具备了方法锁,另一个线程只有等待。

public static synchronized void printStr2(){
        System.out.println("PrintStr2 start ...");
        System.out.println("==>"+Thread.currentThread().getName()+" create");
        try {
            Thread.sleep(1000);
            System.out.println("==>"+Thread.currentThread().getName()+" execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("==>"+Thread.currentThread().getName()+" end");
    }
     public void run() {
        printStr2();
        }
        public static void main(String[] args){
	        Account ac=new Account("2",1000.);
	        Account ac1=new Account("2",1000.);
	       Thread th1=new Thread(ac);
	       Thread th2=new Thread(ac1);
	       th1.start();
	       th2.start();
	       }

在这里插入图片描述
6 当把synchronized删除后,会产生以下的结果,所以synchronized当静态条件下,其实充当了一个类锁,而不是对象锁,能保证同一个类的不同对象实例调用此方法时,如果被锁,则必须等待。但是这个并不是说在分布式系统中就可以用static synchronized 修饰的方法就是线程安全,因为static修饰只是在类加载器中存在唯一一份,但是分布式系统中,不同的主机其实都需要进行加载一份类,这个时候,其实不能满足要求,还是需要借助以上说的分布式系统中的解决方案来执行。

public static void printStr2(){
        System.out.println("PrintStr2 start ...");
        System.out.println("==>"+Thread.currentThread().getName()+" create");
        try {
            Thread.sleep(1000);
            System.out.println("==>"+Thread.currentThread().getName()+" execute");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("==>"+Thread.currentThread().getName()+" end");
    }
     public void run() {
        printStr2();
        }
        public static void main(String[] args){
	        Account ac=new Account("2",1000.);
	        Account ac1=new Account("2",1000.);
	       Thread th1=new Thread(ac);
	       Thread th2=new Thread(ac1);
	       th1.start();
	       th2.start();
	       }

在这里插入图片描述
7 synchronized 作用于方法体中的代码块,作用是只针对代码中的同步部分进行修饰,避免了将整个方法都使用synchronized进行修饰,一定程度上可以减小程序开销,加快程序的执行效率,但问题就是我们需要找到同步代码部分,一般是整个方法体的一小部分。这里需要注意什么是同步代码,总体来讲,就是改变一些共享数据值的代码,比如完成银行取款时候的账户余额等。

public void printStr3(){
        System.out.println("PrintStr3 start");
        printInfo("str3 ",1000);
    }
    public void run() {
        //同步条件:YourClass.class / this / 单例对象
        synchronized(this){
            printStr3();
        }
    }
    public static void main(String[] args){
        Account ac=new Account("2",1000.);
        Account ac1=new Account("2",1000.);
        Account ac2=new Account("2",1000.);
        Account ac3=new Account("2",1000.);
        //new Thread(ac::printStr1).start();
        //new Thread(ac1::printStr1).start();
        //new Thread().start();
       Thread th1=new Thread(ac);
       Thread th2=new Thread(ac1);
        Thread th3=new Thread(ac2);
        Thread th4=new Thread(ac3);
       th1.start();
       th2.start();
        th3.start();
        th4.start();
        }

使用 this 的时候,主要是当前类对象的锁,则不同的对象,锁不一样,就导致执行仍然是乱序执行,可以将其改为 单例对象 和 类锁,具体操作如下:
在这里插入图片描述
8 将其改为 单例对象 和 类锁

类锁:

 synchronized(Account.class){
            printStr3();
        }

在这里插入图片描述
单例对象锁:

private static final Account ac=new Account();
 synchronized(ac){
            printStr3();
        }

在这里插入图片描述

【以上是synchronized的相关原理和使用,关于多线程还有很多内容,慢慢学习】

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值