多线程-synchronized(你以为你真的懂Synchronized)
一、为什么要使用synchronized关键字?
1、使用synchronized关键字的原因:在并发编程问题中存在着共享数据,在多线程操作共享数据时,要保证同一时刻只有一个线程在执行某个方法或某个代码块; synchronized既保证了原子性,又保证了可见性,所以可以使用synchronized来达到目的。
- 原子性:此操作不可分割,不能分为多步操作,也就是在此操作过程中不能有其他线程介入,过来的线程只能等当前线程释放互斥锁,然后获得此锁才能执行该方法或代码块(synchronized 作用 的方法或代码块内容要么不执行,要执行就保证全部执行完毕);
- 可见性:一个线程对共享数据修改后,要保证其他线程能够知道;这会涉及到下一篇我们要讲的Volatile也可以做到可见性;
2、这里解释一下共享变量不可见原因:一般Java的变量都存储在主内存中,每个线程都有自己独立的工作内存,当他们工作时会将共享变量拷贝一份进行操作,并不是直接操作主内存,这里就不是一个原子操作,所以存在中间状态,而多线程之间又会出现交叉执行,就会出现没能及时的对主内存的共享数据进行更新,这就出现了可见性的问题;
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接在主内存中读写;
- 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成;
3、线程执行synchronized代码块的流程:
- 线程获得互斥锁
- 清空工作内存
- 从主内存拷贝共享变量最新的值到工作内存成为副本
- 执行代码
- 将修改后的副本的值刷新回主内存中
- 线程释放锁
二、synchronized的原子性和可见性
举一个例子,多线程对同一个共享变量进行操作,如果有两个线程操作变量 int a = 0; 不加synchronized关键字时,线程1和线程2可以同时操作a变量,恰好它们同时读取了a = 0,然后分别拷贝了一份到自己线程的工作区域;线程1进行a++; 把它修改为1,线程2也做同样的操作也修改成了1,本来已经加了两次,结果应该是2,但是现在的结果还是1。这时我们加上synchronized关键字,如果线程1获取了锁资源,线程2必须等线程1执行结束释放了锁,他才能够获取这个互斥锁,进行操作;
1、第一个小程序:
package com.example.demo.threaddemo.juc_002; /** * 创建100个线程对共享变量a进行操作,比较使用synchronized前后的区别 * 不加synchronized很大程度上会出现脏读,加上synchronized就可以避免这个问题 */ public class SynchronizedTest00 { //定义一个共享变量 private int a = 0; //定义一个Object对象 Object o = new Object(); //对变量进行add操作 public void add(){ synchronized (o) { //这里锁定的是o这个对象 a++; System.out.println(Thread.currentThread().getName() + " a = " + a); } } public static void main(String[] args) { SynchronizedTest00 t = new SynchronizedTest00(); for (int i = 0; i < 100; i++) { new Thread(()-> t.add(),"THREAD"+i).start(); } } }
2、当然每次加锁都要new Object() ,实在太麻烦了,你还可以使用 synchronized (this):
package com.example.demo.threaddemo.juc_002; /** * 创建100个线程对共享变量a进行操作,比较使用synchronized前后的区别 * 不加synchronized很大程度上会出现脏读,加上synchronized就可以避免这个问题 */ public class SynchronizedTest00 { //定义一个共享变量 private int a = 0; //对变量进行add操作 public void add(){ synchronized (this) { //这里锁定的是当前对象 a++; System.out.println(Thread.currentThread().getName() + " a = " + a); } } public static void main(String[] args) { SynchronizedTest00 t = new SynchronizedTest00(); for (int i = 0; i < 100; i++) { new Thread(()-> t.add(),"THREAD"+i).start(); } } }
3、synchronized还可以直接修饰方法(这里用synchronized直接修饰方法显然锁的粒度变大了,某些情况会影响执行效率):
package com.example.demo.threaddemo.juc_002; /** * 创建100个线程对共享变量a进行操作,比较使用synchronized前后的区别 * 不加synchronized很大程度上会出现脏读,加上synchronized就可以避免这个问题 */ public class SynchronizedTest00 { //定义一个共享变量 private int a = 0; //对变量进行add操作 public synchronized void add(){ a++; System.out.println(Thread.currentThread().getName() + " a = " + a); } public static void main(String[] args) { SynchronizedTest00 t = new SynchronizedTest00(); for (int i = 0; i < 100; i++) { new Thread(()-> t.add(),"THREAD"+i).start(); } } }
三、同步方法和非同步方法可以同时调用吗?
就是我有一个同步方法m1,在调用m1的过程中是否可以调用m2(非同步方法) ,用脚趾头想想肯定是可以的因为你调用m2的时候又不需要获得锁;
package com.example.demo.threaddemo.juc_008; import java.util.concurrent.TimeUnit; /** * 测试同步方法和非同步方法是否可以同时调用 */ public class Synchronized_01 { /** * 同步方法 */ public synchronized void m1(){ System.out.println("m1 is begining -----------"); System.out.println(Thread.currentThread().getName()+ "------"+"m1 Thread"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m1 is end ----------------"); } /** * 非同步方法 */ public void m2(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "------"+"m2 Thread"); } public static void main(String[] args) { Synchronized_01 t =new Synchronized_01(); new Thread(t::m1 , "Thread m1").start(); new Thread(t::m2 ,"Thread m2").start(); } }
//运行结果:
m1 is begining -----------
Thread m1------m1 Thread
Thread m2------m2 Thread
m1 is end ----------------
四、synchronized的可重入性
可重入性的意思就是在一个同步方法中调用另一个同步方法;现在有两个同步方法m1、m2 而且加的是同一把锁; 你在方法m1中调用m2,首先获得这把锁开始执行m1方法,当你要执行m2时也要获得这把锁,如果这时锁不可重入,那就进入了死锁的状态; 如果可重入,允许你申请,没毛病 问题不大这就叫可重入锁;
package com.example.demo.threaddemo.juc_008; import java.util.concurrent.TimeUnit; /** * 测试锁的可重入性 */ public class Synchronized_02 { /** * 同步方法m1 */ public synchronized void m1(){ System.out.println("m1 is begining -----------"); System.out.println(Thread.currentThread().getName()+ "------"+"m1 Thread"); m2(); System.out.println("m1 is end ----------------"); } /** * 同步方法m2 */ public synchronized void m2(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "------"+"m2 Thread"); } public static void main(String[] args) { Synchronized_02 t =new Synchronized_02(); new Thread(t::m1 , "Thread m1").start(); } }
五、出现锁异常(并发处理一定要注意)
异常锁 程序中出现异常 默认锁会被释放假设 web端处理程序的过程中,多个servlet共同访问一个资源,这时第一个线程出现了异常此时释放了锁,其他线程就会进入同步代码块,极大可能读取到异常时产生的数据。所以要非常小心的处理同步业务的异常;
package com.example.demo.threaddemo.juc_008; import java.util.concurrent.TimeUnit; /** * 异常锁 程序中出现异常 默认锁会被释放 * 假设 web端处理程序的过程中,多个servlet共同访问一个资源,这时第一个线程出现了异常 * 此时释放了锁,其他线程就会进入同步代码块,极大可能读取到异常时产生的数据。 */ public class Synchronized_03 { private int count = 10; public synchronized void m(){ System.out.println(Thread.currentThread().getName()+"start -----------------"); while(true) { count--; System.out.println(Thread.currentThread().getName() + "-----count:" + count); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if (count == 5) { //故意制造异常 此处出现异常,锁将被释放,想要不释放锁 ,需要进行try catch int m = 1 / 0; } } } public synchronized void mm(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":-----Thread m1 抛异常 释放锁了 我就来了----- count:"+ this.count); } public static void main(String[] args) { Synchronized_03 t =new Synchronized_03(); new Thread(t::m , "Thread m1").start(); new Thread(t::mm , "Thread m2").start(); } }
//运行结果:
Thread m1start -----------------
Thread m1-----count:9
Thread m1-----count:8
Thread m1-----count:7
Thread m1-----count:6
Thread m1-----count:5
Exception in thread "Thread m1" java.lang.ArithmeticException: / by zero
at com.example.demo.threaddemo.juc_008.Synchronized_03.m(Synchronized_03.java:25)
at java.lang.Thread.run(Thread.java:745)
Thread m2:-----Thread m1 抛异常 释放锁了 我就来了----- count:5
六、synchronized的底层实现
在很多人的认知里,都会认为synchronized是重量级的锁;
- 早期的synchronized的底层是重量级的,重量级到这个synchronized都需要向操作系统申请锁资源,这就会造成运行的效率非常的低,后来Java越来越开始处理高并发的程序,很多的程序员开始不满,觉得这个synchronized太重了,没办法需要开发新的框架;
- 后期在jdk1.6以后对synchronized进行的优化,当我们使用synchronized时Hotspot是这样实现的:
1、偏向锁:偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,第一个来访问某把锁的线程,只是在这个对象头(markword)中记录这个线程(此时偏向锁位为”1“ 锁的标志位为”01“ 从锁的标志位不难看出第一次访问的时候实际上没有给这个对象上锁,内部实现只是记录线程的id在对象头中 ),这时为偏向锁;
2、轻量级锁(也称自旋锁):偏向锁如果出现了竞争的话,就会升级为轻量级锁,这时新来的竞争的线程不会跑到等待队列中去,而是在这里自旋(虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word ,拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word , 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”),当自旋的次数达到一定程度或者是自旋的线程数达到一定程度就会进行下一次升级,升级为重量级锁;
3、重量级锁:轻量级锁膨胀为重量级锁,锁的标志位变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态,重量级锁就需要到操作系统中去申请资源。
总结一下:通过以上对synchronized内部锁升级的过程不难看出只有一个线程时,不存在竞争时,这时为偏向锁,锁的标志位为01;
当出现线程竞争时,锁升级为轻量级锁,这里有一个CAS(Compare And Swap)操作也就是自旋,如果markdown中的LockRecoed指针修改成功,说明此对象的锁,锁 的标志位改为00;
当轻量级锁的自旋次数或线程数达到一定程度,就会出现锁的膨胀,升级为重量级锁;
这里注意一点并不是CAS的效率就一定比系统锁的效率要高,这要分不同的情况,执行时间短,线程的数量少,肯定是用自旋,线程数量多,执行时间长,肯定是使用系统锁。