syncronized关键字(一文秒懂)

462 阅读4分钟

首先说明,因本人水平有限(还在学习啦),目前还看不懂Java对象头中的数据,只明白有这样的一个数据结构,并知道他是干什么的。文章中会用到syncronized对于对象头中的Mark Word的细节就不讲啦,等我学会了单独写一章。

Java对象在运行时结构如下

image.png

指针压缩只在64位机上,Class Point原本应位64位,但是在压缩后只有32位,节省一般的内存空间,具体这里就不细说了。 而对象头中,存放有锁的标志位,记录对象的锁状态。

说了这么多,锁究竟是干什么的?

举个例子,一个寝室通常有5个同学,而每个寝室却一般只有一个厕所。当5个同学,都想使用厕所时(假如厕所一次只能进去一个人),进入厕所的同学第一步会先把厕所锁上,那个其他同学看到厕所锁了,有人使用,就会等待,等使用厕所的同学使用好了,开门,一起上,来抢夺这个厕所,抢到的同学继续锁门,其他同学继续等待,直到所有同学都使用完厕所。

其实,这里的同学就可以理解为线程,而厕所就是多个线程争夺的临界资源,线程争夺到临界资源后,会在临界资源对象的Mark Word上进行标记,其他线程看到临界资源对象的Mark Word上被标记“有人使用”了,就会等待,等锁释放之后,在抢夺临界资源对象,抢夺到的线程再标记,如此往复。

为什么要给对象加锁 java中的多线程其实是并发,即对于多个线程访问同一以资源,可能是在某个时间段里是Thread-1的计算时间,记录状态,某个时间段里是Thread-2的计算时间,记录状态......当然,这个计算时间不可能执行完成一个方法。就像上面说的厕所的例子,总不可能是同学A用一下厕所,刚刚脱下衣服,记录同学A的状态---同学B在用厕所,脱下衣服,记录状态---在有同学A用厕所.....这是不符合日常行为的,但在程序中,若不加锁,这就是真实存在的。

sycronized syncronized是java中的锁关键字,是java内的同步机制。当一个线程获得了锁候,其他线程若想获得这把锁就只能等待或阻塞。在jdk5以前,该关键字是仅有的同步手段。 syncronized关键字可以修饰方法、变量、和代码块。

  • 修饰方法
syncronized void Function(){
    //具体业务方法
}

等同于

void Function(){
syncronized(this){
    //具体业务方法
}
}

即调用对象该方法的线程,去竞争当前对象的锁。

  • 修饰静态方法
class Test{
syncronized static void Func(){
//具体业务方法
}
}

等同于

class Test{
static void Func(){
syncronized(Test.class){
//具体业务方法
}
}
}

即线程去竞争Test类的锁

  • 修饰代码块
void Func(){
//前置方法
syncronized(XXX.class\instance){
    //加锁方法
}
//后置方法
}

运行前置方法、后置方法因为没有上锁,所有线程随时可以执行,但是运行加锁方法,只有在线程竞争到锁后才能执行,没有竞争到的线程只能等待锁释放再一次尝试竞争锁。使用XXX.class线程会竞争XXX.class的锁,使用instance(对象的意思),线程会竞争instance对象的锁。

这里提一个问题:对于多个线程,A、B、C对XXX.class上锁,D对instance上锁,若instalce是有XXX.class new出来的,那么D会影响ABC线程等待或阻塞吗?(这里挖个坑,在例子中解答)。

举个例子

定义资源类型

class Resource{
    void use(MyThread myThread){
        System.out.println("线程"+myThread.getName()+"用了该资源");
    }
}

定义线程

class MyThread implements Runnable{
    private String name;
    private Resource resource;
    MyThread(Resource resource,String name){
        this.resource = resource;
        this.name = name;
    }
    public String getName() {
        return name;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep((int)(Math.random()*1000d));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (resource){
                System.out.println("线程:"+name+"获得了资源,期间独占该资源");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                resource.use(this);
                System.out.println("线程:"+name+"用完了 休息");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程:"+name+"准备释放资源");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

main方法

/**
 * 创建一个临界资源
 * */
Resource resource = new Resource();
for (int i=0;i<5;i++){
    new Thread(new MyThread(resource,"Thread-"+i)).start();
}

大家可以模拟运行一下,可以看到各个线程有序运行。 但是,你直到加锁代码块如果做如下改变,虽然程序不变,其中的细节明白吗?

synchronized (Object.class){

}
synchronized (Resource.class){

}
synchronized (resource){

}

其实,将()变化不影响代码运行的主要原因是,各个线程竞争的是同一把锁,synchronized (Object.class),每个线程都去竞争Object.class的锁。使用synchronized (Resource.class),每个线程都去竞争Resource.class的锁,使用synchronized (resource),每个线程都去竞争resource的锁。

synchronized(XXXX){}代码块加锁中XXX的改变,只会影响到竞争XXX的锁的线程, 假如在main方法中添加如下代码

new Thread(()->{
    while (true){
        try {
            Thread.sleep(1001);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (resource){
            System.out.println("大家休息5秒");
            for (int i=1;i<6;i++){
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}).start();

其他争夺resource的资源真的停了5秒,即大家休息5秒生效了,因为线程竞争的是同一把锁,即resource对象的锁。 倘若把这段代码修改为

new Thread(()->{
    while (true){
        try {
            Thread.sleep(1001);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (Object.class){
            System.out.println("大家休息5秒");
            for (int i=1;i<6;i++){
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}).start();

不难发现,其实大家休息五秒的机制就失效了,因为休息的代码精致的是Object.class的锁,而其他线程竞争的是resources对象的锁,就好比914,915 2个寝室,914的同学洗澡肯定是竞争914的厕所的锁,而915的同学厕所锁不锁,和914的同学并没有关机,及时915的人大喊,测试不能用了.914的同学照样好好地在914用自己的厕所。

补坑: 如若将刚才添加的暂停代码快加锁修改为Resources会怎么样呢?

new Thread(()->{
            while (true){
                try {
                    Thread.sleep(1001);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (Resource.class){
                    System.out.println("大家休息5秒");
                    for (int i=1;i<6;i++){
                        System.out.println(i);
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

        }).start();

resource是由Resource new出来的,最开始我也以为暂停方法对其他线程依然有效,其实不然,这里需要注意,我理解为Resource.class的锁和其对象的锁是分开的,互不影响。具体实现原理等我看完了知识点后会单独发一章(这里挖坑)。