1. synchronized 的用处
在学习操作系统的时候, 我们会经常听到死锁, 互斥量等名词, 我们知道这是在多个线程访问一些有限的资源而造成的. 同样, 在 java 多线程编程的时候, 往往这些线程都会在某个时机访问相同的资源, 这里的资源我们可以具体到一个变量, 一个对象, 一段代码块, 甚至一个类. 那么如何使这些线程按照我们的期望访问这些资源, 就要使用关键字 synchronized 来保护访问资源的代码片段. 这样, 当其他任务或者线程想要访问这段受保护的代码块时, 先检查锁是否可用, 如果可用, 则获取锁, 然后执行代码, 直到这段代码执行完毕, 才能释放锁. 在获取锁的线程执行这段受保护的代码期间, 其他任何线程想要访问这段代码块, 都将会被阻塞, 直到锁被释放. 所以我们可以把使用关键字 synchronized 的规则用 < thinking in java > 这本书中的一句话来概括:
如果你正在写一个变量, 它可能接下来被另一个线程读取, 或者正在读取一个上一次已经被另一个线程写过的变量, 那么你必须使用同步, 并且, 读写线程都必须用相同的监视器锁同步.
2. synchronized 的用法
既然知道了 synchronized 关键字的作用, 那么接下来我们学习一下, synchronized 关键字可以保护哪些资源, 以及如何作用到这些资源上. 这里呢, 我们细分为 6 种情况.
2.1 两个线程同时访问一个对象的同步方法
这里我们看一个 demo:
- package SynchronizedDemo;
- import java.util.concurrent.TimeUnit;
- public class SameThreadSync implements Runnable{
- @Override
- public void run() {
- synchronized (this) {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- }
- }
- package SynchronizedDemo;
- public class MainThread {
- public static void main(String[] args) {
- SameThreadSync runnable = new SameThreadSync();
- Thread thread1 = new Thread(runnable, "Amy thread");
- Thread thread2 = new Thread(runnable, "Bob thread");
- System.out.println("currentTime:" + System.currentTimeMillis());
- thread1.start();
- thread2.start();
- }
- }
然后我们运行一下, 看一下运行结果:
- currentTime: 1547988932743
- currentTime: 1547988935744 Amy thread has finished now
- currentTime: 1547988938748 Bob thread has finished now
可以看出, 线程 Bob 是在线程 Amy 执行完, 才开始执行. 另外, 如果我们把 synchronized(this) 换成另外一种写法的结果是一样的.
- package SynchronizedDemo;
- import java.util.concurrent.TimeUnit;
- public class SameThreadSync implements Runnable{
- @Override
- public synchronized void run() {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- }
因为 synchronized 块必须给定一个在其上进行同步的对象, 并且最合理的方式是, 使用其方法正在被调用的当前对象: synchronized(this). 所以我们这两种代码格式锁定的都是同一个对象, 即当前的对象. 所以根据 synchronized 这个关键字的定义, 只有当当前对象的锁释放之后, 另外一个线程才能访问这个对象.
当我去掉同步 synchronized(this) 时, 再运行一遍, 结果如下:
- currentTime: 1547989082259
- currentTime: 1547989085265 Amy thread has finished now
- currentTime: 1547989085265 Bob thread has finished now
这个时候, 我们发现 Bob 和 Amy 几乎同时完成.
2.2 两个线程访问的是两个对象的同步方法
- package SynchronizedDemo;
- public class MainThread {
- public static void main(String[] args) {
- SameThreadSync runnable = new SameThreadSync();
- SameThreadSync runnable2 = new SameThreadSync();
- Thread thread1 = new Thread(runnable, "Amy thread");
- Thread thread2 = new Thread(runnable2, "Bob thread");
- System.out.println("currentTime:" + System.currentTimeMillis());
- thread1.start();
- thread2.start();
- }
- }
运行结果如下:
- currentTime: 1547990419528
- currentTime: 1547990422533 Bob thread has finished now
- currentTime: 1547990422533 Amy thread has finished now
发现两个线程几乎同时完成, 并且第二个线程比第一个线程更先完成. 因为 synchronized 锁住的是当前运行的对象, 而此时 Amy 和 Bob 运行的是两个不同的对象, 所以不会出现一个线程等待另外一个线程释放锁之后, 才能执行. 另外出现 Bob 比 Amy 先结束, 这也不难理解, 当 Amy 线程 sleep 后时, Amy 线程进入休眠状态, CPU 会给 Bob 线程, 当 Amy 线程睡好之后, CPU 还没来的及给 Amy,Bob 就睡好了, 就先执行了 Bob 线程的打印语句.
总结: 对普通方法的锁, 等同于对当前调用对象的锁.
2.3 两个线程访问的是 synchronized 的静态方法
我们修改一下类 SameThread.java 的方法, 而在类 MainThread.java 中仍然创建的是两个不同的对象.
- package SynchronizedDemo;
- import java.util.concurrent.TimeUnit;
- public class SameThreadSync implements Runnable{
- @Override
- public void run() {
- method();
- }
- private synchronized static void method() {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- }
运行一下, 结果如下:
- currentTime: 1547991444723
- currentTime: 1547991447729 Amy thread has finished now
- currentTime: 1547991450734 Bob thread has finished now
为什么这里 Bob 线程要等 Amy 线程完成之后, 才能执行呢? 因为这里静态方法是属于一个类的, 所以对一个静态方法的锁, 就是对这个类锁, 所以对这个类的访问只能同时由一个线程访问, 所以 Bob 只能等待 Amy 对这个类的锁释放之后, 才能访问这个类.
如果我们把 synchronized static 换成 synchronized(SameThread.class) 看看呢?
- currentTime: 1547991699001
- currentTime: 1547991702005 Amy thread has finished now
- currentTime: 1547991705006 Bob thread has finished now
发现结果也是一样.
总结: 对静态方法的锁等同于对当前类的锁.
2.4 同时访问同步方法与非同步方法
- package SynchronizedDemo;
- import java.util.concurrent.TimeUnit;
- public class SameThreadSync implements Runnable{
- @Override
- public void run() {
- if (Thread.currentThread().getName().equals("Amy thread")) {
- method1();
- } else if (Thread.currentThread().getName().equals("Bob thread")) {
- method2();
- }
- }
- private synchronized void method1() {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- private void method2() {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- }
- package SynchronizedDemo;
- public class MainThread {
- public static void main(String[] args) {
- SameThreadSync runnable = new SameThreadSync();
- Thread thread1 = new Thread(runnable, "Amy thread");
- Thread thread2 = new Thread(runnable, "Bob thread");
- System.out.println("currentTime:" + System.currentTimeMillis());
- thread1.start();
- thread2.start();
- }
- }
运行一下, 结果如下:
- currentTime: 1547992475217
- currentTime: 1547992478219 Bob thread has finished now
- currentTime: 1547992478219 Amy thread has finished now
两个线程几乎同时结束. 所以, 如果一个类中的一个成员变量, 在多个方法被写访问时, 同时这个类的对象可能会被多个线程持有时, 那么最安全的做法就是将这个成员变量设置为私有的, 同时对这个变量的写访问的所有方法都要用 synchronized 关键字保护起来.
总结: synchronized 的方法并不能阻塞另一个线程对同一个对象的非 synchronized 的方法的调用.
2.5 访问同一个对象的不同的普通同步方法
我们将上例中的 method2() 方法也加上 synchronized 关键字, 运行一个代码:
- currentTime: 1547992878720
- currentTime: 1547992881726 Amy thread has finished now
- currentTime: 1547992884730 Bob thread has finished now
这个结果也验证了上一节说的对一个成员变量保护的方法. 如果一个线程 A 访问了一个对象的 synchronized 关键字保护的方法, 那么另一个线程 B 必须等待线程 A 释放了锁之后, 才能访问其他的 synchronized 方法.
2.6 同时访问静态的 synchronized 和 非静态 synchronized 方法
- package SynchronizedDemo;
- import java.util.concurrent.TimeUnit;
- public class SameThreadSync implements Runnable{
- @Override
- public void run() {
- if (Thread.currentThread().getName().equals("Amy thread")) {
- method1();
- } else if (Thread.currentThread().getName().equals("Bob thread")) {
- method2();
- }
- }
- private synchronized static void method1() {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- private synchronized void method2() {
- try {
- TimeUnit.MILLISECONDS.sleep(3000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("currentTime:" + System.currentTimeMillis() + "" + Thread.currentThread().getName() +" has finished now");
- }
- }
运行结果:
- currentTime: 1547993122882
- currentTime: 1547993125888 Amy thread has finished now
- currentTime: 1547993125888 Bob thread has finished now
发现二者几乎是同时运行的, 为什么呢? 因为 method1() 方法是一个静态方法, 而静态方法是类级别, 我们在上面也说过, 对一个静态方法的锁, 类似于 synchronized(xx.class), 说明锁的对象是. class 对象, 而 method2() 这个普通方法的锁, 我们等同于 synchronized(this), 说明锁的是当前运行的实例对象, 也就是说 method1() 和 method2() 锁的不是同一个对象, 所以二者是相互不影响的.
总结: 对静态方法的锁, 锁的是 class 对象, 对普通方法的锁, 锁的是当前类的实例对象, 二者相互不影响.
3. synchronized 的核心思想
一把锁只能同时被一个线程获取, 没有拿到锁的线程必须等待
每个实例都对应有自己的一把锁, 不同实例之间互不影响; 例外: 锁对象是 *.class 以及 synchronized 修饰的 static 方法的时候, 所有对象共用同一把类锁.
无论是方法正常执行完毕或者方法抛出异常, 都会释放锁.
来源: http://www.jianshu.com/p/bd3ba6b9431a