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
修饰了方法,也就是说,你要执行这个方法,就必须要获得当前类对象的锁。到这里,我们发现,test
和test2
需要的锁都是当前类对象的锁。因此,当我们在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
的总结就是这样,如果文中有不妥之处,欢迎大家指出,谢谢!
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!