Synchronized锁的区别和原理

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

1. Synchronized使用

保证在同一时刻只有一个线程可以执行某个方法或者代码块(用于操作共享变量),属于互斥锁。synchronized可以用来修饰方法,代码块。分为以下三种:

  1. 修饰实例方法。在当前实例对象上加锁,线程进入当前方法前需要获得实例对象的锁,称之为对象锁。
  2. 修饰静态方法。在前类的class对象上加锁,线程进入静态方法前需要获得类对象的锁,称之为类锁。
  3. 修饰代码块。在实例对象上加锁,称之为对象锁。在类的class对象上加锁,称之为类锁,保证同一时刻只有一个线程获得锁。

1. 修饰实例方法

synchronized修饰的实例方法,指的是非静态方法,某一个线程获得锁时,获取的是对象锁。

public class SyncTask implements Runnable {

    static int count = 0;

    @Override
    public void run() {

        increase();
    }

    private synchronized void increase() {

        for (int i = 0; i < 1000000; i++) {
            count++;
        }
    }
}

//执行类
SyncTask syncTask =new SyncTask();

        Thread thread1=new Thread(syncTask,"thread1");
        Thread thread2=new Thread(syncTask,"thread2");

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("    count  =   " + SyncTask.count);

//结果为
count  =   2000000
复制代码

可以得出结论多个线程在操作同一个对象的方法时,synchronized修饰的实例方法在同一时刻只有一个线程可以获得锁,当前线程执行完毕释放锁后,其他线程才可以竞争锁并执行。

思考一下:当多个线程操纵的是不同的对象的synchronized修饰的方法时,会怎么样?

public static void main(String[] args){
        
        Thread thread1=new Thread(new SyncTask(),"thread1");
        Thread thread2=new Thread(new SyncTask(),"thread2");

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("    count  =   " + SyncTask.count);

    }

//执行结果
count  =   1197724
复制代码

上述代码改变在于不同的线程操纵不同的对象。需要注意到的是SyncTask的synchronized修饰的方法内部操作的是静态变量count。可以看到结果并不是我们想象的2000000。因此可以得出结论:不同线程在操作不同对象的synchronized修饰的方法时互不影响。作用的是当前各自的对象,因此不存在竞争锁的情况,导致了静态变量count的值与预想的有较大出入。相当于不同线程访问同一对象的不带synchronized修饰的increase方法。

2. 修饰静态方法

synchronized修饰的静态方法,获取的是当前的类的class对象锁,也叫类锁。static修饰的成员变量和成员方法属于类独有,因此通过class对象可以保证static方法的并发操作。需要注意的是当线程A操作一个实例对象的非static synchronized修饰的方法和线程B操作类对象的static synchronize方法时是不冲突的。不会发生互斥。因为线程A操作的是对象锁,而线程B操作的是类锁。

public class SyncTask implements Runnable {

    static int count = 0;


    @Override
    public void run() {

        increase();
    }


    private synchronized  static void increase() {

        for (int i = 0; i < 1000000; i++) {
            count++;
        }

    }

}

//执行函数
public static void main(String[] args){

        SyncTask syncTask = new SyncTask();

        Thread thread1=new Thread(new SyncTask(),"thread1");
        Thread thread2=new Thread(new SyncTask(),"thread2");

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("    count  =   " + SyncTask.count);

    }
    
//执行结果
count  =   2000000
复制代码

synchronized修饰的是静态的increase方法,获得的是类锁,和实例方法不同,实例方法获取的是对象锁。无论是操作不同的对象还是同一个对象,如果操作的是类锁,那么同一时刻只有一个线程获得锁。

3. 修饰代码块

如果某一个方法中方法体较多,只有一些部分需要同步执行,此时就可以采用同步代码快的方法来实现部分代码块同步。同步代码块可以采用对象锁,锁住当前对象,或者采用类锁,锁住整个类。

public class SyncTask implements Runnable {

    static int count = 0;

    static  SyncTask syncTask = new SyncTask();


    @Override
    public void run() {

        increase();
    }


    private   void increase() {

        synchronized(syncTask){
            for (int i = 0; i < 1000000; i++) {
                count++;
            }
        }
    }
}

//执行函数
public static void main(String[] args){

        SyncTask syncTask = new SyncTask();

        Thread thread1=new Thread(new SyncTask(),"thread1");
        Thread thread2=new Thread(new SyncTask(),"thread2");

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("    count  =   " + SyncTask.count);

    }
    
//执行结果
 count  =   2000000
复制代码

在increase中synchronized锁住的是syncTask对象,每一个进入increase方法的线程都必须持有syncTask对象锁才可以继续执行,否则只能等待。synchronized中也可以使用this表明锁住的是当前对象,但是我们要看到执行函数中每个线程操作的是不同的对象,因此此时的执行结果就不再是预期结果了。此时我们可以采用类的class对象(类锁)的方式来保证同一时刻只有一个线程获得锁。若是在static
方法中,因为static方法中只能使用静态成员变量和类的class对象,所以可以创建一个静态成员变量或者直接采用类的class对象来保证同步。

4. 类锁和对象锁的使用区别

  1. 多个线程操作同一对象时,synchronized可以锁定一个对象、this关键字或者类的class对象都可以;也就是对象锁,类锁都有互斥作用。
  2. 多个线程操作不同对象时,synchronized可以锁定一个静态对象或者采用类的class对象。this关键字由于作用的是当前线程操作的实例对象,所以不存在互斥锁作用。
  3. 多个线程无论是操作同一对象还是多个对象,都可以采用锁定一个静态对象或者类的class对象。例如上面的syncTask对象属于对象锁。而类的class对象属于类锁,都可以产生作用。

2. Synchronized原理

synchronized的实现是基于Monitor来实现的。synchronized在修饰同步方法和同步代码块时原理不同。

2.1 Synchronized修饰代码块原理

在SyncTask的run方法中创建了一个同步代码块

public class SyncTask implements Runnable {

    static int count = 0;

    static  SyncTask syncTask = new SyncTask();


    @Override
    public void run() {
        synchronized(syncTask){
            for (int i = 0; i < 1000000; i++) {
                count++;
            }
        }
    }
}

复制代码

使用javap -verbose class反编译class文件可以获取字节码,这里只截取了run方法的字节码:

 public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: getstatic     #2                  // Field syncTask:Lcom/mdy/lib/SyncTask;
         3: dup
         4: astore_1
         5: monitorenter       //此处进入同步方法
         6: iconst_0
         7: istore_2
         8: iload_2
         9: ldc           #3                  // int 1000000
        11: if_icmpge     28
        14: getstatic     #4                  // Field count:I
        17: iconst_1
        18: iadd
        19: putstatic     #4                  // Field count:I
        22: iinc          2, 1
        25: goto          8
        28: aload_1
        29: monitorexit      //此处退出同步方法
        30: goto          38
        33: astore_3
        34: aload_1
        35: monitorexit     //此处退出同步方法
        36: aload_3
        37: athrow
        38: return

复制代码

可以看到在同步代码块中使用monitorentermonitorexit来实现。monitorenter指向同步代码块的起始位置,monitorexit指向同步代码块的结束位置。执行到monitorenter时,当前线程会尝试获得objectref(对象锁)对应的的monitor持有权,当objectref的monitor的进入计数器值为0时,当前线程获得monitor,计数器值+1,当前线程获得锁。如果当前线程已经持有monitor,则会重入这个monitor,计数器值+1。倘若其他线程持有objectref的monitor,则当前线程进入阻塞状态。直到其他线程执行完毕,即monitorexit指令被执行,进入计数器值为0,此时其他线程将有机会持有锁。

wait的理解

ObjectMonitor() {
    ...
   _WaitSet      = NULL; //处于wait状态的线程集合
   _owner // 拿到锁的线程
   _EntryList    = NULL ; //处于等待锁block状态的线程集合
   ...
 }
复制代码
  1. 当一个线程申请锁时,就会进入_EntryList集合等待,参与锁竞争。
  2. 当线程获得锁时,_owner就会标记获得锁的线程
  3. 获得锁的线程在调用wait方法时,会释放锁,进入_WaitSet中,等待被唤醒。
  4. 当_WaitSet中线程被唤醒时,重新进入_EntryList集合中,参与竞争锁。

2.2 Synchronized修饰方法原理

SyncTask使用synchronized来修饰同步方法

public class SyncTask implements Runnable {

    static int count = 0;

    @Override
    public void run() {
        inCrease();
    }

    public synchronized  void inCrease(){
        for (int i = 0; i < 1000000; i++) {
            count++;
        }
    }
}
复制代码

使用javap -verbose class反编译class文件可以获取字节码,这里只截取了inCrease方法的字节码:

 public synchronized void inCrease();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: ldc           #3                  // int 1000000
         5: if_icmpge     22
         8: getstatic     #4                  // Field count:I
        11: iconst_1
        12: iadd
        13: putstatic     #4                  // Field count:I
        16: iinc          1, 1
        19: goto          2
        22: return

复制代码

反编译后的字节码中,被synchronized修饰的同步方法内部并没有monitorenter和monitorexit指令,使用ACC_SYNCHRONIZED来标识该方法是不是一个同步方法。

2.3 Synchronized的优化

在Java6 之前synchronized属于重量级锁,在Java6之后为了减少获得锁和释放锁带来的性能消耗,又加入了偏向锁、轻量级锁。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享