什么是锁?
总所周知,锁是控制共享资源的访问机制,防止多线程访问共享资源的时候导致数据的不一致性。
那么在synchronized中锁是如何定义的?
synchronized
现象1: 实例锁竞争
2个同步方法,一个对象,先打印 发短信?打电话?
2个同步方法,两个对象,先打印发短信?打电话?
代码:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
sample s = new sample();
new Thread(() -> {
s.sendSms();
}).start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
s.codePhone();
}).start();
}
}
class sample {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void codePhone() {
System.out.println("打电话");
}
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
sample s = new sample();
sample s2 = new sample();
new Thread(() -> {
s.sendSms();
}).start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
s2.codePhone();
}).start();
}
}
class sample {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void codePhone() {
System.out.println("打电话");
}
}
结果
1:
2:
原因:
由于两个方法都使用了 synchronized,它们会互斥执行。第二个线程在 sendSms() 执行过程中被阻塞,直到 sendSms() 执行完并释放锁,才会开始执行 codePhone(),因此最终的输出顺序是 “发短信” 和 “打电话” 。
代码分析:
sendSms()方法是同步方法 :
-
- 方法上有
synchronized关键字,当一个线程进入该方法时,他会对sample2实例对象锁定, 其他线程无法在此期间访问sendSms(),直到当前线程执行完成该方法。
- 方法上有
codePhone()方法是同步方法:
-
codePhone()方法也加了synchronized关键字,因此它也是同步方法,需要等待同一个实例对象锁,如果锁先给别的线程拿去,需要等待。
线程执行顺序:
- 第一个线程 启动并执行
sendSms()方法,它将获得s对象的锁并开始执行该方法。 sendSms()方法中有sleep(3),这意味着线程会睡眠 3 秒。此时,该线程占用锁,其他线程无法进入任何同步方法(包括codePhone())。- 第二个线程 启动并执行
codePhone()方法,由于sendSms()已经持有了s对象的锁,codePhone()将被阻塞,直到sendSms()执行完毕并释放锁。 - 当第一个线程执行完
sendSms()后,它会释放锁,这时 第二个线程才能获取到锁并执行codePhone(),输出 “打电话” 。
现象2: 实例锁与非锁 并行
1个静态的同步方法,1个普通的同步方法,一个对象,先打印 发短信?打电话?
1个静态的同步方法,1个普通的同步方法,两个对象,先打印发短信?打电话?
代码
public class Test2 {
public static void main(String[] args) throws InterruptedException {
sample2 s = new sample2();
new Thread(() -> {
s.sendSms();
}).start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
s.codePhone();
}).start();
}
}
class sample2 {
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("发短信");
}
public void codePhone() {
System.out.println("打电话");
}
}
结果:
原因:
代码分析:
“打电话” 先输出的原因与 synchronized 的锁机制和线程的执行顺序有关。
sendSms()方法是同步方法 :
-
- 方法上有
synchronized关键字,当一个线程进入该方法时,他会对sample2实例对象锁定, 其他线程无法在此期间访问sendSms(),直到当前线程执行完成该方法。
- 方法上有
codePhone()方法是普通方法:
-
codePhone()方法没有加synchronized,因此它是 非同步方法,不需要等待任何锁,线程可以直接执行。
线程执行顺序
main()方法创建了两个线程,第一个线程执行sendSms()方法,第二个线程执行codePhone()方法。虽然sendSms()被synchronized修饰,但由于codePhone()没有任何同步控制,它是可以立即执行的。- 由于线程的调度是异步的,第一个线程 (
sendSms()) 被启动后,马上会尝试获取当前sample2实例的锁。此时第二个线程 (codePhone()) 虽然在等待sendSms()完成,但没有任何同步机制限制它的执行,它可以直接执行并输出 “打电话” 。 - 由于
sendSms()线程在执行时会睡眠 3 秒(TimeUnit.SECONDS.sleep(3)),而codePhone()是一个无锁的方法,它会 抢先执行,所以会 先输出 "打电话" 。
锁的细节:
sendSms()方法被synchronized修饰,意味着它在执行时会锁住当前对象sample2的实例锁。当第一个线程执行sendSms()时,它会持有该锁,直到sendSms()执行完毕。- 而
codePhone()没有任何同步控制,因此它在没有任何限制的情况下, 可以在sendSms()执行过程中先行执行。
线程调度:
线程的执行顺序是由操作系统的调度器决定的。这里,尽管 sendSms() 在第一个线程中被调用,但由于没有对 codePhone() 方法加锁,它会先执行,输出 “打电话” 。
现象3: 类锁的竞争
增加两个静态的同步方法,只有一个对象,先打印发短信?打电话
增加两个静态的同步方法,两个对象! 先打印发短信?打电话?
代码
public class Test3 {
public static void main(String[] args) throws InterruptedException {
sample3 s = new sample3();
sample3 s2 = new sample3();
new Thread(() -> {
s.sendSms();
}).start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
s2.codePhone();
}).start();
}
}
class sample3 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void codePhone() {
System.out.println("打电话");
}
}
结果:
原因:
sendSms() 和 codePhone() 方法都使用了 static synchronized 修饰符。由于这两个方法是静态的,它们会使用 类锁,而不是实例对象锁。
代码分析:
static synchronized 锁的工作原理:
- 当方法是静态的时,锁是基于 类级别的锁(即
sample3.class),而不是基于实例对象的锁。这意味着,所有调用sendSms()和codePhone()方法的线程都必须争夺同一个 类锁。 - 由于
sendSms()和codePhone()都是静态同步方法,它们都需要 锁住sample3.class类锁。因此,一个线程正在执行sendSms()时,另一个线程无法执行codePhone(),直到第一个线程释放锁。
线程执行顺序:
- 第一个线程:启动后执行
sendSms(),该方法是静态同步的,因此会尝试获取sample3.class类锁,执行该方法。 - 第二个线程:启动后执行
codePhone(),由于sendSms()方法已经持有了sample3.class类锁,所以第二个线程在获取类锁时会被阻塞,直到第一个线程释放锁。
现象4: 实例锁与类锁 并行
一个静态同步方法,一个同步方法,只有一个对象,先打印发短信?打电话
一个静态同步方法,一个同步方法,两个对象! 先打印发短信?打电话?
代码
public class Test3 {
public static void main(String[] args) throws InterruptedException {
sample4 s = new sample4();
sample4 s2 = new sample4();
new Thread(() -> {
s.sendSms();
}).start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
s2.codePhone();
}).start();
}
}
class sample4 {
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void codePhone() {
System.out.println("打电话");
}
}
结果:
原因:
sendSms() 是 静态同步方法,而 codePhone() 是 实例同步方法。这意味着它们会使用 不同的锁,一个是基于 类锁,另一个是基于 实例锁。
代码分析:
sendSms()方法:
-
sendSms()是static synchronized,意味着它锁住的是sample4.class这个类锁,即所有对该类的静态同步方法的访问都需要先获得类的锁。
codePhone()方法:
-
codePhone()是 实例同步方法,它锁住的是 当前对象实例 的锁(即this锁)。因此,不同的实例可以并发执行这个方法,因为它们锁住的是不同的对象实例。
线程执行顺序:
- 第一个线程 启动并执行
sendSms(),它会尝试获取sample4.class类锁,并在执行时会进行 3 秒的sleep,即第一个线程持有 类锁(sample4.class) 3 秒钟。 - 第二个线程 启动并执行
codePhone(),它会尝试获取s2对象实例的锁,即s2实例的锁。由于sendSms()锁住的是 类锁,并不会阻塞codePhone()线程的执行。因此,第二个线程能够 立即执行codePhone()方法。
小结
synchronized锁的粒度不同:
new this 具体的一个手机
static Class 唯一的一个模板