熟练掌握多线程编程是程序猿的基本技能之一,很多朋友在平时的工作中,也许用惯了开源库,虽然知道自己写的代码是支持多线程的,却不懂多线程实现的原理。作者差不多也是这种状态,每次遇到问题才去翻资料。今天恰巧又想到了多线程的一个问题,所以得空自己写个demo证实下。
假如我们有一个工具类Utils,包含两个同步方法,如下:
public synchronized void makeCall() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("makeCall");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void sendMail() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("sendMail");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在main函数开启两个线程分别调用上面两个方法
public static void main(String[] agrs) {
new Thread(new Runnable() {
@Override
public void run() {
Utils utils = new Utils();
utils.makeCall();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Utils utils = new Utils();
utils.sendMail();
}
}).start();
}
结果会是怎样的呢,会存在互斥的问题吗?看下面的结果
makeCall
sendMail
.......
makeCall
sendMail
Process finished with exit code 0
从结果来看,并不存在互斥的情况,有些同学有疑问了,Utils的两个方法不是都加锁了吗,为什么没有同步呢?
我们把调用方法的函数改一改,如下:
public static void main(String[] agrs) {
final Utils utils = new Utils();
new Thread(new Runnable() {
@Override
public void run() {
utils.makeCall();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
utils.sendMail();
}
}).start();
}
看下打印结果:
makeCall
makeCall
makeCall
makeCall
makeCall
sendMail
sendMail
sendMail
sendMail
sendMail
从结果来看,本次调用出现了互斥现象,原因很简单,第一个实验,两个方法都加锁了是没毛病的,问题在于Utils的每个方法的锁是当前对象,对于两个不同的对象,相当于两把不同的锁,当然不存在互斥了。实验二恰好使用的是同一个对象,也就是同一把锁,也就存在互斥行为了。
继续实验三
// 将makeCall改成static的
public static synchronized void makeCall() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("makeCall");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] agrs) {
final Utils utils = new Utils();
new Thread(new Runnable() {
@Override
public void run() {
// 直接调用Utils的静态方法
Utils.makeCall();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
utils.sendMail();
}
}).start();
}
打印结果如下:
makeCall
sendMail
........
makeCall
sendMail
本次打印结果也不存在互斥现象,在改下代码
public class Utils {
//静态同步方法
public static synchronized void makeCall() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("makeCall");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//静态同步方法
public static synchronized void sendMail() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("sendMail");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] agrs) {
new Thread(new Runnable() {
@Override
public void run() {
Utils.makeCall();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Utils.sendMail();
}
}).start();
}
打印结果如下:
makeCall
makeCall
makeCall
makeCall
makeCall
sendMail
sendMail
sendMail
sendMail
sendMail
从输出来看,出现了互斥现象。实验三将makeCall改成静态同步方法,这把锁是类锁,而sendMail的锁依然是一个对象。在实验四,将两个方法都改成静态同步方法,使用的就是同一把锁了。
上面的几个实验都是为了引出两个概念,类锁/对象锁。
对象锁:JVM 在创建对象的时候,默认会给每个对象一把唯一的对象锁,一把钥匙
类锁:每一个类都是一个对象,每个对象都拥有一个对象锁。
总结:
1.对象锁钥匙只能有一把才能互斥,才能保证共享变量的唯一性
2.在静态方法上的锁,和实例方法上的锁,默认不是同样的,如果同步需要制定两把锁一样。
3.关于同一个类的方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同,否则就不相同。比如 new A().x() 和 new A().x(),对象不同,锁不同,如果A的单利的,就能互斥。
4.静态方法加锁,能和所有其他静态方法加锁的进行互斥
5.静态方法加锁,和xxx.class 锁效果一样,直接属于类的
延伸一下:既然有了synchronized修饰方法的同步方式,为什么还需要synchronized修饰同步代码块的方式呢?
当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面。
例如在某个类的方法里面:这个类里面声明了一个对象实例,
Object obj=new Object ();
在某个方法里面调用了这个实例的方法obj.program();但是调用这个方法需要进行同步,不能同时有多个线程同时执行调用这个方法。这时如果直接用synchronized修饰调用了obj.program();代码的方法,那么当某个线程进入了这个方法之后,这个对象其他同步方法都不能给其他线程访问了。假如这个方法需要执行的时间很长,那么其他线程会一直阻塞,影响到系统的性能。如果这时用synchronized来修饰代码块:
synchronized(obj){
obj.program();
}
那么这个方法加锁的对象是obj这个对象,跟执行这行代码的对象没有关系,当一个线程执行这个方法时,这对其他同步方法时没有影响的,因为他们持有的锁都完全不一样。