synchronized 锁

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

这个时候,我们发现BobAmy几乎同时完成。

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锁住的是当前运行的对象,而此时AmyBob运行的是两个不同的对象,所以不会出现一个线程等待另外一个线程释放锁之后,才能执行。另外出现BobAmy先结束,这也不难理解,当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方法的时候,所有对象共用同一把类锁。

  • 无论是方法正常执行完毕或者方法抛出异常,都会释放锁。



作者:雨打空城
链接:https://www.jianshu.com/p/bd3ba6b9431a
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自www.cnblogs.com/hanwuxing/p/11362077.html