synchronized在java多线程编程中是一个常见的也是相对轻量级的关键字,能够保证执行操作的原子性,有序性,以及可靠性,从而保证了并发安全。这篇文章主要讲解synchronized修饰方法时,synchronized是如何保证线程的安全的。
synchronized修饰普通方法时,两个线程对象能否同时调用呢?示例代码:
public class MultiThread {
private Integer num = 0;
public synchronized void printNum(String str) {
if ("a".equals(str)) {
num = 100;
System.out.println("tag " + str + ", set num over");
} else {
num = 200;
System.out.println("tag " + str + ", set num over");
}
System.out.println("tag " + str + ", num = " + num);
}
public static void main(String[] args) {
MultiThread m1 = new MultiThread();
MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}
这里synchronized修饰的时普通方法,看一下测试结果:
tag a, set num over
tag b, set num over
tag a, num = 100
tag b, num = 200
这里说明了两个线程对象是可以同时进入到这个方法的,那么synchronized关键字在这里锁的是什么,下面我们再看一个例子,用synchronized关键字来修饰static方法,示例代码:
public class MultiThread {
private static Integer num = 0;
public static synchronized void printNum(String str) throws InterruptedException {
if ("a".equals(str)) {
num = 100;
System.out.println("tag " + str + ", set num over");
sleep(1000);
} else {
num = 200;
System.out.println("tag " + str + ", set num over");
sleep(1000);
}
System.out.println("tag " + str + ", num = " + num);
}
public static void main(String[] args) {
MultiThread m1 = new MultiThread();
MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
m1.printNum("a");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
m2.printNum("b");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
为了效果明显一些,我们这里让其休眠一秒,来看一下测试结果:
tag b, set num over
tag b, num = 200
tag a, set num over
tag a, num = 100
这里可以看出synchronized修饰static方法时,两个线程对象没有同时进入到这个方法里。这里来解释一下,synchronized关键字修饰普通方法时,取得的锁是对象锁,而不是把一段代码或者方法当作锁,哪个线程先执行synchronized关键字的方法,那个线程就持有该方法所属对象的锁,两个对象同时执行,那么线程获得的就是两个不同的锁,它们之间是互不影响的,但是synchronized关键字修饰static方法时,代表锁定的时当前这个.class类,这是类级别的锁,此时两个对象执行就是互斥的,必须要等其中一个执行完毕释放锁,另一个对象才能执行。
当一个类中两个方法同时被synchronized修饰,同一个对象,能够同时调用这两个方法呢?示例代码:
public class MultiThread2 {
private Integer num = 0;
public synchronized void method1() {
num = 1;
System.out.println(Thread.currentThread().getName() + " , num = " + num);
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void method2() {
num = 2;
System.out.println(Thread.currentThread().getName() + " , num = " + num);
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MultiThread2 mt = new MultiThread2();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mt.method1();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
mt.method2();
}
},"t2");
t1.start();
t2.start();
}
}
来看一下测试结果,这里为了区别,每个方法都休眠了3秒钟。
t2 , num = 2
t1 , num = 2
这里线程t1在执行完method1()后,等待了3秒,线程t2才执行method2()方法,可以看出,同一个对象在访问多个synchronized关键字修饰的方法时,线程间不是同时执行的,必须等待该线程释放对象锁,hashtable以及vector中方法都被synchronized关键字修饰,也是利用这种方式,保证了并发场景下的线程安全。
接下来我们看一下synchronized的可重入的特性,示例代码:
public class MultiThread3 {
private Integer num = 0;
public synchronized void method1(){
num++;
System.out.println("num = " + num);
method2();
}
public synchronized void method2() {
num++;
System.out.println("num = " + num);
method3();
}
public synchronized void method3() {
num++;
System.out.println("num = " + num);
}
public static void main(String[] args) {
MultiThread3 mt = new MultiThread3();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mt.method1();
}
});
t1.start();
}
}
测试结果:
num = 1
num = 2
num = 3
这里即使每个方法都用synchronized所修饰,在方法间的相互调用上,也是可以重新获得锁的,而不会像我们想象中发生死锁情况,接下来再来看一个例子:
public class MainThread {
protected Integer num = 10;
public synchronized void printMainNum(){
num--;
System.out.println("main print num : " + num);
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class SubThread extends MainThread{
public synchronized void printSubNum(){
while (num > 0) {
num--;
System.out.println("sub print num : " + num);
printMainNum();
}
}
public static void main(String[] args) {
SubThread sub = new SubThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
sub.printSubNum();
}
});
thread.start();
}
}
这里subThread继承了父类MainThread,同时子类方法中调用了父类的方法,来看一下测试结果:
sub print num : 9
main print num : 8
sub print num : 7
main print num : 6
sub print num : 5
main print num : 4
sub print num : 3
main print num : 2
sub print num : 1
main print num : 0
这里同样,子类在调用父类方法时,父类也可以拿到锁,这里就要提到synchronized的可重入的特性。synchronized就是一种重入锁,重入锁是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法,当某个线程请求成功后,JVM会记下该锁的持有线程,并且将计数器置为1,此时其它线程请求该锁,则必须等待;而如果同一个线程再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增,当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。所以以上两个示例是不会发生死锁的。