https://mp.weixin.qq.com/s/b3Sx2IDs6pJ3dANRIdPwRw
synchronized在非静态上下文中的应用解析
首先,我们先看一个代码例子,然后再详细分析一下为什么会这样。
1
//模拟一个线程,探究在编程实践中
//synchronized是如何持有对象锁
//如何实现同步控制的
public class DemoA extends Thread{
private String msg;
public DemoA(String str){
this.msg = str;
}
public synchronized void print(String str){
while(true){
try {
//模拟一个耗时操作,调用sleep函数
//会释放占用的CPU,但是不会释放对象锁
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
//覆写Thread的run方法
public void run(){
print(msg);
}
}
2
public class DemoB extends Thread{
private DemoA demoA;
public DemoB(DemoA a){
demoA = a;
}
//覆写Thread方法
//委托DemoA对象,调用操作
public void run(){
demoA.print("B");
}
}
3
//非静态上下文
//synchronized修饰符在同步控制中的作用
public class NoStaticSyncTest {
public static void main(String[] args){
DemoA demoA1 = new DemoA("A1");
demoA1.start(); //线程A1
DemoB demoB = new DemoB(demoA1);
demoB.start(); //线程B
DemoA demoA2 = new DemoA("A2");
demoA2.start(); //线程A2
}
}
老铁们可以先想一想该程序的运行结果,然后再往下看。
线程A1通过对象demoA1启动,并调用了被synchronized修饰的print成员函数,按照我们上文(NO.25 synchronized关键字画像:开胃菜)所提到的,线程要进入并执行该成员函数,需要先获取demoA1所关联锁,由于该锁当前并没有被任何线程使用,所以线程A1顺利的获取了demoA1的所关联的lock,并开始执行print函数。
紧接着,线程B通过对象demoB启动,并通过传入的demoA1对象引用,调用了被synchronized修饰的print成员函数;同理,要执行该函数,需要获取调用该函数的对象demoA1所持有的锁,由于该锁已经被线程A1执行过程所持有,为此线程B只能等待线程A1执行完成释放demoA1所持有的锁以后,才能得到执行的机会;但是,print函数是一个“死”循环,为此线程B会一直被阻塞着,直到JVM退出而终止。
最后,线程A2通过对象demoA2启动,并调用了被synchronized修饰的print成员函数;同理,线程要进入并执行该成员函数,需要先获取demoA2所关联锁,由于该锁(由于每个对象都会有一个与之关联的lock,即使属于同一类型的对象所持有的锁也是不同的,因此该锁不同于demoA1所关联的锁)当前并没有被任何线程使用,所以线程A2顺利的获取了demoA2的所关联的lock,并开始执行print函数。
通过以上分析,线程A1与线程A2会交替执行,线程B会因为一直不能获取到demoA1所关联的锁而被永久阻塞。因此,该函数的输出结果,“可能”会是这样:
结果
A1
A2
A1
A2
A1
A2
02、synchronized在静态上下文中的应用解析
同样地,我们先看一个代码例子,然后再详细分析一下为什么会这样。
4
public class DemoC implements Runnable{
private String msg;
public DemoC(String str){
msg = str;
}
public synchronized void print1(String str){
while(true){
try {
//模拟一个耗时操作,调用sleep函数
//会释放占用的CPU,但是不会释放对象锁
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
public synchronized static void print2(String str){
while(true){
try {
//模拟一个耗时操作,调用sleep函数
//会释放占用的CPU,但是不会释放对象锁
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
public void run(){
print1(msg);
}
}
5
public class StaticSyncTest {
public static void main(String[] args){
DemoC demoC1 = new DemoC("C1");
Thread c1 = new Thread(demoC1);
c1.start();
demoC1.print2("C2");
}
}
老铁们可以先想一想该程序的运行结果,接着,我们再一起往下看。
该程序首先定义了一个对象引用demoC1,然后启动线程c1,该线程会执行被synchronized修饰的print1函数,进入print1函数前,会首先判断是否持有对象引用demoC1的锁,由于该锁尚未被其他线程所占用,为此该线程会被执行;接着demoC1会调用print2函数,该函数同时被synchronized与static关键字修饰,按照我们上文所提及的方法,要进入该函数,需要持有DemoC.class对象所关联lock,由于该对象尚未被其他线程所持有,为此该函数也将被执行。
通过以上分析,线程c1与demoC1.print2都会被执行。因此,该函数的输出结果,“可能”会是这样:
结果
C1
C2
C1
C2
C2
C1
C2
03、合理利用synchronized实现线程同步
通过以上分析,我们就基本掌握了synchronized在非静态上下文与静态上下文中是如何在线程间同步发挥作用的。
推而广之,我们一般有三种利用synchronized方法实现线程同步的方法:
第一种:修饰方法
6
//synchronized修饰成员函数
//该函数能被执行,则意味则持有了调用
//该函数的对象引用所持有的lock
//多个函数如果存在同步关系,则可以在这
//些函数前面都增加synchronize修饰
public synchronized void subroutine1(){
}
第二种:修饰一般对象或this对象
7
//多个函数如果存在同步关系,则可以
//用synchronize修饰this对象
public synchronized void subroutine1(){
synchronized(this){
}
}
private DemoA demoA = new DemoA("A");
//多个函数如果存在同步关系,则可以
//用synchronize修饰demoA对象
public synchronized void subroutine2(){
synchronized(demoA){
}
}
第三种,修饰轻量级对象
8
private byte[] lock = new byte[1];
//多个函数如果存在同步关系,则可以
//用synchronize修饰lock对象
//lock是一个轻量级对象
public void subroutine2(){
synchronized(lock){
}
}
为了适应高并发性能以及快速响应的要求,synchronized不同的写法对程序响应的快慢和对CPU等资源利用率是不同的;对比以上方式,从程序运行性能与执行效率来看,从高到低依次排序为:第三种方式 > 第二种方式 > 第一种方式。
划重点
关键字synchronized锁定的是对象,而不是函数或代码。函数或代码区块被声明或修饰为synchronized,并非意味着它同一时刻只能由一个线程执行。
因为锁定的对象不一样,加锁与解锁都需要此对象资源,为此锁定的对象资源越小,性能开销就越小,采用byte作为锁对象最为经济。
当synchronized用于修饰对象引用时,则取得的lock将被交给该引用所指向的对象。
当调用一个synchronized static 函数时,获得的lock将与定义该函数的class相关联,而不是与调用函数的那个对象相关联。当synchronized修饰的是A.class时,获得的lock也是与上述相同,即与class相关联。
synchronized在非静态上下文中的应用解析
首先,我们先看一个代码例子,然后再详细分析一下为什么会这样。
1
//模拟一个线程,探究在编程实践中
//synchronized是如何持有对象锁
//如何实现同步控制的
public class DemoA extends Thread{
private String msg;
public DemoA(String str){
this.msg = str;
}
public synchronized void print(String str){
while(true){
try {
//模拟一个耗时操作,调用sleep函数
//会释放占用的CPU,但是不会释放对象锁
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
//覆写Thread的run方法
public void run(){
print(msg);
}
}
2
public class DemoB extends Thread{
private DemoA demoA;
public DemoB(DemoA a){
demoA = a;
}
//覆写Thread方法
//委托DemoA对象,调用操作
public void run(){
demoA.print("B");
}
}
3
//非静态上下文
//synchronized修饰符在同步控制中的作用
public class NoStaticSyncTest {
public static void main(String[] args){
DemoA demoA1 = new DemoA("A1");
demoA1.start(); //线程A1
DemoB demoB = new DemoB(demoA1);
demoB.start(); //线程B
DemoA demoA2 = new DemoA("A2");
demoA2.start(); //线程A2
}
}
老铁们可以先想一想该程序的运行结果,然后再往下看。
线程A1通过对象demoA1启动,并调用了被synchronized修饰的print成员函数,按照我们上文(NO.25 synchronized关键字画像:开胃菜)所提到的,线程要进入并执行该成员函数,需要先获取demoA1所关联锁,由于该锁当前并没有被任何线程使用,所以线程A1顺利的获取了demoA1的所关联的lock,并开始执行print函数。
紧接着,线程B通过对象demoB启动,并通过传入的demoA1对象引用,调用了被synchronized修饰的print成员函数;同理,要执行该函数,需要获取调用该函数的对象demoA1所持有的锁,由于该锁已经被线程A1执行过程所持有,为此线程B只能等待线程A1执行完成释放demoA1所持有的锁以后,才能得到执行的机会;但是,print函数是一个“死”循环,为此线程B会一直被阻塞着,直到JVM退出而终止。
最后,线程A2通过对象demoA2启动,并调用了被synchronized修饰的print成员函数;同理,线程要进入并执行该成员函数,需要先获取demoA2所关联锁,由于该锁(由于每个对象都会有一个与之关联的lock,即使属于同一类型的对象所持有的锁也是不同的,因此该锁不同于demoA1所关联的锁)当前并没有被任何线程使用,所以线程A2顺利的获取了demoA2的所关联的lock,并开始执行print函数。
通过以上分析,线程A1与线程A2会交替执行,线程B会因为一直不能获取到demoA1所关联的锁而被永久阻塞。因此,该函数的输出结果,“可能”会是这样:
结果
A1
A2
A1
A2
A1
A2
02、synchronized在静态上下文中的应用解析
同样地,我们先看一个代码例子,然后再详细分析一下为什么会这样。
4
public class DemoC implements Runnable{
private String msg;
public DemoC(String str){
msg = str;
}
public synchronized void print1(String str){
while(true){
try {
//模拟一个耗时操作,调用sleep函数
//会释放占用的CPU,但是不会释放对象锁
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
public synchronized static void print2(String str){
while(true){
try {
//模拟一个耗时操作,调用sleep函数
//会释放占用的CPU,但是不会释放对象锁
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
}
}
public void run(){
print1(msg);
}
}
5
public class StaticSyncTest {
public static void main(String[] args){
DemoC demoC1 = new DemoC("C1");
Thread c1 = new Thread(demoC1);
c1.start();
demoC1.print2("C2");
}
}
老铁们可以先想一想该程序的运行结果,接着,我们再一起往下看。
该程序首先定义了一个对象引用demoC1,然后启动线程c1,该线程会执行被synchronized修饰的print1函数,进入print1函数前,会首先判断是否持有对象引用demoC1的锁,由于该锁尚未被其他线程所占用,为此该线程会被执行;接着demoC1会调用print2函数,该函数同时被synchronized与static关键字修饰,按照我们上文所提及的方法,要进入该函数,需要持有DemoC.class对象所关联lock,由于该对象尚未被其他线程所持有,为此该函数也将被执行。
通过以上分析,线程c1与demoC1.print2都会被执行。因此,该函数的输出结果,“可能”会是这样:
结果
C1
C2
C1
C2
C2
C1
C2
03、合理利用synchronized实现线程同步
通过以上分析,我们就基本掌握了synchronized在非静态上下文与静态上下文中是如何在线程间同步发挥作用的。
推而广之,我们一般有三种利用synchronized方法实现线程同步的方法:
第一种:修饰方法
6
//synchronized修饰成员函数
//该函数能被执行,则意味则持有了调用
//该函数的对象引用所持有的lock
//多个函数如果存在同步关系,则可以在这
//些函数前面都增加synchronize修饰
public synchronized void subroutine1(){
}
第二种:修饰一般对象或this对象
7
//多个函数如果存在同步关系,则可以
//用synchronize修饰this对象
public synchronized void subroutine1(){
synchronized(this){
}
}
private DemoA demoA = new DemoA("A");
//多个函数如果存在同步关系,则可以
//用synchronize修饰demoA对象
public synchronized void subroutine2(){
synchronized(demoA){
}
}
第三种,修饰轻量级对象
8
private byte[] lock = new byte[1];
//多个函数如果存在同步关系,则可以
//用synchronize修饰lock对象
//lock是一个轻量级对象
public void subroutine2(){
synchronized(lock){
}
}
为了适应高并发性能以及快速响应的要求,synchronized不同的写法对程序响应的快慢和对CPU等资源利用率是不同的;对比以上方式,从程序运行性能与执行效率来看,从高到低依次排序为:第三种方式 > 第二种方式 > 第一种方式。
划重点
关键字synchronized锁定的是对象,而不是函数或代码。函数或代码区块被声明或修饰为synchronized,并非意味着它同一时刻只能由一个线程执行。
因为锁定的对象不一样,加锁与解锁都需要此对象资源,为此锁定的对象资源越小,性能开销就越小,采用byte作为锁对象最为经济。
当synchronized用于修饰对象引用时,则取得的lock将被交给该引用所指向的对象。
当调用一个synchronized static 函数时,获得的lock将与定义该函数的class相关联,而不是与调用函数的那个对象相关联。当synchronized修饰的是A.class时,获得的lock也是与上述相同,即与class相关联。