转载

java并发synchronized的原理和应用

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 代码块");
        }
    }
}

java并发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!");
    }
}

java并发synchronized的原理和应用

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");
        }
    }
}

java并发synchronized的原理和应用

两个线程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();
}

java并发synchronized的原理和应用

多个线程访问的是同一个对象,哪个线程先执行带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");
        }
    }
}

java并发synchronized的原理和应用

原文  https://segmentfault.com/a/1190000020534181
正文到此结束
Loading...