多线程:synchronized关键字解析

本文详细解析了Java中的synchronized关键字,包括其作为方法同步和代码块同步的原理和使用。synchronized是JVM层面的锁,通过ACC_SYNCHRONIZED标志实现方法同步,通过monitorenter和monitorexit指令实现代码块同步。文中还探讨了锁的升级机制,从无状态锁到偏向锁、轻量级锁和重量级锁,并通过实例展示了同步方法和同步代码块的区别,以及synchronized的锁重入特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原理

synchronized是JVM层面的锁,是一种重量级的锁。synchronized可以同步方法和代码块。

public class Synchronized {
    public static void main(String[] args) {
    // 对Synchronized Class对象进行加锁
        synchronized (Synchronized.class) {
        }
    // 静态同步方法,对Synchronized Class对象进行加锁
        m();
    }
    public static synchronized void m() {
    }
}

执行javap - v Synchronized

public static void main(java.lang.String[]);
// 方法修饰符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
    Code:
        stack=2, locals=1, args_size=1
        0: ldc #1  // class com/murdock/books/multithread/book/Synchronized
        2: dup
        3: monitorenter  // monitorenter:监视器进入,获取锁
        4: monitorexit   // monitorexit:监视器退出,释放锁
        5: invokestatic  #16 // Method m:()V
        8: return

    public static synchronized void m();
    // 方法修饰符,表示: public static synchronized
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
            stack=0, locals=0, args_size=0
            0: return

方法级别的同步是隐式的,无需通过字节码指令来控制,它依靠的是方法表里的ACC_SYNCHRONIZED标志(什么是方法表和标志?),当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED是否被设置了,如果被设置了执行线程首先需要持有管程才能执行方法,执行后或异常时释放管程。

而代码块级别的同步依靠的是monitorenter和monitorexit指令,这两个指令总是成对执行的,在程序异常时编译器会生成一个异常处理器来执行monitorexit指令。

无论采用哪种方式,都是对一个对象的监视器或叫做管程(Monitor)进行获取,这个过程是排他的,也就是同一时刻只可以有一个线程获取到有synchronized保护对象的监视器。获取不到的线程会阻塞在同步方法或同步块的入口处,进入BLOCKED阻塞状态。这里要区别一下阻塞状态和等待状态,使用Object的wait方法后会进入等待队列,notify后唤醒线程从等待队列移入到阻塞(同步)队列。线程正常结束或者异常释放monitor。

以下是对象,对象的监视器,同步队列以及执行线程的关系

另外,JVM对重量级锁进行了优化,在对象头里存放着锁的类型和偏向线程id。                                 

偏向锁:某个线程用这个锁用的比较频繁,那就把这个线程id存起来,锁类型设为偏向锁。那么下次如果还是他来获取锁的话,不用CAS直接将锁给他。

轻量级锁:多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放。

锁的级别从低到高为:无状态锁,偏向锁,轻量级锁(自旋),重量级锁。锁只可以升级不可以降级。

使用

两个线程操作同一个对象里的实例变量,为什么是实例变量?因为局部变量是没有线程安全问题的。

不安全的代码如下:

public class HasSelfPrivateNum {
     private  int num = 0;
     public void  addi(String username){    (1)
          try{
               if (username.equals("a")){
                    num = 100;
                    System.out.println("a set over!");
                    Thread.sleep(3000);
               }else {
                    num = 200;
                    System.out.println("b set over!");
               }
               System.out.println( username + " num = " + num);
          }catch (InterruptedException e){
               e.printStackTrace();
          }
     }
}
public class ThreadA extends Thread {
     private HasSelfPrivateNum num;
     public ThreadA(HasSelfPrivateNum num){
          this.num = num;
     }
     @Override
     public void run() {
          super.run();
          num.addi("a");
     }
}
public class ThreadB extends Thread{
     private HasSelfPrivateNum num;
     public ThreadB(HasSelfPrivateNum num){
          this.num = num;
     }
     @Override
     public void run() {
          super.run();
          num.addi("b");
     }
}
public class Run {
     public static void main(String[] args) {
          HasSelfPrivateNum num = new HasSelfPrivateNum();
          // HasSelfPrivateNum num1 = new HasSelfPrivateNum();    (2)
          ThreadA threadA = new ThreadA(num);
          threadA.start();
          ThreadB threadB = new ThreadB(num);    (3)
          threadB.start();
     }
}

执行结果:

a set over!
b set over!
b num = 200
a num = 200

执行结果显然发生了线程安全的问题。

接下来:

使用synchronized同步方法,在HasSelfPrivateNum的方法(1)上添加sychronized,即

synchronized public void  addi(String username){...}

此时的执行结果为:

a set over!
a num = 100
b set over!
b num = 200

接下来:

在之前添加synchronized的基础上,我们将之前两个线程访问同一个对象改为每个线程单独访问一个对象,将Run类中的(2)的注释打开,将(3)处传入的对象改为num1。

此时的执行结果为:

a set over!
b set over!
b num = 200
a num = 100

可以看到没有线程安全问题,但是执行结果的顺序是交叉的。

    这是因为关键词synchronized取得的都是对象的锁,所以当两个线程访问同一个对象的时候,这个对象的锁没有释放另一个线程就无法访问,执行结果就会是按照顺序的。但是如果两个线程执行的是同一个类的两个对象,那么就会创建两个锁,两个线程分别执行互不影响。所以执行结果就会是交叉的。

以上代码证明了多个线程可以异步操作多个对象的同一个sychronized方法。

但是,多个线程却不可以操作同一个类的同一个sychronized类型的静态方法,因为同步方法因为可以有多个对象所以会对应多个monitor,而静态方法只会对应一个monitor。多个线程访问时只有一个可以获取monitor。

接下来讨论一下同步方法和同步代码块的区别,以实例的为例。

代码如下:

public class Var {

    synchronized public void methodA(){
        try {
            System.out.println(Thread.currentThread().getName() + " run method A " + System.currentTimeMillis());
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void methodB(){
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + " run method B " + System.currentTimeMillis());
        }
    }

    public void methodC(){
        String syn = "synchtronized";
        synchronized (syn){
            System.out.println(Thread.currentThread().getName() + " run method C " + System.currentTimeMillis());
        }
    }

}
public class Test {
    public static void main(String[] args) {
        Var var = new Var();

        new Thread(new Runnable() {
            @Override
            public void run() {
                var.methodA();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                var.methodB();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                var.methodC();
            }
        }).start();

    }
}

执行结果:

Thread-0 run method A 1520142974301
Thread-2 run method C 1520142974301
Thread-1 run method B 1520142977302

可以看到方法B比其两个方法打印慢3秒,线程Threa-0首先获得对象var的锁,接着线程Thread-0会休眠3秒,这时虽然线程Thread-1先进入线程规划器,但是因为方法methodB内部使用了sychronized代码块,而因为methodC同步的只是方法内部的一个变量所以可以执行。

脏读

发生脏读的代码如下:

public class PublicVar {

    public String username = "A";
    public String password = "AA";
    synchronized public void setValue(String username,String password){
        try {
            this.username = username;
            Thread.sleep(1000);
            this.password = password;
            System.out.println("current thread = " + Thread.currentThread().getName() + " username = " + username
            + " password = " + password);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public void getValue(){
        System.out.println("username = " + username + " password = " + password);
    }

}
public class ThreadA extends Thread {

    private PublicVar publicVar;
    public ThreadA(PublicVar publicVar){
        super();
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        super.run();
        publicVar.setValue("B","BB");
    }
}
public class Test
{
    public static void main(String[] args) {
        try {
            PublicVar publicVar = new PublicVar();
            ThreadA threadA = new ThreadA(publicVar);
            threadA.start();
            Thread.sleep(500); // 打印结果受此值影响,大于线程threadA(即setValue方法)休眠的时间就不会出现脏读
            publicVar.getValue();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

执行结果:

username = B password = AA
current thread = Thread-0 username = B password = BB

如果也将getValue设为sychronized,那么执行结果:

current thread = Thread-0 username = B password = BB
username = B password = BB

此实验可以得到另个结论:

1,A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object的非sychronized;

2,A线程持持有object对象的Lock锁,B线程如果要调用object的sychronized类型方法则需等待,也就是同步。

第一次执行的时候,线程threadA先获得publicVar对象的锁,但是main线程依然可以调用publicVar对象的非sychronized方法getValue,此时username已被更改,password没被该。

第二次执行的时候,线程threadA先获得publicVar对象的锁,但是main线程在threadA没有执行完成setValue方法之前是不可以调用publicVar对象的sychronized方法getValue的,也就是只有threadA释放了锁,将username和password都赋值了,main线程才可以获取publicVar的锁进而调用getValue方法。

为什么会这样呢?之前提到过在调用方法前会检查方法的ACC_SYNCHRONIZED标志是否被标志了,标志的情况下才需要获取锁,如果没有标志即使这个对象的锁没有被当前对象持有依然可以执行。所以实例方法同步的是对象,静态方法同步的是类这个说法不是很全面。

sychronized锁重入

    sychronized关键字拥有锁重入的功能,也就是在一个线程得到一个对象琐时,再次请求此对象锁时是可以得到对象锁的,广义的可重入锁也叫递归锁,是指同一线程外层函数获得锁之后,内层还可以再次获得此锁。这也证明了在sychronized方法内部调用本类的其他sychronized方法时,是可以永远得到锁的。

 

 

参考:《深入理解JVM虚拟机》《Java并发编程的艺术》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值