当前位置:   article > 正文

【JAVA】#详细介绍!!! synchronized 加锁 详解(1)!_java 加锁

java 加锁

本文分以下几点来介绍synchronized(根据JDK1.8)

1. 介绍synchronized

2. synchronized 为什么能保证线程安全

3. synchronized 的 用法

4. synchronized 的锁特性

目录

1. 介绍synchronized

2. synchronized的用法

2.1 synchronized修饰指定代码块

2.2 synchronized修饰实例方法、

2.3 synchronized修饰static修饰的静态方法

3. synchronized具体是如何实现线程安全(底层实现)

4. synchronized的锁特性



1. 介绍synchronized

synchronized是解决多线程执行下,访问资源同步性问题的关键字;

我们都知道多线程情况下针对同一个资源进行写访问会导致线程不安全问题;

那么synchronized关键字则是JVM给开发者提供的针对针对指定对象加锁以保证线程安全的关键字

synchronized是依赖监视器锁(monitor)来实现加锁的

在JDK1.6之前,监视器锁(monitor)是直接使用底层操作系统的mutex lock 来实现的,那么每次线程进行加锁解锁操作都得依赖操作系统调度,此时操作系统调度线程是内核态操作,大量的用户态内核态操作切换,则会导致系统资源的浪费,并且内核态操作时间效率也很低下。

所以在JDK1.6之前使用synchronized进行加锁操作,那么这个锁必然是重量级锁,效率较低

从JDK1.6开始,针对synchronized进行了一系列强有力的优化,synchronized加锁不直接依赖使用操作系统底层的互斥锁(mutex lock)来进行加锁的,而是根据一系列情况(锁竞争的强弱等)来升级锁的强度。只有在锁竞争很激烈且线程每次占用锁时间较长的时候才会把锁升级为直接依赖互斥锁的重量级锁。这样很好的提高了线程的并发效率并且大大减少了内核态操作,提高了线程执行效率

大致升级优化过程如下(下篇文章详解):

无锁->偏向锁->轻量级锁->重量级锁

其中包括:偏向锁,锁消除,锁粗化,自旋锁,自适应自旋锁等一系列优化手段,大大提高了锁操作的效率(详情请看下篇文章)

2. synchronized的用法

synchronized的使用得基于某个锁对象进行加锁操作,在一个线程中针对某个锁对象进行加锁后,在该线程没对加锁对象解锁前,其他线程不能使用该锁对象进行加锁操作

举个例子:

  1. public class Test {
  2. //手动指定一个不变得Object对象,专门用于某个synchronized加锁
  3. private static final Object lock = new Object();
  4. public static void showThread (Thread t){
  5. //对锁对象进行加锁
  6. synchronized (lock){
  7. while(true){
  8. System.out.println(t.getName()+"正在执行");
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }
  17. public static void main(String[] args) {
  18. //创建两个线程并发执行加锁代码块
  19. Thread t1 = new Thread(() -> {
  20. showThread(Thread.currentThread());
  21. },"t1");
  22. Thread t2 = new Thread(() -> {
  23. showThread(Thread.currentThread());
  24. },"t2");
  25. t1.start();
  26. t2.start();
  27. }
  28. }

结果:

上诉代码创建了两个线程(t1,t2)去同时执行同一个锁对象的加锁代码块,发现当t1线程先抢到锁(t1线程先使用指定锁对象进行了加锁操作去执行代码),并且在锁对象没解锁的情况下,t2线程使用相同锁对象并不能进入加锁代码块执行代码

当然必须得是同一个锁对象进行加锁,不同的锁对象之间不会发生线程互斥执行的反应,但是多线程针对不同锁对象加锁去访问相同的数据,加锁也就等于不加锁,虽然加锁了但是保证不了线程安全,违背了加锁的初心 

2.1 synchronized修饰指定代码块

  1. class CountAdd {
  2. private final Object lock = new Object();
  3. public int count;
  4. CountAdd(int count){
  5. this.count = count;
  6. }
  7. //方法1.使用自定义的对象锁lock
  8. public void add(){
  9. synchronized (lock){
  10. count++;
  11. }
  12. }
  13. //方法2.使用对象的引用作为对象锁
  14. //这里的this是对象的引用,synchronized锁对象可以是任何Object类对象
  15. public void add1(){
  16. synchronized (this){
  17. count++;
  18. }
  19. }
  20. }
  21. public class Demo1 {
  22. public static void main(String[] args) throws InterruptedException {
  23. CountAdd countAdd = new CountAdd(0);
  24. Thread t1 = new Thread(()->{
  25. for (int i = 0; i < 5000; i++) {
  26. countAdd.add();
  27. }
  28. });
  29. Thread t2 = new Thread(() -> {
  30. for (int i = 0; i < 5000; i++) {
  31. countAdd.add();
  32. }
  33. });
  34. t1.start();
  35. t2.start();
  36. Thread.sleep(1000);
  37. System.out.println(countAdd.count);
  38. }
  39. }

上述代码 定义了两个线程同时对count进行自增操作,结果count=10000(线程安全);

synchronized使用:

方法1:使用自定义的锁对象lock,来控制线程进行synchronized加锁解锁操作

方法2:直接使用对象的引用this来作为锁对象控制线程加锁解锁

这里 用使用lock和this效率一样,但是如果有两个加锁代码块,且两者并不相关,那么此时就可以定义两个锁对象来分别控制并发保证线程安全

此时都用this也能保证线程安全,但是这两者的加锁代码块进行了绑定,本来多线程分别执行这两个代码块,本来就是线程安全的,由于你们的锁对象相同,依旧会串行执行,使得效率变低

注意:还是上面说的:多个线程访问修改同一个数据,进行加锁能保证线程安全的前提是锁对象相同,锁对象相同时使得线程执行加锁的代码块是串行执行的

如果多个加锁代码块并发执行也时线程安全,那么此时用一个锁对象使得两个代码块串行执行就是多次一举了,此时我们自己给定锁对象显然更好

2.2 synchronized修饰实例方法

  1. synchronized public void add(){
  2. count++;
  3. }

给整个add方法加锁,那么此时synchronized的作用域就是整个方法,进入该方法前加锁,执行完方法解锁。锁对象为this(调用该方法的对象)

这里的效果和上面synchronized(this)那个方案相同

2.3 synchronized修饰static修饰的静态方法

  1. synchronized public static void add(){
  2. count++;
  3. }

给static静态方法加锁,那么等同于给整个类对象加锁

我们都知道static修饰的变量和方法不属于任何实例对象,时属于类的成员

那么给static方法加锁,那么执行时会对整个类进行加锁操作,多线程执行下,该类的其他加锁操作执行全部受影响

锁对象:类对象

3. synchronized具体是如何实现线程安全(底层实现)

观察某加锁的代码执行的汇编码

 前面说了synchronized是基于monitor(监视器锁)来实现的,此时控制加锁和解锁的指令如图所示为:monitorenter(进入监视器锁),monitorexit(结束监视器锁)

加锁执行的代码块就是monitorenter和monitorexit中间的部分

注意:细心的读者会会发现在正常执行完monitorexit之后后面又有一条monitorexit指令

原因:避免加锁代码在执行过程中发生异常没指令monitorexit指令就直接退出了加锁代码块,此时编译器会产生一个异常处理器,目的是代码出现异常时执行monitorexit,确保该线程正常解锁,避免其他线程因为该线程没解锁就出锁进而等待的异常

根据Java虚拟机(HotShop),每个Object对象都内置了一个monitor对象。

当执行到monitorenter指定时,线程会试图获取锁对象的monitor的所有权

获取成功:此时线程执行加锁代码,直到执行到monitorexit解锁

获取失败:如果当前锁对象的monitor已经被其他线程占有,此时当前线程就获取失败,那么当前线程就进行锁等待,等待到线程释放了锁(执行了monitorexit),在尝试去获取锁对象的monitor所有权。

如果当前线程获取到锁对象的monitor所有权时,那么锁的计数器会进行+1,表示该锁对象已经被使用,其他线程不能对该锁对象进行加锁

注意:可重入锁可以使锁对象对相同线程进行多次加锁(下篇介绍)

图解:

 解锁操作同理:执行monitorexit时该对象锁的计数器会进行-1操作,表时该线程本次已经解锁

4. synchronized的锁特性

synchronized 开始为乐观锁;当冲突过高且线程持有锁时间较长 时转化为悲观锁

synchronized时轻量级锁(大概率使用自旋锁实现),也是重量级锁,是可重入锁,

是不公平锁

synchronized不是读写锁 

每种锁的基本详情请看:http://t.csdn.cn/TTzAD(常见锁策略)


本篇文章讲到这就大致结束了,下篇会详细将synchronized锁的优化详情,感兴趣的友友可以关注一下!!!


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/292110
推荐阅读
相关标签
  

闽ICP备14008679号