【五】Java多线程之线程同步synchronized(同步方法、同步代码块、偏向锁、轻量级锁、自旋锁、重量级锁、消除锁)

一、简介

synchronized能保证原子性、可见性、有序性

synchronized是一种 对象锁(锁的是对象而非引用变量)作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 。

synchronized是可重入的。其可重入最大的作用是避免死锁,如:子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁。

换句话说,这里的可重入是指定同一个线程可重入获取同一个对象锁,不同的线程对同一个对象的锁是互斥的。

Java 虚拟机中的Synchronization基于进入和退出Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。

被 synchronized 修饰的同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的。

同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置。

JVM保证每一个monitorenter都有一个monitorexit与之相对应。

任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。

线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁

1.1原理机制:

线程获取锁资源失败进入同步队列等待竞争锁资源。

线程在获取到对象的锁后,执行的逻辑中如果遇到wait,则释放CPU资源,释放锁资源,进入等待队列,等着被notify或者notifyall唤醒,唤醒后会再次去获取锁。

当做完synchronized中的逻辑过后,从synchronized出来JVM会自动帮我们释放锁

1.2对象在内存中的布局分为三块区域

  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
  • 对象头Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

1.3锁的状态总共有四种:

1.无锁状态

2.偏向锁

3.轻量级锁

4.重量级锁

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

1.4Java虚拟机对synchronize的优化:

1.偏向锁

1.6之后加入的新锁,它是一种针对加锁操作的优化手段。

出现场景:锁不仅不存在多线程竞争,而且总是由同一线程多次获得,

因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构。

当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

所以,如果没有多个线程去竞争锁,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁

但是对于锁竞争比较激烈,每次申请锁的线程都是不相同的,偏向锁就失效了。

偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

2.轻量级锁

偏向锁失败,则采用轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。

对绝大部分的锁,在整个同步周期内都不存在竞争,这只是经验数据。

出现场景:有多个线程需要访问相同的锁,但这多个线程是替执行同步块,并不是在同一时间竞争。

如果存在线程同一时间访问同一锁的场合,就可能会导致轻量级锁膨胀为重量级锁。

3.自旋锁

轻量级锁失败后,JVM为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

如果直接在操作系统层面挂起线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

自旋锁是假设在不久将来,当前的线程可以获得锁,因此JVM会让当前想要获取锁的线程做几个(不多,类似于50、100个循环)空循环(这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。

如果在若干次空循环后还不能获得锁,那才会将线程在操作系统层面挂起,升级为重量级锁。

4.重量级锁

出现场景:多个线程同意时间访问同一锁

5.锁消除

消除锁是虚拟机另外一种锁的优化。

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

比如:StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

二、两种线程同步的方式

1.同步化方法

使用synchronized关键字修改方法来同步化对方法的访问。

当调用方法时,调用线程进入对象监视器,对象监视器锁住对象(锁住synchronized修饰的那个方法的类的当前对象)。

在对象被锁住的同时,其他线程不能进入该方法,也不能进入该对象定义的其他同步化方法,但是该对象的非同步化方法可以被访问

当线程从方法返回时,监视器为对象解锁,允许下一个线程使用对象。

public class SynchronizedMethod {
    public static void main(String[] args) {
        int a[] = {1,2,3,4,5};
        Thread t1 = new Thread(new MyThread(a),"Child #1");
        Thread t2 = new Thread(new MyThread(a),"Child #2");
        Thread t3 = new Thread(new MyThread(a),"Child #3");
        Thread t4 = new Thread(new MyThread(a),"Child #4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class Test{
    private int sum;
    //同步化方法1
    synchronized int sumArray(int nums[]){
        sum=0; //重置sum
        for(int i=0; i< nums.length;i++){
            sum+=nums[i];
            System.out.println("Running total for "+Thread.currentThread().getName()+" is "+sum);
            try{
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return  sum;
    }
    //同步化方法2
    synchronized void count1(){
        int count=0;
        for(int i =0; i<=20;i++){

            System.out.println("Running count1 for "+Thread.currentThread().getName()+" is "+count);
            count++;
            try{
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    //非同步化方法
    void count2(){
        int count2=0;
        for(int i =0; i<=20;i++){
        System.out.println("Running count2 for "+Thread.currentThread().getName()+" is "+count2);
        count2++;
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        }
    }
}

class MyThread implements Runnable{
    /**
     * synchronized锁的是这个Test对象sa,
     * 而这个Test对象sa又是静态的,
     * 所以所有的MyThread线程的对象共用一个Test的对象sa
     * 当Test的对象sa被锁住了,就会互斥其他想用sa中的synchronized方法的线程
     * 因此,结果能看到:
     * 一旦有一个线程调用同步方法sumArray成功,锁了对象
     * 其他要调用sumArray和count1同步方法的线程都要等待解锁
     * 而非同步方法count2,则可在sa被锁住的时候被其他线程调用
     * 
     */
    
    static Test sa = new Test();
    int a[];
    int answer;
    MyThread(int nums[]){
        a = nums;
    }
    public void run(){
        System.out.println(Thread.currentThread().getName()+" starting.");
        if(Thread.currentThread().getName().equals("Child #1")||Thread.currentThread().getName().equals("Child #2")){
            answer = sa.sumArray(a);
            System.out.println("Sum for "+Thread.currentThread().getName()+" is "+answer);
        }
        if(Thread.currentThread().getName().equals("Child #3")){
            sa.count1();
        }
        if(Thread.currentThread().getName().equals("Child #4")){
            sa.count2();
        }

        System.out.println(Thread.currentThread().getName() +" terminating.");
    }
}

Child #1 starting.
Running total for Child #1 is 1
Child #2 starting.
Child #3 starting.
Child #4 starting.
Running count2 for Child #4 is 0
Running total for Child #1 is 3
Running count2 for Child #4 is 1
Running count2 for Child #4 is 2
Running total for Child #1 is 6
Running total for Child #1 is 10
Running count2 for Child #4 is 3
Running total for Child #1 is 15
Running count2 for Child #4 is 4
Running count2 for Child #4 is 5
Running count1 for Child #3 is 0
Sum for Child #1 is 15
Child #1 terminating.
Running count2 for Child #4 is 6
Running count1 for Child #3 is 1
Running count1 for Child #3 is 2
Running count2 for Child #4 is 7
Running count1 for Child #3 is 3
Running count2 for Child #4 is 8
Running count2 for Child #4 is 9
Running count1 for Child #3 is 4
Running count2 for Child #4 is 10
Running count1 for Child #3 is 5
Running count1 for Child #3 is 6
Running count2 for Child #4 is 11
Running count2 for Child #4 is 12
Running count1 for Child #3 is 7
Running count1 for Child #3 is 8
Running count2 for Child #4 is 13
Running count2 for Child #4 is 14
Running count1 for Child #3 is 9
Running count2 for Child #4 is 15
Running count1 for Child #3 is 10
Running count2 for Child #4 is 16
Running count1 for Child #3 is 11
Running count1 for Child #3 is 12
Running count2 for Child #4 is 17
Running count1 for Child #3 is 13
Running count2 for Child #4 is 18
Running count1 for Child #3 is 14
Running count2 for Child #4 is 19
Running count2 for Child #4 is 20
Running count1 for Child #3 is 15
Running count1 for Child #3 is 16
Child #4 terminating.
Running count1 for Child #3 is 17
Running count1 for Child #3 is 18
Running count1 for Child #3 is 19
Running count1 for Child #3 is 20
Running total for Child #2 is 1
Child #3 terminating.
Running total for Child #2 is 3
Running total for Child #2 is 6
Running total for Child #2 is 10
Running total for Child #2 is 15
Sum for Child #2 is 15
Child #2 terminating.

Process finished with exit code 0

 /**
     * synchronized锁的是这个Test对象sa,
     * 而这个Test对象sa又是静态的,
     * 所以所有的MyThread线程的对象共用一个Test的对象sa
     * 当Test的对象sa被锁住了,就会互斥其他想用sa中的synchronized方法的线程
     * 因此,结果能看到:
     * 一旦有一个线程调用同步方法sumArray成功,锁了对象
     * 其他要调用sumArray和count1同步方法的线程都要等待解锁
     * 而非同步方法count2,则可在sa被锁住的时候被其他线程调用
     * 如果Test对象sa不是静态的,则实现互斥锁需要static synchronized方法
     */

2.同步化语句

由同步化方法的介绍可知,同步化方法锁定对象后,该对象中的所有同步化方法都不能被其他线程使用,但是该对象中的非同步化方法还是可以被其他线程使用。

如果你想让对象被锁后,它的非synchronized修饰的方法也能被同步化呢?

这种情况一般发生在想使用由第三方创建的类,而无法访问源码时,那么在类中把synchronized加到相应的方法上是不可能的。

同步化语句使用:把对这种类定义的方法的调用放入synchronized代码块中就可以了。

synchronized(syncObject) {

  //允许访问控制的代码

}

public class SynchronizedMethod {
    public static void main(String[] args) {
        int a[] = {1,2,3,4,5};
        Thread t1 = new Thread(new MyThread(a),"Child #1");
        Thread t2 = new Thread(new MyThread(a),"Child #2");
        Thread t3 = new Thread(new MyThread(a),"Child #3");
        Thread t4 = new Thread(new MyThread(a),"Child #4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class Test{
    private int sum;

    int sumArray(int nums[]){
        sum=0; //重置sum
        for(int i=0; i< nums.length;i++){
            sum+=nums[i];
            System.out.println("Running total for "+Thread.currentThread().getName()+" is "+sum);
            try{
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return  sum;
    }

    void count1(){
        int count=0;
        for(int i =0; i<=20;i++){

            System.out.println("Running count1 for "+Thread.currentThread().getName()+" is "+count);
            count++;
            try{
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    void count2(){
        int count2=0;
        for(int i =0; i<=20;i++){
        System.out.println("Running count2 for "+Thread.currentThread().getName()+" is "+count2);
        count2++;
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        }
    }
}

class MyThread implements Runnable{

    static Test sa = new Test();
    int a[];
    int answer;
    MyThread(int nums[]){
        a = nums;
    }
    public void run(){
        System.out.println(Thread.currentThread().getName()+" starting.");

        if (Thread.currentThread().getName().equals("Child #1") || Thread.currentThread().getName().equals("Child #2")) {
            synchronized(sa) {
                answer = sa.sumArray(a);
                System.out.println("Sum for " + Thread.currentThread().getName() + " is " + answer);
            }
        }
        if (Thread.currentThread().getName().equals("Child #3")) {
            synchronized(sa) {
                sa.count1();
            }
        }
        if (Thread.currentThread().getName().equals("Child #4")) {

                sa.count2();

        }


        System.out.println(Thread.currentThread().getName() +" terminating.");
    }
}

Child #2 starting.
Child #4 starting.
Child #3 starting.
Child #1 starting.
Running total for Child #2 is 1
Running count2 for Child #4 is 0
Running count2 for Child #4 is 1
Running total for Child #2 is 3
Running count2 for Child #4 is 2
Running total for Child #2 is 6
Running total for Child #2 is 10
Running count2 for Child #4 is 3
Running total for Child #2 is 15
Running count2 for Child #4 is 4
Running count2 for Child #4 is 5
Sum for Child #2 is 15
Child #2 terminating.
Running total for Child #1 is 1
Running count2 for Child #4 is 6
Running total for Child #1 is 3
Running total for Child #1 is 6
Running count2 for Child #4 is 7
Running total for Child #1 is 10
Running count2 for Child #4 is 8
Running total for Child #1 is 15
Running count2 for Child #4 is 9
Sum for Child #1 is 15
Child #1 terminating.
Running count2 for Child #4 is 10
Running count1 for Child #3 is 0
Running count1 for Child #3 is 1
Running count2 for Child #4 is 11
Running count2 for Child #4 is 12
Running count1 for Child #3 is 2
Running count1 for Child #3 is 3
Running count2 for Child #4 is 13
Running count1 for Child #3 is 4
Running count2 for Child #4 is 14
Running count2 for Child #4 is 15
Running count1 for Child #3 is 5
Running count2 for Child #4 is 16
Running count1 for Child #3 is 6
Running count1 for Child #3 is 7
Running count2 for Child #4 is 17
Running count1 for Child #3 is 8
Running count2 for Child #4 is 18
Running count1 for Child #3 is 9
Running count2 for Child #4 is 19
Running count2 for Child #4 is 20
Running count1 for Child #3 is 10
Child #4 terminating.
Running count1 for Child #3 is 11
Running count1 for Child #3 is 12
Running count1 for Child #3 is 13
Running count1 for Child #3 is 14
Running count1 for Child #3 is 15
Running count1 for Child #3 is 16
Running count1 for Child #3 is 17
Running count1 for Child #3 is 18
Running count1 for Child #3 is 19
Running count1 for Child #3 is 20
Child #3 terminating.

Process finished with exit code 0

count2一来就能执行是因为它不需要去获取sa对象的锁,所以它不需要等别人释放锁。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值