synchronized关键字经常被用来做线程互斥锁,但是使用不当的话,经常达不到目的。初学者常对锁住的是对象还是类有疑问。
原理:无论是对象还是类都有唯一的锁,synchronized只是声明了函数调用时需要什么锁,每个锁同一时间只能由一个线程获取,借此实现了线程互斥。
(1)分析对象锁
A.synchronized修饰非静态函数
接下来看实例:
public enum Person {
Alice,
Bob;
public synchronized void say() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " say hello world! " +TimeUtil.getCurrentTime());
}
public synchronized void speak() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
}
public void print() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " print hello world! " + TimeUtil.getCurrentTime());
}
}
这是一个Person类,其中有三个函数,say()和speak()这种声明方式意味着调用该函数需要持有Person实例的对象锁,即用哪个实例调用,则需要持有哪个对象的锁。print()函数无需任何锁。
public class ThreadA implements Runnable {
@Override
public void run() {
Person.Alice.say();
}
}
public class ThreadB implements Runnable {
@Override
public void run() {
Person.Bob.say();
}
}
public class ThreadC implements Runnable {
@Override
public void run() {
Person.Alice.print();
}
}
public static String getCurrentTime() {
SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
return sdf.format(new Date());
}
创建多个线程,然后执行函数:
public class Main {
public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA());
threadA.setName("threadA");
Thread threadB = new Thread(new ThreadB());
threadB.setName("threadB");
Thread threadC = new Thread(new ThreadC());
threadC.setName("threadC");
threadA.start();
System.out.println("A启动了 " + TimeUtil.getCurrentTime());
threadB.start();
System.out.println("B启动了 " + TimeUtil.getCurrentTime());
threadC.start();
System.out.println("C启动了 " + TimeUtil.getCurrentTime());
}
}
输出结果为:
A启动了 20:26
B启动了 20:26
C启动了 20:26
threadA say hello world! 20:28
threadC print hello world! 24:21
threadB say hello world! 20:28
可以看出,用两个实例分别调用say()函数是不会出现互斥的。函数执行时,每个函数都可以拿到调用对象的锁。
接下来我们进行改动,将ThreadB改为:
public class ThreadB implements Runnable {
@Override
public void run() {
Person.Alice.say();
}
}
执行输出:
A启动了 25:33
B启动了 25:33
C启动了 25:33
threadA say hello world! 25:35
threadC print hello world! 25:35
threadB say hello world! 25:37
可以明显看出线程B和A存在对象锁竞争,A持有Alice锁的时候,B等待。
改动ThreadB为
public class ThreadB implements Runnable {
@Override
public void run() {
Person.Alice.speak();
}
}
执行输出:
A启动了 27:29
B启动了 27:29
C启动了 27:29
threadA say hello world! 27:31
threadC print hello world! 27:31
threadB speak hello world! 27:33
可以看出即使用Alice调用不同的函数,还是会出现等待.
总结:对象锁同一时间只能由一个线程持有,此时其余线程无法再用此对象调用需要此对象锁的函数。
B.synchronized修饰代码块
实例如下:
public enum Person {
Alice,
Bob;
public synchronized void say() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " say hello world! " + TimeUtil.getCurrentTime());
}
public void speak() {
synchronized (Alice) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
}
}
}
public class ThreadA implements Runnable {
@Override
public void run() {
Person.Alice.say();
}
}
public class ThreadB implements Runnable {
@Override
public void run() {
Person.Alice.speak();
}
}
public class Main {
public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA());
threadA.setName("threadA");
Thread threadB = new Thread(new ThreadB());
threadB.setName("threadB");
threadA.start();
System.out.println("A启动了 " + TimeUtil.getCurrentTime());
threadB.start();
System.out.println("B启动了 " + TimeUtil.getCurrentTime());
}
}
执行输出:
A启动了 35:26
B启动了 35:26
threadA say hello world! 35:28
threadB speak hello world! 35:30
可以看出A和B出现了互斥,A调用的say()函数用
synchronized关键字修饰,所以此时A占用Alice锁,speak中的代码块用synchronized (Alice)修饰,说明同样需要Alice锁,因此互斥。
改动speak函数:
public void speak() {
synchronized (Bob) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
}
}
将需求锁改为Bob,执行输出:
A启动了 38:36
B启动了 38:36
threadA say hello world! 38:38
threadB speak hello world! 38:38
可以看到互斥消失了。
继续更改:
public void speak() {
synchronized (this) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
}
}
这里的this指的是需要调用当前函数的对象锁,执行输出:
A启动了 40:45
B启动了 40:45
threadA say hello world! 40:47
threadB speak hello world! 40:49
互斥又出现了,因为又在竞争Alice锁
(2)分析类锁
每一个类都有唯一且同一时间只能被唯一线程持有的类锁。
实例如下:
public enum Person {
Alice,
Bob;
public static synchronized void say() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " say hello world! " + TimeUtil.getCurrentTime());
}
public static synchronized void speak() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
}
}
public class ThreadA implements Runnable {
@Override
public void run() {
Person.say();
}
}
public class ThreadB implements Runnable {
@Override
public void run() {
Person.speak();
}
}
public class Main {
public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA());
threadA.setName("threadA");
Thread threadB = new Thread(new ThreadB());
threadB.setName("threadB");
threadA.start();
System.out.println("A启动了 " + TimeUtil.getCurrentTime());
threadB.start();
System.out.println("B启动了 " + TimeUtil.getCurrentTime());
}
}
执行输出:
A启动了 44:18
B启动了 44:18
threadA say hello world! 44:20
threadB speak hello world! 44:22
明显看到线程互斥。
改动代码:
public static void speak() {
synchronized (Person.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
}
}
改动speak函数,将里面的的代码用 synchronized (Person.class) 修饰,执行输出:
A启动了 44:18
B启动了 44:18
threadA say hello world! 44:20
threadB speak hello world! 44:22
两个线程依旧互斥,因为还在竞争类锁。
总结:类锁类似对象锁,唯一且同一时间只能由唯一线程持有。
最后补充一点:类锁和对象锁是两套互斥机制,互不影响,具体看你的函数需求的是对象锁(哪个对象)还是类锁