java高级特性 - 多线程基础(3)线程同步_java多线程开发之旅-基础3-Synchronized的用法

Synchronized实现同步的基础

在JAVA中,synchronized的使用具体表现为三种形式:

  1. 修饰普通的实例方法,这时候的锁是当前实例对象;
  2. 修饰静态方法,这时候的锁是当前类的Class对象;
  3. 修饰方法块,这时候锁是synchronized参数传递的对象;

Synchronized实现同步的原理(简单描述)

Synchronized实现原理涉及到底层计算机硬件的支持,这里从JVM层面了解一下Synchrinized实现原理,因此我们看一下JVM规范中是怎么说的:

Java虚拟机中的同步Synchronized是用monitor的进入和退出实现的,无论是显式同步(有明确的monitorenter指令和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现)。

但是使用synchronized来同步方法和同步代码块的实现是不一样的,同步代码块使用的是monitorenter和monitorexit指令。而方法同步不是使用monitorenter和monitorexit指令,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的。

JVM要保证每个monitorenter指令都要有与之对应的monitorexit指令,不管方法是否正常结束,因此编译器会自动生成一个异常处理器用于处理所有的异常,并在其中执行monitorexit指令。

任何对象都有一个monitor与之对应,当一个monitor被持有后,对象即处于锁定状态,其他线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也即尝试获得对象的锁。

synchronized同步方法的特点

  1. synchronized取得的锁都是对象锁;
  2. A线程持有对象的锁时,B线程可以异步调用同一个对象的非synchronized类型的方法;
  3. synchronized拥有锁重入的功能,A线程在获得对象锁后,可以再次请求该对象的锁,也就是说在synchronized方法/块内部请求同一个对象的其他synchronized方法/块时,永远可以得到锁。
  4. 在父子继承的环境中,synchronized同样具备锁的可重入性:在子类的synchronized方法中,可以调用父类的synchronized方法。
a026527515f2b7081b80c65fc6cb96d3.png

synchronized使用示例

使用synchronized修饰访问对象实例变量的方法,确保线程安全(synchronized基本用法):

package com.demo;public class TestSync {    private int num = 0;    synchronized public void setNumber(String threadName){        try {            if(threadName.equals("A")){                num = 100;                System.out.println("Thread A set number complete!");                Thread.sleep(1000);            }else{                num = 200;                System.out.println("Other thread set number complete!");            }            System.out.println("Thread name: " + Thread.currentThread().getName() + ", num = " + num);        }catch (InterruptedException e){            e.printStackTrace();        }    }    public static void main(String[] args) {        TestSync testSync = new TestSync();        Thread threadA = new Thread(new Runnable() {            @Override            public void run() {                testSync.setNumber("A");            }        });        threadA.setName("A");        threadA.start();        Thread threadB = new Thread(new Runnable() {            @Override            public void run() {                testSync.setNumber("B");            }        });        threadB.setName("B");        threadB.start();    }}

上述代码中的setNumber方法会修改实例中的num变量,如果是线程A调用的该方法,则将num值置为100,过1秒后再输出设置的num值。如果不是A线程执行该方法,则置num为200.接下来的代码中开启了两个线程,名字分别设置为A和B,运行结果如下:

Thread A set number complete!

Thread name: A, num = 100

Other thread set number complete!

Thread name: B, num = 200

Process finished with exit code 0

上面的代码,setNumber方法如果省去了synchronized关键字,变为一个非同步方法,就会因为两个线程前后修改实例变量导致A线程修改的值100被覆盖,输出的结果如下:

Thread A set number complete!

Other thread set number complete!

Thread name: B, num = 200

Thread name: A, num = 200

从打印的顺序也可以看出来,非同步方法,多个线程可以任意进入方法中,线程间是交叉乱序执行的。


接下来验证“A线程持有对象的锁时,B线程可以异步调用同一个对象的非synchronized类型的方法;”这个结论。将上面的代码稍作修改,增加一个非同步的方法notSynMethod,该方法将num改为666然后打印当前线程名字和修改后的num值。

package com.demo;public class TestSync {    private int num = 0;    synchronized public void setNumber(String threadName){        try {            if(threadName.equals("A")){                num = 100;                System.out.println("Thread A set number complete!");                Thread.sleep(1000);            }else{                num = 200;                System.out.println("Other thread set number complete!");            }            System.out.println("Thread name: " + Thread.currentThread().getName() + ", num = " + num);        }catch (InterruptedException e){            e.printStackTrace();        }    }    public void notSynMethod(){        num = 666;        System.out.println("Thread name: " + Thread.currentThread().getName() + " has set num to " + num);    }    public static void main(String[] args) {        TestSync testSync = new TestSync();        Thread threadA = new Thread(new Runnable() {            @Override            public void run() {                testSync.setNumber("A");            }        });        threadA.setName("A");        threadA.start();        Thread threadB = new Thread(new Runnable() {            @Override            public void run() {                testSync.notSynMethod();            }        });        threadB.setName("B");        threadB.start();    }}

输出结果:

Thread A set number complete!

Thread name: B has set num to 666

Thread name: A, num = 666

Process finished with exit code 0

从输出结果可以看出,线程A在将num设置为100后开始sleep1秒钟(并未释放testSync对象锁),这期间B线程启动并修改num值为666,结果A线程sleep结束并打印出来num的值是修改后的666。这说明“A线程持有testSync对象的锁时,B线程可以异步调用同一个对象的非synchronized类型的notSynMethod方法;”


最后验证synchronized对象锁的可重入性。再次回忆一下,“可重入”指的是线程在得到对象锁之后,再次请求该对象锁是可以再次得到该对象的锁的。因此测试代码构造两个同步方法,在第一个同步方法synMethod1中调用第二个同步方法synMethod2,如果能成功调用,说明对象锁是可重入的。

package com.demo;public class TestSync {    synchronized void synMethod1() throws Exception{        System.out.println("in synMethod1...");        this.synMethod2();        Thread.sleep(500);        System.out.println("synMethod1 completed!");    }    synchronized void synMethod2(){        System.out.println("in synMethod2...");    }    public static void main(String[] args) {        TestSync testSync = new TestSync();        Thread thread = new Thread(new Runnable() {            @Override            public void run() {                try {                    testSync.synMethod1();                } catch (Exception e) {                    e.printStackTrace();                }            }        });        thread.start();    }}

程序输出:

in synMethod1...

in synMethod2...

synMethod1 completed!

Process finished with exit code 0

分清线程执行不同的synchronized同步方法时所持有的锁

synchronized同步方法是对当前对象加锁,而synchronized代码块是对某个对象加锁,这个对象称为“对象监视器”。在使用synchronized关键字时,时刻都要分辨清楚被同步的方法/代码块对应的“对象监视器”是什么,如果使用了错误的对象监视器,会得到与预期不一致的结果。

在文章开始就说过了synchronized对于修饰实例方法、静态方法、代码块时使用的是不同的锁,前面的例子中已经验证了,线程进入synchronized修饰的实例方法时,得到的是当前对象锁。接下来验证对于静态方法,对应的锁是当前Java Class类。

package com.demo;public class TestSync {    synchronized static void staticMethodA(){        System.out.println(Thread.currentThread().getName() + " enter staticMethodA()...");        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(Thread.currentThread().getName() + " exit staticMethodA()...");    }    synchronized static void staticMethodB(){        System.out.println(Thread.currentThread().getName() + " in staticMethodB()...");    }    synchronized void synMethod(){        System.out.println(Thread.currentThread().getName() + " in synMethod()");    }    public static void main(String[] args) {        TestSync testSync = new TestSync();        Thread threadA = new Thread(new Runnable() {            @Override            public void run(){                TestSync.staticMethodA();            }        });        threadA.setName("A");        Thread threadB = new Thread(new Runnable() {            @Override            public void run() {                TestSync.staticMethodB();            }        });        threadB.setName("B");        Thread threadC = new Thread(new Runnable() {            @Override            public void run() {                testSync.synMethod();            }        });        threadC.setName("C");        threadA.start();        threadB.start();        threadC.start();    }}

输出结果:

A enter staticMethodA()...

C in synMethod()

A exit staticMethodA()...

B in staticMethodB()...

Process finished with exit code 0

上面的代码中,TestSync类有两个静态方法,一个实例方法,当开启三个线程分别执行三个方法时,可以看到线程A进入了静态方法staticMethodA()尚未退出之前,线程B是无法进入静态方法staticMethodB()的,但是线程C可以执行synchronized方法synMethod()。这说明线程A所持有的锁和线程B请求的锁是同一个,都是当前类。线程C可以无阻碍的进入方法synMethod(),说明线程C请求的是testSync对象的锁,它没有被其他线程持有,因此可以进入该方法。

总结成一句话就是:“多个线程在访问某个方法前,如果请求的是同一把锁,那么这些线程必须排队等待(同步执行),否则线程之间就是异步执行”。

String类型的常量池特性

如果使用String类型的对象作为对象监视器时要注意,下面这样的代码,变量a和b引用的是同一个对象:

String a = "111";String b = "111";

如果将这两个对象作为监视器同步代码块时,A线程获取并持有a对象锁时,B线程是无法b对象的锁的,因为a和b指向的是同一个对象。下面举例验证一下,顺便演示synchronized代码块的语法:

package com.demo;public class TestSync {    private String obj;    TestSync(String lockObject){        this.obj = lockObject;    }    public void synBlock(){        synchronized (obj){            System.out.println(Thread.currentThread().getName() + " Holding lock: "+ obj +" enter synBlock()...");            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println(Thread.currentThread().getName() + " Release lock: "+ obj +" leaving synBlock()...");        }    }    public static void main(String[] args) {        String a = "111";        String b = "111";        TestSync testSyncA = new TestSync(a);        TestSync testSyncB = new TestSync(b);        Thread threadA = new Thread(new Runnable() {            @Override            public void run() {                testSyncA.synBlock();            }        });        threadA.setName("Thread A");        Thread threadB = new Thread(new Runnable() {            @Override            public void run() {                testSyncB.synBlock();            }        });        threadB.setName("Thread B");        threadA.start();        threadB.start();    }}

执行结果:

Thread A Holding lock: 111 enter synBlock()...

Thread A Release lock: 111 leaving synBlock()...

Thread B Holding lock: 111 enter synBlock()...

Thread B Release lock: 111 leaving synBlock()...

Process finished with exit code 0

由于a和b引用的是同一个字符串对象,导致原本不希望同步的两个线程被同步执行了。

关于synchronzed关键字的用法,暂时就介绍这么多了,其他用法(例如内部类中使用synchronized方法)大同小异,可以自行分析。

这里是NPE,共同学习,向JAVA架构师迈进,欢迎关注、评论和转发,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值