java并发编程-Synchronized关键字

以下是本文的目录大纲:

 一.什么是线程安全,解决线程安全问题的方法及同步思路、实现线程安全的前提,?

 二.synchronized详细分析

 什么是线程安全:

    当多线程访问某个类时,这个类始终都变现出正确的行为,那么这个类就是线程安全的,线程安全最核心的概念就是正确性。

 线程导致的安全问题:  

     当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

  解决线程问题:

    在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。本文主要讲述synchronized的使用方法,Lock的使用方法在下一篇博文中讲述。

  实现同步的前提条件:

    1,必须要有两个或者两个以上的线程。

    2,必须是多个线程使用同一个锁。

  实现同步的思路:

     1,明确哪些代码是多线程运行代码。

     2,明确共享数据。

     3,明确多线程运行代码中哪些语句是操作共享数据的。

   synchronized详细分析

      在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,顾名思义:能到达到互斥访问目的的锁。

    举个简单的例子:如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。

    在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

    在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。  

      Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

      Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。从语法上讲,Synchronized总共有三种用法:

     1.修饰同步代码块:

        Synchronized( 对象 ){

             Xxoo;

       }

    2.修饰同步方法:

        Public synchronized void add(int num){};

    3.修饰静态方法:

       Public synchronized static void add(int num){};

    下面通过几个简单的例子来说明synchronized关键字的使用:

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

  执行结果如下,线程1和线程2同时进入执行状态,线程2执行速度比线程1快,所以线程2先执行完成,这个过程中线程1和线程2是同时执行的。

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

1、修饰普通方法:

public synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

  执行结果如下,跟代码段一比较,可以很明显的看出,线程2需要等待线程1的method1执行完成才能开始执行method2方法。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

2.修饰静态方法:

public static synchronized void method1(){
         System.out.println("Method 1 start");
         try {
             System.out.println("Method 1 execute");
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }
 
     public static synchronized void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }
 
     public static void main(String[] args) {
         final Test test = new Test ();
         final Test test2 = new Test ();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test2.method2();
             }
         }).start();
     }

   执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使test和test2属于不同的对象,但是它们都属于Test类的实例,所以也只能顺序的执行method1和method2,不能并发执行。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

2.修饰代码块:

public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }
public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }

 执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,需要等待线程1中同步块执行完成。

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

上述修饰方式的区别:

    由于java的每个对象都有一个内置锁,当用此关键字修饰同步方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。所以同步方法使用的锁的对象是this,当前对象引用。

   同步代码块,使用的锁可以是任意对象,但是操作共享资源时,锁必须唯一!

   synchronized修饰静态方法,静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class

Synchronized 原理

 我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:

public class ThredDemo2 {
 public void method() {
  synchronized (this) {
  System.out.println("Method 1 start");
 }
 }
}

javac ThredDemo2.java

javap -c ThredDemo2

  blob.png

  从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。

    monitorenter :

    每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 

     1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

     2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

     3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

  monitorexit: 

    执行monitorexit的线程必须是objectref所对应的monitor的所有者。

    指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 

   通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

 我们再来看一下同步方法的反编译结果:

public synchronized void method() {
  System.out.println("Method 1 start");
  }
}

反编译结果:

    blob.png

     从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

运行结果解释

  有了对Synchronized原理的认识,再来看上面的程序就可以迎刃而解了。

   1、代码段2结果:

  虽然method1和method2是不同的方法,但是这两个方法都进行了同步,并且是通过同一个对象去调用的,所以调用之前都需要先去竞争同一个对象上的锁(monitor),也就只能互斥的获取到锁,因此,method1和method2只能顺序的执行。

   2、代码段3结果:

  虽然test和test2属于不同对象,但是test和test2属于同一个类的不同实例,由于method1和method2都属于静态同步方法,所以调用的时候需要获取同一个类上monitor(每个类只对应一个class对象),所以也只能顺序的执行。

  3、代码段4结果:

  对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1和method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。

  Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,但是加入Synchronized将导致程序的性能下降,又因为Synchronized是基于Mutex Lock来实现的重量级锁,所有加大了线程之间切换的时间。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。接下来我们会详细分析怎么提高效率,优化锁。

发表评论