Android进阶之彻底理解Synchronized关键字

移动开发 Android
synchronized是Java中的一个关键字,在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。

[[417605]]

本文转载自微信公众号「Android开发编程」,作者Android开发编程。转载本文请联系Android开发编程公众号。

一、Synchronized详解

synchronized是Java中的一个关键字,在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。

二、Synchronized的特性

  1. 原子性。synchronized可以确保多线程下对共享资源的互斥访问,被synchronized作用的代码可以实现原子性。
  2. 可见性。synchronized保证对共享资源的修改能够及时被看见。在Java内存模型中,对一个共享变量操作后进行释放锁即进行unlock操作前,必须将修改同步到主内存中。如果对一个共享资源进行加锁即lock操作之前,必须将工作内存中共享变量的值清空(因为每一个线程获取的共享变量都是主存中共享变量的一个副本,如果不进行清空,就会发生数据不一致,即当前线程中的共享变量与主存中的共享变量不一致),在使用此共享变量时,就需要从主存中重新加载此共享变量以获得该共享变量最新的值。
  3. 有序性。synchronized可以有效解决重排序问题,即一个unlock解锁操作必定先行发生于后面线程对同一个锁的lock操作,这样就会保证主内存值的共享变量永远是最新的。

三、Synchronized的使用

在应用Sychronized关键字时需要把握如下注意点:

一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;

每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁;

synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。

对象锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象锁)

代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁

  1. public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); // 创建2把锁 Object block1 = new Object(); Object block2 = new Object(); @Override public void run() { // 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行 synchronized (block1) { System.out.println("block1锁,我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block1锁,"+Thread.currentThread().getName() + "结束"); } synchronized (block2) { System.out.println("block2锁,我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block2锁,"+Thread.currentThread().getName() + "结束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } } 复制代码 

输出结果:

  1. block1锁,我是线程Thread-0 block1锁,Thread-0结束 block2锁,我是线程Thread-0  // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把 block1锁,我是线程Thread-1 block2锁,Thread-0结束 block1锁,Thread-1结束 block2锁,我是线程Thread-1 block2锁,Thread-1结束  

方法锁形式:synchronized修饰普通方法,锁对象默认为this

  1. public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { method(); } public synchronized void method() { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }  

类锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)

synchronize修饰静态方法(类的class对象)

  1. public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把 public static synchronized void method() { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }  

输出结果:

我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束 复制代码

synchronized修改实例方法

  1. /** * synchronized修饰实例方法,当线程拿到锁,其他线程无法拿到该对象的锁,那么其他线程就无法访问该对象的其他同步方法 * 但是可以访问该对象的其他非synchronized方法 * 锁住的是类的实例对象 */ public class synchronizedDemo1 implements Runnable { //模拟一个共享数据 private static int total=0; //同步方法,每个线程获取到锁之后,执行5次累加操作 public synchronized void increase(){ for (int i = 1; i < 6; i++) { System.out.println(Thread.currentThread().getName()+"执行累加操作..."+"第"+i+"次累加"); try { total=total+1; Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } //实例对象的另一个同步方法 public synchronized void declare(){ System.out.println(Thread.currentThread().getName()+"执行total-1"); total--; System.out.println(Thread.currentThread().getName()+"执行total-1完成"); } //普通实例方法 public void simpleMethod(){ System.out.println(Thread.currentThread().getName()+ " ----实例对象的普通方法---"); } @Override public void run() { //线程执行体 System.out.println(Thread.currentThread().getName()+"准备执行累加,还没获取到锁"); //执行普通方法 simpleMethod(); //调用同步方法执行累加操作 increase(); //执行完increase同步方法后,会释放掉锁,然后线程1和线程2会再一次进行锁的竞争,谁先竞争得到锁,谁就先执行declare同步方法 System.out.println(Thread.currentThread().getName()+"完成累加操作"); //调用实例对象的另一个同步方法 System.out.println(Thread.currentThread().getName()+"准备执行total-1"); declare(); } public static void main(String[] args) throws InterruptedException { synchronizedDemo1 syn = new synchronizedDemo1(); Thread thread1 = new Thread(syn,"线程1"); Thread thread2 = new Thread(syn,"线程2"); thread1.start(); thread2.start(); } }  

输出结果:

线程1准备执行累加,还没获取到锁 线程2准备执行累加,还没获取到锁 线程2 ----实例对象的普通方法--- 线程2执行累加操作...第1次累加 //线程2通过与线程1的竞争率先拿到了锁,进入increase同步方法 线程2执行累加操作...第2次累加 线程1 ----实例对象的普通方法--- //从这里可看出,在线程2访问同步方法时,线程1是可以访问非同步方法的,但是不可以访问另外一个同步方法 线程2执行累加操作...第3次累加 线程2执行累加操作...第4次累加 线程2执行累加操作...第5次累加 线程2完成累加操作 //线程2执行累加后会释放掉锁 线程2准备执行total-1 线程1执行累加操作...第1次累加 //然后线程1拿到锁后进入increase同步方法执行累加 线程1执行累加操作...第2次累加 线程1执行累加操作...第3次累加 线程1执行累加操作...第4次累加 线程1执行累加操作...第5次累加 线程1完成累加操作 //线程1完成累加操作也会释放掉锁,然后线程1和线程2会再进行一次锁竞争 线程1准备执行total-1 线程2执行total-1 //线程2通过竞争率先拿到锁进入declear方法执行total-1操作 线程2执行total-1完成 线程1执行total-1 线程1执行total-1完成 复制代码

四、Synchronized实现原理

加锁和释放锁

synchronized同步是通过monitorenter和monitorexit等指令实现的,会让对象在执行,使其锁计数器加1或者减1。

monitorenter指令:每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,会发生如下3种情况之一:

  • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  • 若其他线程已经持有了对象监视器,则当前线程进入阻塞状态,直到对象监视器的进入数为0,重新尝试获取monitor的所有权。

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

对象、对象监视器、同步队列以及执行线程状态之间的关系:

该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

可重入原理:加锁次数计数器

从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。

Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

保证可见性的原理:内存模型和happens-before规则

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。

public class MonitorDemo { private int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 } // 6 } 复制代码

happens-before关系如图所示:

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。

总结

  • synchronized同步语句块的实现使?的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置, monitorexit指令则指明同步代码块的结束位置。
  • synchronized修饰的?法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该?法是?个同步?法。

不过两者的本质都是对对象监视器 monitor 的获取。

使用Synchronized有哪些要注意的?

  • 锁对象不能为空,因为锁的信息都保存在对象头里;
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错;
  • 避免死锁;
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,因为代码量少,避免出错。

 

责任编辑:武晓燕 来源: Android开发编程
相关推荐

2019-12-20 15:19:41

Synchroinze线程安全

2024-03-15 15:12:27

关键字底层代码

2021-03-10 15:59:39

JavaSynchronize并发编程

2022-01-26 00:03:00

关键字线程JVM

2017-05-27 20:59:30

Java多线程synchronize

2021-01-12 09:22:18

Synchronize线程开发技术

2022-06-29 08:05:25

Volatile关键字类型

2009-08-12 13:37:01

Java synchr

2021-09-04 07:29:57

Android

2011-07-14 23:14:42

C++static

2019-09-04 14:14:52

Java编程数据

2009-06-29 18:26:11

Java多线程Synchronize同步类

2009-12-18 11:37:54

Ruby关键字yiel

2023-11-10 09:29:30

MySQLExplain

2024-03-15 11:52:03

C++关键字编程

2023-10-04 00:04:00

C++extern

2011-03-09 14:36:44

synchronizevolatile

2023-05-15 09:39:10

Java监视器锁

2011-06-27 15:08:15

SEO

2009-08-21 14:58:56

C# this关键字
点赞
收藏

51CTO技术栈公众号