Synchronized
MoMo Lv5

作用

保证同一时刻最多只有 1 个线程执行 被Synchronized修饰的方法 / 代码

其他线程 必须等待当前线程执行完该方法 / 代码块后才能执行该方法 / 代码块

应用场景

保证线程安全,解决多线程中的并发同步问题(实现的是阻塞型并发),具体场景如下:

  1. 修饰 实例方法 / 代码块时,(同步)保护的是同一个对象方法的调用 & 当前实例对象
  2. 修饰 静态方法 / 代码块时,(同步)保护的是 静态方法的调用 & class 类对象

原理

  1. 依赖 JVM 实现同步
  2. 底层通过一个监视器对象(monitor)完成, wait()notify() 等方法也依赖于 monitor 对象

监视器锁(monitor)的本质 依赖于 底层操作系统的互斥锁(Mutex Lock)实现

具体使用

Synchronized 用于 修饰 代码块、类的实例方法 & 静态方法

锁的类型 & 等级

由于Synchronized 会修饰 代码块、类的实例方法 & 静态方法,故分为不同锁的类型

类型 定义 作用 使用
对象锁 含 synchronized 方法/代码块的类的实例对象 控制同步方法之间的同步
  • 使用前必须先获得对象的锁
  • 线程进入 synchronized 方法获取该对象的锁,若此对象的锁被其他调用者占用,则线程将被阻塞
  • Java的所有对象都含有1个互斥锁,锁由JVM自动获取和释放
  • synchronized方法运行期间如出现异常会终止,JVM 会自动释放锁
  • 当用 synchronized 加锁的代码块时,方法结束时由 JVM 自动释放锁
方法锁
(也属于对象锁)
使用 synchronized 修饰的方法 控制同步方法之间的同步 synchronized方法控制对类成员变量的访问
  • 每个类实例对应一把锁
  • 每个synchronized方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞
  • 方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态
  • 所有声明为synchronized的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突
类锁 使用 synchronized 修饰静态方法 / 代码块(类锁实际是锁 Class 对象,但具体表现为:锁静态方法 / 代码块) 控制静态方法 / 静态变量之间的同步
  • 一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁
  • 一个类不论被实例化多少次,其静态方法 & 变量在内存中都只有1份

之间的区别

image

使用规则

image

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* 对象锁
*/
public class Test{
// 对象锁:形式1(方法锁)
public synchronized void Method1(){
System.out.println("我是对象锁也是方法锁");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}

}

// 对象锁:形式2(代码块形式)
public void Method2(){
synchronized (this){
System.out.println("我是对象锁");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}

}


/**
* 方法锁(即对象锁中的形式1)
*/
public synchronized void Method1(){
System.out.println("我是对象锁也是方法锁");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}

}

/**
* 类锁
*/
public class Test{
 // 类锁:形式1 :锁静态方法
public static synchronized void Method1(){
System.out.println("我是类锁一号");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}

}

// 类锁:形式2 :锁静态代码块
public void Method2(){
synchronized (Test.class){
System.out.println("我是类锁二号");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}

}

}

特别注意

Synchronized修饰方法时存在缺陷:若修饰 1 个大的方法,将会大大影响效率

若使用Synchronized关键字修饰线程类的run(),由于run()在线程的整个生命期内一直在运行,因此将导致它对本类任何Synchronized方法的调用都永远不会成功

解决方案: 使用 Synchronized关键字声明代码块
该解决方案灵活性高:可针对任意代码块 & 任意指定上锁的对象

1
2
3
4
synchronized(syncObject) {
// 访问或修改被锁保护的共享状态
// 上述方法 必须 获得对象 syncObject(类实例或类)的锁
}

特点

image

注:原子性、可见性、有序性的定义

image

其他控制并发 / 线程同步方式

Lock、ReentrantLock

  • 简介
类型 定义 作用 特点
Lock 接口
(Java5 后引入)
提供了比使用 synchronized 更广泛的锁定操作,功能更强大
  • 允许更灵活地使用锁(获取 & 释放顺序,不同的作用范围)
  • 支持多个相关的 Condition 对象

Condition 对象:

  • 定义:接口
  • 作用:解决 Object 的监视器方法(wait, notify 和 notifyAll) 的局限,分解成不同对象,以便精确地控制这些对象与线程的交互
  • 锁:每个锁对象可以绑定多个 Condition 对象
  • 常用的 Lock 实现类:ReentrantLock,ReadWriteLock(实现类 ReentrantReadWriteLock)
  • Lock 替代了 synchronized 方法和语句的使用,Lock 的 Condition 对象替代了 synchronized 的 Object 监视器方法使用
ReentrantLock Lock 的实现类
一个可重入的互斥锁 Lock
(重入性:自己可以再次获取自己的内部锁)
提供了比使用 synchronized 更广泛的锁定操作,功能更强大

ReentrantLock 相对于 synchronized 多了 3 个高级功能:

  1. 公平锁:保证线程获取锁时的公平顺序(先来后到的线程获取锁)
    • synchronlzed=非公平锁:在等待队列里随机挑选1个线程执行任务,即后进来的线程也可和前边等待锁的线程竞争锁资源
    • 非公平领执行效率高:因为公平锁要实现顺序执行=维护1个有序队列,每次获取锁时需先是不是线程队列的第1个,才会让线程获得锁
    • 非公平锁产生饥馕现象:即有些线程(优先级较低)可能永远也无法获取CPU的执行权,优先级高的线程会不断执行
  2. 等待可中断:在持有锁的线程长时间不释放锁时,等待的线程可选择放弃等待
  3. ReentrantLock的lock机制有2种:

    • 忽略中断锁:在持有锁的线程长时间不释故锁时,忽略 [待等待线程] 响应 [不再等待锁而中断等待] 的请求
    • 响应中断锁:在持有锁的线程长时间不释放锁时,响应 [待等待线程] 希望 [不再等待锁而希望中断等待] 的请求,即 [让其中断线程] 而完全放弃等待锁

    例子说明

    • B2个线程去竞争锁,A线程得到了锁,B线程等待;由于等待时间太久,B线程不想等了,希望中断等待去处理其他任务,此时ReentranlLock就提供了上述2种机制来处 瑾改情况:
    • 选择 [忽略中断锁机制]:即忽路B线程不再等待锁而希望中断 的请求,继续B线程等待
    • 选择 [响应中断]:即 响应B线程不再等待锁而希望中断的请求, 让B线程中断等待,从而可以做其他任务
  4. 绑定多个 Condition 对象:通过多个 newCondition 可获得多个 Condition 对象,可以简单的通过 await(),signal() 实现复杂的线程同步功能
  • 区别
类型 功能 & 性能 关于锁 应用场景 其他
锁机制 锁的范围 锁的获取 锁的释放 产生死锁的可能性 执行操作
synchronized 实现线程切换需从用户态转换到内核态,从而效率低 悲观锁
(线程获得的是独占锁:其他线程只能依靠阻塞来等待线程释放锁)
synchronized 修饰的范围(方法 & 块)
强制要求获得锁 自动释放
(JVM)
一般情况下优先使用 容易产生死锁
(一旦产生死锁问题,线程将一直阻塞 结果=请求成功or一直阻塞)
托管给 JVM 执行
Lock
(ReentrantLock)
功能更强大 & 更好的性能
  • 公平锁
  • 等待可中断
  • 可绑定多个 Condition 对象
乐观锁
(每次不加锁&假设无冲突而去完成某项操作;若因为冲突失数就重试,直到成功为止)
可跨方法,灵活性更大
(Lock 的使用基于方法调用)
不强制要求获得锁 手动释放
(最好在 finally 块中释放)
需公平锁、等待可中断、绑定多个 Condition 对象时
  • 某个线程在等待1个锁的控制权时需中断等待
  • 需分开处理wait-notfy,ReentrantLock里面的Condltion应用,能够控制notify哪个线程
  • 具有公平锁功能,每个到来的线程都将排队等候
  • (比如实现生产者和消费者的多线程唤醒机制)
    不容易产生死锁
    • 提供了可中断的锁获取方式,能确保下述运行的处理
    • 每次公平锁的线程等待可中断
    • 能绑定多个 Condition,可实现更精确的 wait-notify 组合
    • 有自公平机制,能均衡线程的执行
    Java 写的控制锁的代码

    CAS

    定义

    Compare And Swap,即 比较 并 交换,是一种解决并发操作的乐观锁
    其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值

    synchronized锁住的代码块:同一时刻只能由一个线程访问,属于悲观锁

    原理

    当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。

    与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

    简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就需要重新读取,再次尝试修改就好了。

    CAS 的操作参数

    • 内存位置(A)
    • 预期原值(B)
    • 预期新值(C)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    // 使用CAS解决并发的原理:
    // 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
    // 2. 通过死循环,以不断尝试尝试更新的方式实现并发

    // 伪代码如下
    public boolean compareAndSwap(long memoryA, int oldB, int newC){
    if(memoryA.get() == oldB){
    memoryA.set(newC);
    return true;
    }
    return false;
    }

    优点

    资源耗费少:相对于synchronized,省去了挂起线程、恢复线程的开销

    但若迟迟得不到更新,死循环对CPU资源也是一种浪费

    具体实现方式

    • 使用 CAS 有个“先检查后执行”的操作
    • 而这种操作在 Java 中是典型的不安全的操作,所以 CAS在实际中是C++通过调用 CPU 指令实现的
    • 具体过程
      1. CAS 在 Java 中的体现为 Unsafe 类
      2. Unsafe 类会通过 C++直接获取到属性的内存地址
      3. 接下来 CAS 由 C++的 Atomic::cmpxchg 系列方法实现

    应用:

    AtomicInteger

    非阻塞算法就是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。非阻塞算法就需要借助CAS操作来实现,这也是CAS的一个主要应用方向。

    现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些CPU提供的特殊指令代替了锁定。

    原文链接:https://blog.csdn.net/cy973071263/article/details/104422594

    对 i++ 与 i–,通过compareAndSet & 一个死循环实现

    compareAndSet函数内部 = 通过jni操作CAS指令。直到 CAS 操作成功跳出循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    private volatile int value;
    /**
    * Gets the current value.
    *
    * @return the current value
    */
    public final int get() {
    return value;
    }
    /**
    * Atomically increments by one the current value.
    *
    * @return the previous value
    */
    public final int getAndIncrement() {
    for (;;) {
    int current = get();
    int next = current + 1;
    if (compareAndSet(current, next))
    return current;
    }
    }

    /**
    * Atomically decrements by one the current value.
    *
    * @return the previous value
    */
    public final int getAndDecrement() {
    for (;;) {
    int current = get();
    int next = current - 1;
    if (compareAndSet(current, next))
    return current;
    }
    }

    CAS存在的问题

    CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:循环时间太长、只能保证一个共享变量原子操作、ABA问题。

    循环时间太长

    如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。

    只能保证一个共享变量原子操作

    看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

    ABA问题

    CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

    从Java1.5 开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。其实就类似于引入了版本概念,给每一个数据都有一个它唯一的版本号,通关检查版本号来判断数据是否被修改。

    CAS造成Cache一致性流量过大

    现在几乎所有的锁都是可重入的,即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用(使本地调用不是那么及时),因此偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。轻量级锁就是基于CAS操作的

    Powered by Hexo & Theme Keep
    Unique Visitor Page View