1、前言
程序员经常听到“并发锁”这个名词,而且实际项目中也确实避免不了要加锁。那么什么是锁?锁的是什么?今天文章从8个有意思的案例,彻底弄清这两个问题。
2、什么是synchronized
Java中的一个保留字(关键字)。加了synchronized表明该段代码为同步块,也称之为同步锁。synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。JDK1.5之前synchronized是一个重量级锁,相对于JUC中的Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。
简单的使用示例代码为:
public synchronized void doSomething(){ ... } 或 public void doSomething() { synchronized(this){ ... } }
3、代码一:单实例synchronized调用
eat():普通synchronized方法
sleep():普通synchronized方法
思考问题:控制台先打印什么?先吃饭?还是先睡觉?
/** * @author Shamee loop * @date 2023/4/9 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog = new Dog(); new Thread(() -> dog.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog.sleep()).start(); } } class Dog { public synchronized void eat(){ TimeUnit.SECONDS.sleep(2); System.out.println("先吃饭"); } public synchronized void sleep(){ System.out.println("先睡觉"); } }
执行结果:
原因:
普通方法加了synchronized,而synchronized锁的是该方法的调用者,也就是dog实例。这时候两个线程都由同一个dog实例调用,因此sleep()方法需要等待。
这里的eat()方法为什么也睡了2s,原因是为了排除程序打印”先吃饭“是因为方法被先调用的干扰。
4、代码二、多实例synchronized调用
eat():普通synchronized方法
sleep():普通synchronized方法
思考问题:控制台先打印什么?先吃饭?还是先睡觉?
/** * @author Shamee loop * @date 2023/4/9 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog1 = new Dog(); Dog dog2 = new Dog(); new Thread(() -> dog1.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog2.sleep()).start(); } } class Dog { public synchronized void eat(){ TimeUnit.SECONDS.sleep(2); System.out.println("先吃饭"); } public synchronized void sleep(){ System.out.println("先睡觉"); } }
执行结果:
原因:
普通方法加了synchronized,而synchronized锁的是该方法的调用者,也就是dog实例。这时候两个线程由不同的两个实例dog1、dog2调用,因此相互之间锁不会等待,而eat()睡了2s,因此就先输出了“先睡觉”。
5、代码三、单实例synchronized和普通方法
eat():普通synchronized方法
sleep():普通synchronized方法
这里增加一个普通方法drink()。
思考问题:控制台先打印什么?先吃饭?还是先喝水?
public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog = new Dog(); new Thread(() -> dog.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog.drink()).start(); } } class Dog { public synchronized void eat(){ try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("先吃饭"); } public synchronized void sleep(){ System.out.println("先睡觉"); } public void drink(){ System.out.println("先喝水"); } }
执行结果:
原因:
drink()方法是普通方法,没有加锁,因此不会收到锁影响。
6、代码四、多实例synchronized和普通方法
eat():普通synchronized方法
sleep():普通synchronized方法
这里增加一个普通方法drink()。
思考问题:控制台先打印什么?先吃饭?还是先喝水?
public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog1 = new Dog(); Dog dog2 = new Dog(); new Thread(() -> dog1.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog2.drink()).start(); } } class Dog { public synchronized void eat(){ try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("先吃饭"); } public synchronized void sleep(){ System.out.println("先睡觉"); } public void drink(){ System.out.println("先喝水"); } }
执行结果:
原因:
drink()方法是普通方法,没有加锁,因此不会收到锁影响。
7、代码五、单实例static和synchronized调用
eat():静态synchronized方法
sleep():静态synchronized方法
这里eat()和sleep()添加static修饰。
思考问题:控制台先打印什么?先吃饭?还是先睡觉?
/** * @author Shamee loop * @date 2023/4/9 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog = new Dog(); new Thread(() -> dog.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog.sleep()).start(); } } class Dog { public static synchronized void eat(){ try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("先吃饭"); } public static synchronized void sleep(){ System.out.println("先睡觉"); } }
执行结果:
原因:
这里加了static之后,synchronized锁的就不再是调用者实例了。由于static特性,这里锁定的是整个class类,而不是实例。
8、代码六、多实例static和synchronized调用
eat():静态synchronized方法
sleep():静态synchronized方法
这里eat()和sleep()添加static修饰。
思考问题:控制台先打印什么?先吃饭?还是先睡觉?
/** * @author Shamee loop * @date 2023/4/9 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog1 = new Dog(); Dog dog2 = new Dog(); new Thread(() -> dog1.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog2.sleep()).start(); } } class Dog { public static synchronized void eat(){ try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("先吃饭"); } public static synchronized void sleep(){ System.out.println("先睡觉"); } }
执行结果:
原因:
这里加了static之后,synchronized锁的就不再是调用者实例了。由于static特性,这里锁定的是整个class类,而不是实例。因此这里跟多实例调用无关。
9、代码七、单实例static同步方法和普通方法调用
eat():静态synchronized方法
sleep():普通synchronized方法
思考问题:控制台先打印什么?先吃饭?还是先睡觉?
/** * @author Shamee loop * @date 2023/4/9 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog = new Dog(); new Thread(() -> dog.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog.sleep()).start(); } } class Dog { public static synchronized void eat(){ try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("先吃饭"); } public synchronized void sleep(){ System.out.println("先睡觉"); } }
执行结果:
原因:
eat()这里加了static之后,synchronized锁的是整个class类,而sleep()锁的是调用者实例dog;因此锁互不影响。
10、代码八、多实例static同步方法和普通方法调用
eat():静态synchronized方法
sleep():普通synchronized方法
思考问题:控制台先打印什么?先吃饭?还是先睡觉?
/** * @author Shamee loop * @date 2023/4/9 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Dog dog1 = new Dog(); Dog dog2 = new Dog(); new Thread(() -> dog1.eat()).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> dog2.sleep()).start(); } } class Dog { public static synchronized void eat(){ try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); } System.out.println("先吃饭"); } public synchronized void sleep(){ System.out.println("先睡觉"); } }
执行结果:
原因:
eat()这里加了static之后,synchronized锁的是整个class类,而sleep()锁的是调用者实例dog;因此锁互不影响。
11、结论
因此,对于锁。我们需要搞懂锁是谁持有的,通常一般属于调用者持有。调用者又分两类:实例调用(new),类调用(static)。这里用最简单的代码来理解锁。当然锁的机制远比这里的复杂很多,这里只是入门。