数据同步与对synchonized的深入理解

前言

在上文中,我写了ReentrantLock有关的代码分析,它是基于Lock基础类的。在Java中一般有两种实现锁的方式,一种是基于Lock的,一种是基于JVM的synchonized锁的,在本文中将要介绍后面一种方式。jDK官网中对它进行了这样的解释:它可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来继续来进行。
它具备下面几个特点

  1. 线程在解锁前必须把共享变量的最新值刷新到主内存中。
  2. 线程在加锁时将清空工作内存中共享变量的值,从而在使用共享变量时需要从主内存中重新获取最新的值
  3. 线程解锁前对共享变量的修改在下次加锁时其他线程可见
  4. 严格遵守Java happens-before规则,一个monitor exit指令之前必定有一个monitor enter。

数据不一致问题

举一个经典的例子,创建两个线程,index=0,执行index++,并打印输出,当大于300时退出程序。
理想的状态是index从0逐一变到300,
但实际的状况会出现下面几种情况:

  • 两个线程中的index数据一致,比如都是10
  • 数据输出出现了跳跃,比如,上一个数据是30,但接下来的一个却是32,丢失了31
  • 数据超过了300,出现了301
    对上面情况进行解析:
  • 线程1执行index+1, 然后被暂停,执行线程2操作,由于线程1并没有对index进行赋值操作,index仍然为原值,并没有增加为10,线程2执行index+1并赋值,变成了10,之后线程执行原来停留的index+1,也为10,出现了数据的重复。
  • 当线程1和线程2都执行到了index=30的位置,其中线程2将index修改为31后执行输出之前切换到了线程1,执行index+1,index变成了32,并输出,而中间的31却没有被输出
  • 当index=299的时候,线程1和线程2都看到了条件满足,线程2暂时停顿,线程1中index+1变成了300,之后线程2继续执行,因为它此时已经在条件判断代码块里面,不受条件的控制,执行index+1,就变成了301

monitor

monitor机制需要几个元素来配合,分别是:

  1. 临界区
  2. monitor对象及锁
  3. 条件变量以及定义在monitor对象上的wait和signal操作。
    使用monitor机制主要是为了互斥进入临界区,每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得。sychronized关键字包含monitor enter和monitor exit两个JVM指令。
    monitor存在计数器,如果为0,则说明该monitor的lock还没有被获得。某个线程获得后,该计数器会加1,当计数器为0,那就意味着该线程不再拥有该monitor的所有权。

synchonized出现的锁

在synchonized中会出现3类锁,偏向锁、轻量级锁和重量级锁。

  • 偏向锁是指在一段同步代码一直被一个线程访问,那么该线程就会自动获取锁,降低获取锁的代价。
  • 轻量级锁是指当锁为偏向锁的时候,被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方法尝试获取锁,不会阻塞,提高性能。
  • 重量级锁是指当锁为轻量级锁时,另一个线程在自旋,当尝试一定次数之后,还是没有获取到锁,就会进入阻塞,该锁就会变成重量级锁,重量级锁会让其他申请的线程进入阻塞,性能降低。

synchonized锁的实现

主要有三种形式:

  1. 修饰实例对象,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码前获得给定对象的锁
    对象锁(synchonized method{})和类锁(static synchonized method{})的区别
    对象锁也叫实例锁,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁
    对象锁能防止在同一时刻多个线程访问同一个对象的synchonized块
    类锁是一个全局锁,无论多少个对象共享一个锁,当一个线程访问时,其他线程等待。
    将synchronized关键字加static方法和不加static方法有时可能效果是一样的,但两者有着本质的不同
    synchronized关键字加static静态方法是将Class类对象作为锁,而synchronized关键字加到非static静态方法是将方法所在类的对象作为锁
public class Service {
    synchronized public static void printA() {
        try {
            System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printA");
            Thread.sleep(3000);
            System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    synchronized public static void printB() {
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printB");
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printB");

    }
    synchronized public  void printC() {
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printC");
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printC");

    }
}


public class ThreadA extends Thread {
    private Service service;
    public ThreadA(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
      service.printA();
    }
}

public class ThreadB extends Thread {
    private Service service;
    public ThreadB(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run(){
        service.printB();
    }
}

public class ThreadC extends Thread {
    private Service service;
    public ThreadC(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.printC();
    }
}

public class Run {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();

        ThreadC c = new ThreadC(service);
        c.setName("C");
        c.start();
    }
}

运行结果:
线程名称为:A在1561702771777进入printA
线程名称为:C在1561702771778进入printC
线程名称为:C在1561702771778离开printC
线程名称为:A在1561702774777离开printA
线程名称为:B在1561702774777进入printB
线程名称为:B在1561702774777离开printB

从中可以看出加上static和不加static是有区别的,主要是产生了不同的锁,一个是将类Service的对象作为锁,另一个是将Service类对应的Class类的对象作为锁,A、B线程和C线程是异步的关系,而A线程和B线程是同步的关系。

synchronized锁重入

该关键字拥有重入锁的功能,即在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时也可以得到该对象锁。

public class Mysyn {
    synchronized public void syn1() {
      System.out.println("syn1");
      syn2();  
    }
     synchronized public void syn2() {
      System.out.println("syn2");
      syn3();  
    }
    synchronized public void syn3() {
      System.out.println("syn3");
    }
}

public class MyThread extends Thread {
     @Override
    public void run() {
				       Mysyn syn = new Mysyn();
       syn.syn1();
    }
}

public class Run {
   public static void main(String[] args) {
       MyThread t = new MyThread();
       t.start();
   }
}

结果输出:
syn1
syn2
syn3

上述代码中的锁是对象锁,当这个对象锁还没有被释放时,还可以重新进入获取该对象锁,如果不是可重入锁的话,是不能调用sync2()方法的。


不同锁形式调用

当在一个类中定义了不同形式的synchronized()方法时,混合调用可能会产生不同的效果。

  • 锁重入支持继承的环境,当父类中定义了一个synchronized()方法时,子类继承并重新定义了该方法,并在方法内部调用了父类的方法,在实际执行中会先调用子类的方法,然后调用父类的方法。
  • 重写方法如果不使用synchronized关键字,会变成非同步方法,使用后会变成同步方法
  • 如果只是在方法的部分区域定义synchronized代码块,在区域内部的代码会同步,在区域外部的代码会异步
  • 当先后调用synchronized(this)方法和synchronized public void func1()方法时,都会将当前类的对象作为锁,都是一把锁,运行的结果是同步的效果。
  • 使用同步代码块锁非this对象,即synchronized(非this)代码块中的程序与同步方法是异步的,因为有两把锁,不与其他锁this同步方法争抢this锁,可以大大提高运行效率。
  • 多个锁就是异步执行
  • 同步syn static方法可以对类的所有对象实例起作用
  • 同步syn(class)代码块可以对类的所有对象实例起作用
  • 在大多数情况下,同步synchronized代码块不使用String作为锁对象,这是String常量池所带来的问题

才疏学浅,有不足的地方希望多多指教
本文章也在我的个人博客 https://spurstong.github.io

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值