Java多线程编程深入理解synchronized关键字
1. 对Synchronized的认识
1.1 为什么需要使用Synchronized?
在并发编程中会存在多个线程操作同一个资源的情况,此时原本在单线程下正常运行的程序可能会在多线程情况下发生一些难以预料的错误;
这里我举一个例子:初始化一个计数变量,开启10个线程,然后每个线程对这个计数变量进行10000次的累加,按照单线程的常规操作,最后这个程序的执行结果应该是10*10000 = 100000,我们可以通过下面的程序进行论证:
public class SynchronizedDemo { private static int count = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 10000; j++) { count++; } }).start(); } Thread.sleep(100); System.out.println(count); } }
运行结果:每次都不一样,但是没有一次是100000的;
之所以是这样的,是因为有多个线程同时对count进行了数据操作,比如说A线程在count=1000时候读取了数据,在count++的过程中,B线程同样读取了count=1000,并且也对count++进行了操作,那这个时候两个线程的结果都是1001,覆盖掉了,所以这次修改就时一次无效的修改。
一般这个时候使用synchronized加锁就能解决这个问题;
public class SynchronizedDemo { private static int count = 0; public static void main(String[] args) throws InterruptedException { // Object o = new Object(); for (int i = 0; i < 10; i++) { new Thread(() -> { // synchronized (o){ synchronized (SynchronizedDemo.class) { for (int j = 0; j < 10000; j++) { count++; } } }).start(); } Thread.sleep(100); System.out.println(count); } }
在并发编程中存在线程安全问题主要的原因是:存在共享数据,然后多个线程操作共享的数据;
synchronized 关键字能够解决多个线程之间访问同一个资源的同步性问题,能够保证被synchronized修饰的方法或者代码块在任意时刻只能被一个线程使用;同时synchronized可以保证一个线程变化的可见性;
2. 常见的几种加锁场景
2.1 具体表现形式
一个对象里面如果有多个 synchronized
方法,某一时刻内,只要有一个线程去调用其中一个 synchronized 方法了,其他线程就只能等待。
换句话说就是:某一个时刻内,只能有唯一的一个线程去访问这些 synchronized 方法,此时加锁的是当前的对象 this,被锁定后,其它的线程都不能进入到当前的对象的其他的 synchronized方法。
若此时,在对象中添加一个普通方法后会发现普通方法与加了同步锁的方法无关;
若换成两个对象后,不是同一把锁,情况也会发生改变;
若都换成静态同步方法之后,情况又发生了变化(此时锁的是方法区的模板类);
所有的非静态同步方法用的都是同一把锁————实例对象本身;
synchronized 实现同步的基础:Java中的每一个对象可以作为锁。
具体表现为以下 3 种形式:
- 对于普通同步方法,锁的是当前实例对象。
- 对于静态同步方法,锁的是当前类的 Class 模板。
- 对于同步方法块,锁的是 synchronized 括号里面配置的对象。
2.2 锁的几种不同应用场景
-
标准访问:两个同步方法;
// 资源类 class ShareSrc { public synchronized void methodOne() { System.out.println("------One"); } public synchronized void methodTwo() { System.out.println("------Two"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src.methodTwo(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
此时结果:
------One
------Two在同一时间内,只有一个线程能够操作资源类对象,锁的是对象;
-
在方法一中先暂停4秒钟:
// 资源类 class ShareSrc { public synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public synchronized void methodTwo() { System.out.println("------Two"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src.methodTwo(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
此时结果:
------One
------Two这种情景跟方法一对比来学习,同样是一个资源类的对象在一个时刻只能被一个线程使用,所以方法一先得到锁,然后就只能等到方法一执行完后释放锁,方法二才能获得锁;
-
新增一个普通方法,跟一个同步方法进行对比:
// 资源类 class ShareSrc { public synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public void methodThree() { System.out.println("------Three"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src.methodThree(); } catch (Exception e) { e.printStackTrace(); } }, "Three").start(); } }
此时,在同步方法中设置一个限制,先暂停几秒,否则是看不到效果的;
此时结果:
------Three
------One之所以会优先输出普通方法的结果,就能够说明,普通方法不用等到同步方法执行完毕后才被执行,也体现出来了普通方法存在线程安全问题,不用等待当前线程释放资源,另外的线程就可以抢占资源;
-
实例化两个资源类的对象:(方法一延时4秒)
// 资源类 class ShareSrc { public synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public synchronized void methodTwo() { System.out.println("------Two"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); ShareSrc src2 = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src2.methodTwo(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
运行结果:
------Two
------One此时锁加在方法上,也就是加在了对象上,使用两个不同的对象,锁住的完全也不是同一个对象,所以也不存在方法二要等待方法一执行完释放锁才能被执行;
-
两个静态同步方法:
// 资源类 class ShareSrc { public static synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public static synchronized void methodTwo() { System.out.println("------Two"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src.methodTwo(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
运行结果:
------One
------Two此时,锁加在了静态方法上,锁住的是同一个资源类的class对象,需要创建,获得这个类的对象之前都要先获得这个类的锁;
-
两个同步静态方法,两个资源类对象:
// 资源类 class ShareSrc { public static synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public static synchronized void methodTwo() { System.out.println("------Two"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); ShareSrc src2 = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src2.methodTwo(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
运行结果:
------One
------Two此时,两个静态方法,两个资源类,此时锁加在了class类对象上,所以不管new多少个对象都要先获得锁,然后才能创建自己的对象,可以跟第5种对比学习;
-
一个普通方法,一个静态同步方法,一个资源类:
// 资源类 class ShareSrc { public static synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public synchronized void methodThree() { System.out.println("------Three"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src.methodThree(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
运行结果:
------Three
------One此时是两个不同的锁,普通同步方法的锁是加在了实例对象上,而静态同步方法的锁是加在了class类上,所以这个两个方法的执行是互不影响的,因为静态方法延时了,所以会后执行结束;
-
一个普通同步方法,一静态同步方法,两个资源类:
// 资源类 class ShareSrc { public static synchronized void methodOne() { try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();} System.out.println("------One"); } public synchronized void methodThree() { System.out.println("------Three"); } } // 八锁问题 public class Lock_8 { public static void main(String[] args) { ShareSrc src = new ShareSrc(); ShareSrc src2 = new ShareSrc(); new Thread(() -> { try { src.methodOne(); } catch (Exception e) { e.printStackTrace(); } }, "One").start(); new Thread(() -> { try { src2.methodThree(); } catch (Exception e) { e.printStackTrace(); } }, "Two").start(); } }
运行结果:
------Three
------One此时,同样是两种不同的锁,所以执行不会互相影响,对比方法7学习;
2.3 小结
通过上面的几种案例,相信大家对锁的具体对象也有了一个充分的认识,下面我们来总结归纳一下:
分类 | 资源类 | 被锁的对象 |
---|---|---|
两个同步方法 | 一个资源类 | 实例对象,方法执行需要等待锁 |
一个同步方法 | 一个资源类 | 实例对象,方法执行不受影响 |
一个同步,一个普通方法 | 一个资源类 | 实例对象,普通方法不受锁约束 |
两个同步方法 | 两个资源类 | 实例对象,实例不同,锁也不同 |
两个静态同步方法 | 一个资源类 | class类对象,方法执行需要等待 |
两个静态同步方法 | 两个资源类 | class类对象(只有一个模板),方法执行需要等待 |
一个同步,一个静态同步 | 一个资源类 | 一个实例对象,一个class类对象,互不影响 |
一个同步,一个静态同步 | 两个资源类 | 一个实例对象,一个class类对象,互不影响 |
三种使用方式:
- 修饰实例方法,对当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
- 修饰静态方法,对当前类对象加锁,进入同步代码前要获得当前类对象的锁;当给当前类对象加锁的时候会作用于这个类的所有实例,静态成员不属于某一个实例,而是这个类的成员,此时加锁在类对象指的是,锁加在了Class模板类上,所有的对象实例化都是通过这一个模板生成的,此时把这个模板锁住了,在实例化新对象的时候就需要先获得锁;
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁,
synchronized (this)
代码块的锁是加在了实例对象上,synchronized (class)
代码块的锁是加在了Class类对象上;
3. 关于Synchronized的底层认识
3.1 Synchronized同步语句块
-
编写一个使用Synchronized的同步方法:
public class SynchronizedTest { public void method() { synchronized (this) { System.out.println("demoj"); } } }
然后通过jdk自带的javap命令查看SynchronizedTest类相关的字节码信息;首先切换到类的对应目录执行javac命令,然后再对生成的字节码文件执行
javap -c -s -v -l SynchronizedTest.class
命令; -
从下面的字节码中我们可以看到:
Synchronized 同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中monitorenter 执行指向同步代码块的开始位置,monitorexit 指令则指向同步代码块的结束位置。当执行 monitorenter 执行时,线程试图获取锁也就是获取monitor (monitor对象存在每个Java对象的对象头中,Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就阻塞等待,知道锁被另一个线程释放为止。
3.2 Synchronized修饰同步方法
-
编写程序使用synchronized修饰同步方法:
public class SynchronizedTest2 { public synchronized void method() { System.out.println("synchronied 方法"); } }
-
synchronized 修饰的方法并没有 monitorenter 和 monitorexit 指令,取而代之的是
ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用;