JAVA并发编程之synchronized
synchronized介绍与使用
synchronized即是通过对代码块/普通方法/静态方法加锁以保证多线程访问共享变量区域时“串行化”。下面介绍synchronized修饰不同对象的使用与区别。
普通方法
synchronized修饰普通方***将整个对象锁住。使得对象内别的被synchronized修饰的普通方法也无法被其他线程访问。
下面展示几种情况的例子。
1.
public class synchronizedTest implements Runnable { //共享资源 static int i =0; /** * synchronized 修饰实例方法 */ public synchronized void increase(){ i++; } @Override public void run(){ for (int j =0 ; j<10000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { synchronizedTest test = new synchronizedTest(); Thread t1 = new Thread(test); Thread t2 = new Thread(test); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
上述代码展示了两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁。
2.
public class SynchronizedTest { public synchronized void method1() { System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public synchronized void method2() { System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(test::method1).start(); new Thread(test::method2).start(); } }
上述代码展示了两个线程对一个对象的两个synchronized方法进行调用。最终线程1在调用method1时会将其对象锁住,导致线程2无法调用该对象的method2.
3.
public class SynchronizedTest { public synchronized void method1() { System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2() { System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(test::method1).start(); new Thread(test::method2).start(); } }
上述代码展示了一个线程获取了该对象的锁之后,其他线程来访问其他非synchronized实例方法。最终线程1的访问不会与线程2的访问互斥。
4.
public class SynchronizedTest { public synchronized void method1() { System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public synchronized void method2() { System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test1 = new SynchronizedTest(); final SynchronizedTest test2 = new SynchronizedTest(); new Thread(test1::method1).start(); new Thread(test2::method2).start(); } }
上述代码展示多个线程调用于不同的对象的方法,显然是不会互斥的。
静态方法
synchronized对静态方法加锁,锁住的将会是类,即类的所有对象。
public class synchronizedTest implements Runnable { //共享资源 static int i =0; /** * synchronized 修饰实例方法 */ public static synchronized void increase(){ i++; } @Override public void run(){ for (int j =0 ; j<10000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new synchronizedTest()); Thread t2 = new Thread(new synchronizedTest()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }
上述代码两个线程通过两个对象,访问同一个加了锁的静态方***产生互斥。因为对静态方法的锁是类锁,具体而言是对类的Class对象加锁。
代码块
可以从被synchronized修饰的普通方法中看到,若某些情况下方法体很大,而方法体内同时存在一些比较耗时的操作,但需要同步的代码又只有一小部分,那么对整个方法的同步可能得不偿失。对代码块加锁,可以指定被锁的是对象还是类,即代码块的锁即可以是对象锁,也可以是类锁。使用方法如下所示:
//this即当前对象,因此下面实例对象锁。 synchronized(this){ for(int j=0;j<1000000;j++){ i++; } } //class对象锁,即类的运行时对象锁,因此下面是类锁。 synchronized(AccountingSync.class){ for(int j=0;j<1000000;j++){ i++; } } ## 总结 对synchronized的不同锁的对比,如下所示: 
synchronized底层原理
被synchronized修饰的方法/静态方法/代码块是怎么实现锁的机制的?
同步代码块的底层实现
我们知道代码块可以指定是对象锁,也可以指定是类锁。但是两者实际都是一致的。当指定是类锁的时候,则锁类的Class对象。实现对对象加锁的操作,主要靠两个指令:monitorenter、monitorexit。每个对象会有一个计数器,用于标记该对象现在是否被锁住,当计数器为0意味着没有被锁,当大于等于1意味着被锁。
当一个线程企图进入一个同步代码块时,首先调用对象的monitorenter,当计数器为0时则可以进入同步代码块,否则等待。当线程成功进入后,则计数器+1,意味着线程取得了对象锁。当线程退出代码块,则调用monitorexit,使得计数器-1。若计数器为0意味着不再有线程获得该对象的锁,其他线程可以进入。
同步方法的底层实现
一个方***有一个标志位ACC_SYNCHROCHIZED用于标识该方法是否为同步方法。当线程调用某方法时,首先通过判断ACC_SYNCHROCHIZED标志位,若不是同步方法,正常执行;若是,则调用monitorenter指令,企图获得该方法的对象或类的Class对象的锁,流程与为对象加锁的流程一致。因此同步方法最终依然是使用monitorenter与monitorexit指令为对象加锁与释放锁的操作。
synchronized的改进
最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。这种方式效率低下,因为每次只能通过一个线程。假设有一共享区域大多数情况下是并行读的操作,只有少数情况下会对共享数据进行写操作。那么synchronized就显得有点低效了。
悲观锁/乐观锁
synchronized是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。
还有一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。而如果出现了冲突时,再采取别的策略。显然当出现了并行读次数大于并行写时,乐观锁策略会更加合适。下面介绍乐观锁的实现方式。
CAS操作
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试。
因此,可以基于CAS操作对并行读频率大于并行写频率的代码块使用无锁(乐观锁)——不对共享区加悲观锁,大多数情况下可以多线程并行访问共享区,当需要写时再执行CAS操作。
CAS的问题
- ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C,这就可以甄别是否有改动了。 - 自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。可以通过设置最大的自旋时间解决。
参考
https://blog.csdn.net/u011521203/article/details/80186218
https://blog.csdn.net/zjy15203167987/article/details/82531772
https://www.cnblogs.com/wangwudi/p/12302668.html
https://www.zhihu.com/question/57794716?sort=created
https://blog.csdn.net/carson_ho/article/details/82992269