synchronized总结

synchronized是java语言中的关键字,可以用来修饰方法和代码块。被synchronized修饰的方法或是代码块不能同时被多个线程执行,也就是说同一时间只能有一个线程能访问,解决多线程中并发同步的问题。

举个例子,我们去医院看病,首先要挂号,挂号完毕后我们就到对应医生的诊室门口等通知,通知到你了,你就可以进去看病了。这个例子如果我们换一个角度看:首先医生给病人看病,我们可以定义一个方法叫做checkUp(),每一位病人我们可以看做是一个线程,如果每一个线程挂号后就能直接成功的访问checkUp(),那估计医生诊室内部将会是这样:

拥挤

这样的话,显然医生是无法给病人看病的,也就是说checkUp方法是无法正常工作的,而正确的做法就应该是一位病人进去了,就把门关上,下一位病人只能等待上一位病人看完病出来后,才能进去,也就是俗称的“排队”:

排队

那怎么才能让这些病人(线程)乖乖的排队,一次只能有一个病人(线程)进来呢?就是使用synchronized关键字。

闲话不多说,我们先来看第一版代码:

public class Doctor {
    public void checkUp(String name) {
        System.out.println("开始给" + name + "做检查");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("检查完毕,下一位!");
    }
}

public static void main(String[] args) {
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            Doctor doctor = new Doctor();
            doctor.checkUp("张三" + Thread.currentThread().getName());
        }).start();
    }
}

上面的代码中,有一个Doctor类,类中有一个checkUp(String name)方法。在main方法中,通过循环创建了三个线程并启动,每个线程都会去创建Doctor对象并执行checkUp(String name)方法。现在这种写法,就类似于所有病人直接往医生诊室里面挤,显然是不可行的,我们来看下执行结果:

开始给张三Thread-0做检查
开始给张三Thread-1做检查
开始给张三Thread-2做检查
检查完毕,下一位!
检查完毕,下一位!
检查完毕,下一位!

而我们预期的结果是检查完一位患者后,再去检查下一位患者。上面说了可以用synchronized来实现,那好啊,加上不就万事大吉了吗?

public synchronized void checkUp(String name) {
    System.out.println("开始给" + name + "做检查");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("检查完毕,下一位!");
}

注意,经过修改后的checkUp方法,就是多了一个synchronized关键字修饰而已。是不是这样就没问题了呢?我们再运行一次:

开始给张三Thread-0做检查
开始给张三Thread-1做检查
开始给张三Thread-2做检查
检查完毕,下一位!
检查完毕,下一位!
检查完毕,下一位!

玩我呢?之前不是说被synchronized关键字修饰的方法同一时间只能有一个线程访问吗?我就不信,我又换了一种写法:

public void checkUp(String name) {
    synchronized (this) {
        System.out.println("开始给" + name + "做检查");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("检查完毕,下一位!");
    }
}

依然没任何效果啊!如果我们用第二种写法,实际上是给checkUp方法中的代码加上了对象锁。意思就是,你要执行synchronized里面的的代码,必须要先获得这个对象(上面代码中的this指的是当前类对象)的对象锁,一旦获得了,你就可以执行里面的代码,如果有线程比你先一步获得,你就只有等其它线程执行完,释放锁后,才能获得锁并执行其中的代码。注意对象锁是每个实例都存在一把的。而我们用的第一种写法,synchronized修饰在成员方法上,实际上是给checkUp方法加上了方法锁,这个方法锁实际上和对象锁是差不多的,方法锁是锁住当前类对象中的checkUp方法。回头看我们的main方法,我们创建了三个线程,每一个线程中都创建了一个Doctor对象,这三个对象地址值是不同的,难怪synchronized不起作用。修改成如下代码:

public static void main(String[] args) {
    Doctor doctor = new Doctor();
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            doctor.checkUp("张三" + Thread.currentThread().getName());
        }).start();
    }
}

这样,我们的三个线程都是调用的同一个对象的checkUp方法,当Thread-0拿到了doctor对象的锁以后,它就可以开始执行checkUp中的方法,因为三个线程调用的是同一个对象中的checkUp方法,因此在Thread-0没有执行完(释放锁)之前,Thread-1和Thread-2是无法访问checkUp方法的。这样我们的synchronized就起作用了,得到如下输出:

开始给张三Thread-0做检查
检查完毕,下一位!
开始给张三Thread-2做检查
检查完毕,下一位!
开始给张三Thread-1做检查
检查完毕,下一位!

趁热打铁,我们来一道思考题:
请听题,听nm,直接上代码:

public class Doctor {
    public void test() {
        synchronized (this) {
            System.out.println("test");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void test2() {
        System.out.println("test2");
    }
}

public static void main(String[] args) {
    Doctor doctor = new Doctor();
    doctor.test();
    doctor.test2();
}

请问是输出了test后立马输出test2呢还是输出了test后等待两秒再输出test2?如果你回答“test输出后立马输出test2”那你就完了。我们简单分析下,test方法中,用synchronized修饰了代码块,也就是说,你要执行这个代码块立马的内容,你就必须要获得对象锁,哪个对象的对象锁呀?synchronized (this)这不写着呢吗?this呀,当前类对象的锁呀;test2方法中,synchronized修饰了方法,也就是说,你要执行这个方法,就必须要获得当前类对象的锁。到这里,我们发现,testtest2需要的锁都是当前类对象的锁。因此,当我们在main方法中创建了doctor对象,并调用test方法后,就获得了doctor的锁,接着当我们调用doctor对象的test2方法时,因为doctor对象的锁已经被拿走了,只能等test执行完,释放锁以后test2才能获得锁,所以这个问题的答案,相信大家都明白了。

好,继续回到我们最开始的那段代码,什么?你忘记代码内容了?好吧,我把它复制了过来:

public class Doctor {
    public void checkUp(String name) {
        synchronized (this) {
            System.out.println("开始给" + name + "做检查");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("检查完毕,下一位!");
        }
    }
}

public static void main(String[] args) {
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            Doctor doctor = new Doctor();
            doctor.checkUp("张三" + Thread.currentThread().getName());
        }).start();
    }
}

上面的代码,前面已经解释过了,checkUp还是可以被多个线程同时执行,原因就是每个线程都创建了新的Doctor对象,而每个对象都有一把对象锁,因此三个不同的对象调用checkUp方法,都能获取到各自的对象锁从而成功调用方法。那我们就思考了,synchronized (this)这里的this,我不让它需要当前类对象的锁,因为每个对象都会有各自的锁,我让他用Class类的锁不就好了吗?这样,就算你是不同的对象,但是Doctor.class是唯一的。因此,全局也就只有一把锁可用,我们可以把它称之为类锁。我们改下代码:

public void checkUp(String name) {
    synchronized (Doctor.class) {
        System.out.println("开始给" + name + "做检查");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("检查完毕,下一位!");
    }
}

然后我们再运行下:

开始给张三Thread-0做检查
检查完毕,下一位!
开始给张三Thread-2做检查
检查完毕,下一位!
开始给张三Thread-1做检查
检查完毕,下一位!

非常不错,就算你是不同的Doctor对象,也不能为所欲为了。

最后一种情况,如果是静态方法呢?例如我把checkUp方法变为静态的了:

public static synchronized void checkUp(String name) {
    System.out.println("开始给" + name + "做检查");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("检查完毕,下一位!");
}

这样写,其实这个方法默认的锁就是类锁了,因为静态方法已经不是成员方法了,它已经不属于类对象而是属于类了。

好了,关于synchronized的总结就是这样,如果文中有不妥之处,欢迎大家指出,谢谢!



Java技术      多线程 线程同步

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!