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
方法的时候,所有对象共用同一把类锁。 -
无论是方法正常执行完毕或者方法抛出异常,都会释放锁。
作者:雨打空城
链接:https://www.jianshu.com/p/bd3ba6b9431a
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。