java并发synchronized的原理和应用

5次阅读

共计 4496 个字符,预计需要花费 12 分钟才能阅读完成。

java 并发编程这个领域中 synchronized 关键字一直都是元老级的角色,在 java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在 JDK1.6 之后 java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

synchronized 实现原理

通过反编译下面的代码来看看 Synchronized 是如何实现对代码块进行同步的,切换到类的对应目录执行 javac SynchronizedTest.java 命令生成编译后的.class 文件,然后执行 javap -v SynchronizedTest.class。

1)synchronized 同步代码块

public class SynchronizedTest {public static void main(String[] args) {new SynchronizedTest().method();}

    public void method() {synchronized (this) {System.out.println("synchronized 代码块");
        }
    }
}

从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因) 的持有权. 当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized 的语义底层是通过一个 monitor 的对象来完成,其实 wait/notify 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。

2)synchronized 修饰方法

public class SynchronizedTest {public static void main(String[] args) {new SynchronizedTest().method();}

    public synchronized void method() {System.out.println("Hello World!");
    }
}

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

小结:对于同步块的实现使用了 monitorenter 和 monitorexit 指令,而同步方法则是依靠方法修饰符上的 ACC_SYNCHRONIZED 来完成的。无论采用哪种方法,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一个时刻只能有一个线程获取到由 synchronized 所保护对象的监视器。

synchronized 的应用

synchronized 实现同步的基础是:Java 中每个对象都可以作为锁,具体表现为以下三种形式:
1. 对于普通同步方法,锁是当前实例对象;
2. 对于静态同步方法,锁是当前类的 class 对象;
3. 对于同步方法块,锁是 synchronized 括号里配置的对象

1)多个线程访问的是多个对象

public class HasSelfPrivateNum {

    private int num = 0;

    public static void main(String[] args) {HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
        HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();

        ThreadTestA thread1 = new ThreadTestA(numRef1);
        thread1.start();

        ThreadTestB thread2 = new ThreadTestB(numRef2);
        thread2.start();}

    synchronized public void add(String username) {
        try {if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");

                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + "num=" + num);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    static public class ThreadTestA extends Thread {
        private HasSelfPrivateNum numRef;

        public ThreadTestA(HasSelfPrivateNum numRef) {this.numRef = numRef;}

        @Override
        public void run() {numRef.add("a");
        }
    }

    static public class ThreadTestB extends Thread {
        private HasSelfPrivateNum numRef;

        public ThreadTestB(HasSelfPrivateNum numRef) {this.numRef = numRef;}

        @Override
        public void run() {numRef.add("b");
        }
    }
}


两个线程 ThreadTestA 和 ThreadTestB 分别访问同一个类的不同实例的相同名称的同步方法,但是效果确实异步执行,因为 synchronized 取得的锁都是对象锁,而不是把一段代码或方法当做锁。所以在上面的实例中,哪个线程先执行带 synchronized 关键字的方法,则哪个线程就持有该方法所属对象的锁 Lock,那么其他线程只能呈等待状态。当前创建了两个 HasSelfPrivateNum 类对象,所以就产生了两个锁。当 ThreadTestA 的引用执行到 add 方法 run 中的 Thread.sleep(2000)语句时,ThreadB 就会“乘机执行”。

2)多个线程访问的是同一个对象

public static void main(String[] args) {HasSelfPrivateNum numRef = new HasSelfPrivateNum();

    ThreadTestA thread1 = new ThreadTestA(numRef);
    thread1.start();

    ThreadTestB thread2 = new ThreadTestB(numRef);
    thread2.start();}


多个线程访问的是同一个对象,哪个线程先执行带 synchronized 关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。

3)脏读
发生脏读的情况实在读取实例变量时,此值已经被其他线程更改过。

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(3000);
            this.password = password;

            System.out.println("setValue method thread name="
                    + Thread.currentThread().getName() + "username="
                    + username + "password=" + password);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    // 该方法前加上 synchronized 关键字就同步了
    public void getValue() {
        System.out.println("getValue method thread name="
                + Thread.currentThread().getName() + "username=" + username
                + "password=" + password);
    }

    public static void main(String[] args) {
        try {PublicVar publicVarRef = new PublicVar();
            ThreadC thread = new ThreadC(publicVarRef);
            thread.start();

            Thread.sleep(500);// 打印结果受此值大小影响

            publicVarRef.getValue();} catch (InterruptedException e) {e.printStackTrace();
        }

    }

    static class ThreadC extends Thread {

        private PublicVar publicVar;

        public ThreadC(PublicVar publicVar) {this.publicVar = publicVar;}

        @Override
        public void run() {publicVar.setValue("B", "BB");
        }
    }
}

正文完
 0