🍤 并发操作合集系列 目录
🍕 并发操作合集系列 源代码
这篇文章将告诉你如何正确的使用synchronized关键字。通过示例代码彻底了解synchronized关键字的使用规则和一些隐患。
1.Java的锁
1.1 锁的内存语义
- 锁可以让临界区互斥执行,还可以让释放锁的线程向同一个锁的线程发送消息
- 锁的释放要遵循Happens-before原则(
锁规则:解锁必然发生在随后的加锁之前
)
- 锁在Java中的具体表现是
Synchronized
和 Lock
2.Synchronized的综述
- 同步机制: synchronized是Java同步机制的一种实现,即互斥锁机制,它所获得的锁叫做互斥锁
- 互斥锁: 指的是每个对象的锁一次只能分配给一个线程,同一时间只能由一个线程占用
- 作用: synchronized用于保证同一时刻只能由一个线程进入到临界区,同时保证共享变量的可见性、原子性和有序性
- 使用: 当一个线程试图访问同步代码方法(块)时,它首先必须得到锁,退出或抛出异常时必须释放锁
3.Synchronized的使用
3.1 Synchronized的三种应用方式
补充: 使用同步代码块的好处在于其他线程仍可以访问非synchronized(this)的同步代码块
3.2 Synchronized的使用规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
|
public class SynchronizedDemo { public static synchronized void staticMethod(){ System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问静态同步方法staticMethod"); } public static void staticMethod2(){ System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod2"); synchronized (SynchronizedDemo.class){ System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中获取了SynchronizedDemo.class"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void synMethod(){ System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod"); } public synchronized void synMethod2(){ System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod2"); } public void method(){ System.out.println(Thread.currentThread().getName() + "访问了普通方法method"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束访问普通方法method"); } private Object lock = new Object(); public void chunkMethod(){ System.out.println(Thread.currentThread().getName() + "访问了chunkMethod方法"); synchronized (lock){ System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中获取了lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void chunkMethod2(){ System.out.println(Thread.currentThread().getName() + "访问了chunkMethod2方法"); synchronized (lock){ System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中获取了lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void chunkMethod3(){ System.out.println(Thread.currentThread().getName() + "访问了chunkMethod3方法"); synchronized (this){ System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中获取了this"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void stringMethod(String lock){ synchronized (lock){ while (true){ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
|
3.2.1 普通方法与同步方法调用互不关联
当一个线程进入同步方法时,其他线程可以正常访问其他非同步方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { synDemo.method(); }); Thread thread2 = new Thread(() -> { synDemo.synMethod(); }); thread1.start(); thread2.start(); } ---------------------
Thread-1访问了同步方法synMethod Thread-0访问了普通方法method Thread-0结束访问普通方法method Thread-1结束访问同步方法synMethod
|
3.2.2 所有同步方法只能被一个线程访问
当一个线程执行同步方法时,其他线程不能访问任何同步方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { synDemo.synMethod(); synDemo.synMethod2(); }); Thread thread2 = new Thread(() -> { synDemo.synMethod2(); synDemo.synMethod(); }); thread1.start(); thread2.start(); } ---------------------
Thread-0访问了同步方法synMethod Thread-0结束访问同步方法synMethod Thread-0访问了同步方法synMethod2 Thread-0结束访问同步方法synMethod2 Thread-1访问了同步方法synMethod2 Thread-1结束访问同步方法synMethod2 Thread-1访问了同步方法synMethod Thread-1结束访问同步方法synMethod
|
3.2.3 同一个锁的同步代码块同一时刻只能被一个线程访问
当同步代码块都是同一个锁时,方法可以被所有线程访问,但同一个锁的同步代码块同一时刻只能被一个线程访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { synDemo.chunkMethod(); synDemo.chunkMethod2(); }); Thread thread2 = new Thread(() -> { synDemo.chunkMethod(); synDemo.chunkMethod2(); }); thread1.start(); thread2.start(); } ---------------------
Thread-0访问了chunkMethod方法 Thread-1访问了chunkMethod方法 Thread-0在chunkMethod方法中获取了lock ...停顿等待... Thread-1在chunkMethod方法中获取了lock ...停顿等待... Thread-0访问了chunkMethod2方法 Thread-0在chunkMethod2方法中获取了lock ...停顿等待... Thread-1访问了chunkMethod2方法 Thread-1在chunkMethod2方法中获取了lock
|
3.2.4 线程间同时访问同一个锁的多个同步代码的执行顺序不定
- 线程间同时访问同一个锁多个同步代码的执行顺序不定,即使是使用同一个对象锁,这点跟同步方法有很大差异
- ??读者可以先思考为什么会出现这样的问题??
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { synDemo.chunkMethod(); synDemo.chunkMethod2(); }); Thread thread2 = new Thread(() -> { synDemo.chunkMethod2(); synDemo.chunkMethod(); }); thread1.start(); thread2.start(); } ---------------------
Thread-0访问了chunkMethod方法 Thread-1访问了chunkMethod2方法 Thread-0在chunkMethod方法中获取了lock ...停顿等待... Thread-0访问了chunkMethod2方法 Thread-1在chunkMethod2方法中获取了lock ...停顿等待... Thread-1访问了chunkMethod方法 Thread-0在chunkMethod2方法中获取了lock ...停顿等待... Thread-1在chunkMethod方法中获取了lock
|
3.2.5 不同锁之间访问非阻塞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> synDemo.chunkMethod()); Thread thread2 = new Thread(() -> synDemo.chunkMethod3()); Thread thread3 = new Thread(() -> synDemo.staticMethod()); Thread thread4 = new Thread(() -> synDemo.staticMethod2());
thread1.start(); thread2.start(); thread3.start(); thread4.start(); } ---------------------
Thread-1访问了chunkMethod3方法 Thread-1在chunkMethod3方法中获取了this Thread-2访问了静态同步方法staticMethod Thread-0访问了chunkMethod方法 Thread-0在chunkMethod方法中获取了lock Thread-3访问了静态同步方法staticMethod2 ...停顿等待... Thread-2结束访问静态同步方法staticMethod Thread-3在staticMethod2方法中获取了SynchronizedDemo.class
|
3.3 Synchronized的可重入性
- 重入锁:当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功
- 实现:一个线程得到一个对象锁后再次请求该对象锁,是允许的,每重入一次,monitor进入次数+1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { synDemo.synMethod(); synDemo.synMethod2(); }); Thread thread2 = new Thread(() -> { synDemo.synMethod2(); synDemo.synMethod(); }); thread1.start(); thread2.start(); } ---------------------
Thread-0访问了同步方法synMethod Thread-0结束访问同步方法synMethod Thread-0访问了同步方法synMethod2 Thread-0结束访问同步方法synMethod2 Thread-1访问了同步方法synMethod2 Thread-1结束访问同步方法synMethod2 Thread-1访问了同步方法synMethod Thread-1结束访问同步方法synMethod
|
3.4 Synchronized与String锁
- 隐患:由于在JVM中具有String常量池缓存的功能,因此相同字面量是同一个锁!!!
- 注意:严重不推荐将String作为锁对象,而应该改用其他非缓存对象
- 提示:对字面量有疑问的话请先回顾一下String的基础,这里不加以解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void main(String[] args) { SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> synDemo.stringMethod("sally")); Thread thread2 = new Thread(() -> synDemo.stringMethod("sally")); thread1.start(); thread2.start(); } ---------------------
Thread-0 Thread-0 Thread-0 Thread-0 ...死循环...
|
3.5 Synchronized与不可变锁
- 隐患:当使用不可变类对象(final Class)作为对象锁时,使用synchronized同样会有并发问题
- 原因:由于不可变特性,当作为锁但同步块内部仍然有计算操作,会生成一个新的锁对象
- 注意:严重不推荐将final Class作为锁对象时仍对其有计算操作
- 补充:虽然String也是final Class,但它的原因却是字面量常量池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class SynchronizedDemo { static Integer i = 0; public static void main(String[] args) throws InterruptedException { Runnable runnable = new Runnable() { @Override public void run() { for (int j = 0;j<10000;j++){ synchronized (i){ i++; } } } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i); } } ---------------------
14134
|
3.6 Synchronized与死锁
- 死锁:当线程间需要相互等待对方已持有的锁时,就形成死锁,进而产生死循环
- 注意:**代码中严禁出现死锁!!!**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public static void main(String[] args) { Object lock = new Object(); Object lock2 = new Object(); Thread thread1 = new Thread(() -> { synchronized (lock){ System.out.println(Thread.currentThread().getName() + "获取到lock锁"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ System.out.println(Thread.currentThread().getName() + "获取到lock2锁"); } } }); Thread thread2 = new Thread(() -> { synchronized (lock2){ System.out.println(Thread.currentThread().getName() + "获取到lock2锁"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock){ System.out.println(Thread.currentThread().getName() + "获取到lock锁"); } } }); thread1.start(); thread2.start(); } ---------------------
Thread-1获取到lock2锁 Thread-0获取到lock锁 .....
|
Reference
Java 8 并发篇 - 冷静分析 Synchronized(上)
并发番@Synchronized一文通(1.8版)