Java 并发之synchronized关键字深入

·  阅读 930

先从无synchronized 说起

观察如下代码

public class StatefulTest {
    public static void main(String[] args) {
        Runnable stateOfX = () ->{//实现Runnable接口
            /*
            成员变量被多个线程共享 ,如果一个对象有可被修改的成员变量,就称它为有状态的对象,
            反之,如果一个对象没有可被修改的成员变量,就称之为无状态的对象
            */
            int x = 0;
            while(x!=40){
                System.out.println("x: "+x++);
                try {
                    Thread.sleep((long) (Math.random()*100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //通过两个线程同时访问stateOfX
        new Thread(stateOfX).start();
        new Thread(stateOfX).start();
    }
}
复制代码
  • 运行结果

image.png

  • 从结果可知
    • 两个线程会同时得到x = 0这个状态,说明x可被多个线程共享
    • 相同数字只会出现两次
    • 一个线程修改了值,其他线程可以得到被修改后的值

用一用synchronized

代码

  • 创建一个实例对象
public class HelloAndWorld {
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Runnable run1 = test::hello;//引用test的实例方法
        Runnable run2 = test::world;
        new Thread(run1).start();
        Thread.sleep((long) (Math.random() * 200));//线程sleep,run1一定先启动执行
        new Thread(run2).start();
    }

}
class Test{//该类有两个synchronized
    public synchronized void hello()  {
        try {
            Thread.sleep((long) (Math.random()*1000));
            System.out.println("hello");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void world(){
        System.out.println("world");
    }
}
复制代码

image.png

  • 创建两个实例对象
public class HelloAndWorld {
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Test test2 = new Test();//创建两个实列对象
        Runnable run1 = test::hello;
        Runnable run2 = test2::world;//引用第二个实列对象的方法
        new Thread(run1).start();
        Thread.sleep((long) (Math.random() * 200));//线程sleep,run1一定先启动执行
        new Thread(run2).start();
    }
}
class Test{
    public synchronized void hello()  {
        try {
            Thread.sleep((long) (Math.random()*1000));
            System.out.println("hello");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void world(){
        System.out.println("world");
    }
}
复制代码

image.png

以上两个例子想说明

  • 一个线程正在执行一个实列对象的一个synchronized方法,另外一个线程也不能执行没有线程执行的synchronized方法
    • 一个线程执行到synchronized,就获得了对象的锁,其他线程无法执行任何其他synchronized修饰的方法,不管获得当前对象锁的线程是否在执行该方法
  • 当一个线程执行完一个synchronized方法,一定会释放锁让其他线程有机会获得当前实例对象的锁
  • 一个线程正在执行一个实列对象的一个synchronized方法,另外一个线程可以执行另外一个实例对象synchronized方法
    • 不同的实例对象有不同的锁,即一个实例对象有一把锁
    • 值得一提的是synchronized static方法属于Class对象,并不属于实例对象它的锁得另外算

从字节码的角度分析synchronized关键字

例一,修饰一个代码块时

public class MyTest {
    Object object = new Object();
    public void method(){
        synchronized (object){
            System.out.println("hello world");
        }
    }
}
复制代码
  • Javap 查看字节码指令

image.png

  • 小结
  • synchronized关键字修饰代码块时,括号中的实例对象可以任意,前提是存在的对象,无论你括号中是什么对象,其将同步的代码块不会受影响
  • synchronized关键字修饰代码块时,之所以一个monitorenter对应两个monitorexit,是因为一个用来当代码块正常结束让线程释放锁,一个用来当代码块异常结束时让线程释放锁

例二,手动让同步代码块抛异常

  • 通过例一可知一个monitorenter对应两个monitorexit,其中一个monitorexit用来预备当代码块异常结束时也能释放锁
  • 试一试主动让同步代码块抛异常
public class MyTest {
    Object object = new Object();
    public void method2(){
        synchronized (object){
            System.out.println("hello world");
            throw new RuntimeException();//直接抛异常
        }
    }
}
复制代码
  • 字节码

image.png

  • 小结
  • 例一释放锁的方式有异常结束和正常结束两种可能,所以有两个monitorexit
  • 例二释放锁的方式只有一个就是抛出异常(异常结束),来释放锁,所以只有一个monitorexit
  • 总之,系统总会想方设法让线程能释放对象的锁

例三,用synchronized修饰方法

public class MyTest {
    public synchronized void method3(){
        System.out.println("hello world");
    }
}
复制代码
  • 字节码

image.png

  • 小结
  • 对于synchronized关键字修饰方法来说,并没有出现monitorentermonitorexit指令,而是出现了一个ACC_SYNCHRONIZED标志
  • JVM使用了ACC_SYNCHRONIZBD访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC_SYNCHRONIZBD标志
  • 如果有ACC_SYNCHRONIZBD标志,那么执行线程将会先持有方法所在对象的Monitor,然后再去执行方法体,在该方法执行期间,其他任何线程均无法再获取到这个Monitor,当线程执行完该方法后,它会释放掉这个Monitor,换句话说只要方法执行完就会释放锁(Monitor

例四,用synchronized static修饰方法(同步静态方法)

public class MyTest {
    public synchronized static void method4(){
        System.out.println("hello world");
    }
}
复制代码
  • 字节码

image.png

  • 小结
  • 和例三类似,不再赘述
分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改