写点什么

Java 多线程:锁(一)

作者:Java-fenn
  • 2022 年 9 月 13 日
    湖南
  • 本文字数:8574 字

    阅读完需:约 28 分钟

CAS 比较与交换的意思


举个例子,内存有个值是 3,如果用 Java 通过多线程去访问这个数,每个线程都要把这个值 +1。


之前是需要加锁,即 synchronized 关键字来控制。但是 JUC 的包出现后,有了 CAS 操作,可以不需要加锁来处理,流程是:


第一个线程:把 3 拿过来,线程本地区域做计算加 1,然后把 4 写回去。


第二个线程:也把 3 这个数拿过来,线程本地区域做计算加 1 后,在回写回去的时候,会做一次比较,如果原来的值还是 3,那么说明这个值之前没有被打扰过,就可以把 4 写回去,如果这个值变了,假设变为了 4,那么说明这个值已经被其他线程修改过了,那么第二个线程需要重新执行一次,即把最新的 4 拿过来继续计算,回写回去的时候,继续做比较,如果内存中的值依然是 4,说明没有其他线程处理过,第二个线程就可以把 5 回写回去了。


流程图如下


CAS 会出现一个 ABA 的问题,即在一个线程回写值的时候,其他线程其实动过那个原始值,只不过其他线程操作后这个值依然是原始值。


如何来解决 ABA 问题呢?


我们可以通过 版本号 或者 时间戳 来控制,比如数据原始的版本是 1.0,处理后,我们把这个数据的版本改成变成 2.0 版本, 时间戳来控制也一样。


以 Java 为例, AtomicStampedReference 这个类,它内部不仅维护了对象值,还维护了一个时间戳。当 AtomicStampedReference 对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当 AtomicStampedReference 设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。


代码示例


package git.snippets.juc;


import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicStampedReference;


/**


  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2022/9/10

  • @since*/public class ABATest {public static void main(String[] args) throws InterruptedException {abaCorrect();}

  • private static void abaCorrect() throws InterruptedException {AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10, 0);Thread threadA = new Thread(() -> {try {int[] stamp = new int[1];Integer value = ref.get(stamp); //同时获取时间戳和数据,防止获取到数据和版本不是一致的

  • }


}CAS 的底层调用了汇编的 LOCK_IF_MP 方法:


lock cmpxchg 虽然 cmpxchg 指令不是原子的,但是加了 lock 指令后,则 cmpxhg 被上锁,不允许被打断。 在单核 CPU 中,无须加 lock ,在多核 CPU 中,必须加 lock ,可以参考 stackoverflow 上的这个回答: is-x86-cmpxchg-atomic-if-so-why-does-it-need-lock


使用 CAS 好处


jdk 早期是重量级别锁 ,通过 0x80 中断 进行用户态和内核态转换,所以效率比较低,有了 CAS 操作,大大提升了效率。


锁升级过程如下:


偏向锁 synchronized 代码段多数时间是一个线程在运行,谁先来,这个就偏向谁,用当前线程标记一下。


轻量级锁(自旋锁,无锁)偏向锁撤销,然后竞争,每个线程在自己线程栈中存一个 LR(lock record)锁记录


偏向锁和轻量级锁都是 用户空间 完成的,重量级锁需要向操作系统申请。


两个线程争抢的方式将 lock record 的指针,指针指向哪个线程的 LR,哪个线程就拿到锁,另外的线程用 CAS 的方式继续竞争


重量级锁 JVM 的 ObjectMonitor 去操作系统申请。


如果发生异常, synchronized 会自动释放锁,


示例代码如下:


package git.snippets.juc;


import java.util.concurrent.TimeUnit;


public class ExceptionCauseUnLock {/*volatile */ boolean stop = false;


public static void main(String[] args) {    ExceptionCauseUnLock t = new ExceptionCauseUnLock();    new Thread(t::m, "t1").start();    try {        TimeUnit.SECONDS.sleep(4);    } catch (InterruptedException e) {        e.printStackTrace();    }    if (t.stop) {        int m = 1 / 0;    }}
synchronized void m() { while (!stop) { stop = true; }}
复制代码


}其中


int m = 1 / 0;会抛出异常,锁会自动释放。


锁重入 synchronized 是可重入锁, 可重入次数必须记录,因为解锁需要对应可重入次数的记录。


偏向锁:记录在线程栈中,每重入一次,LR 加 1,备份原来的 markword


轻量级锁:类似偏向锁


重量级锁:记录在 ObjectMonitor 的一个字段中


自旋锁什么时候升级为重量级锁?


有线程超过十次自旋


-XX:PreBlockSpin(jdk1.6 之前)


自旋的线程超过 CPU 核数一半


jdk1.6 以后,JVM 自己控制


为什么有偏向锁启动和偏向锁未启动?


未启动:普通对象 001 已启动:匿名偏向 101


为什么有自旋锁还需要重量级锁?


因为自旋会占用 CPU 时间,消耗 CPU 资源,如果自旋的线程多,CPU 资源会被消耗,所以会升级成重量级锁(队列)例如: ObjectMonitor 里面的 WaitSet ,重量级锁会把线程都丢到 WaitSet 中冻结, 不需要消耗 CPU 资源


偏向锁是否一定比自旋锁效率高?


明确知道多线程的情况下,不一定。 因为偏向锁在多线程情况下,会涉及到锁撤销,这个时候直接使用自旋锁,JVM 启动过程,会有很多线程竞争,比如启动的时候,肯定是多线程的,所以默认情况,启动时候不打开偏向锁,过一段时间再打开,JVM 有一个参数可以配置: BiasedLockingStartupDelay 默认是 4s


synchronized 锁定对象 package git.snippets.juc;


/**


  • synchronized 锁定对象

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2021/4/15

  • @since*/public class SynchronizedObject implements Runnable {static SynchronizedObject instance = new SynchronizedObject();final Object object = new Object();static volatile int i = 0;

  • @Overridepublic void run() {for (int j = 0; j < 1000000; j++) {// 任何线程要执行下面的代码,必须先拿到 object 的锁 synchronized (object) {i++;}}}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}锁定方法锁定静态方法相当于锁定当前类


package git.snippets.juc;


/**


  • synchronized 锁定静态方法,相当于锁定当前类

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2021/4/15

  • @since*/public class SynchronizedStatic implements Runnable {static SynchronizedStatic instance = new SynchronizedStatic();static volatile int i = 0;

  • @Overridepublic void run() {increase();}

  • // 相当于 synchronized(SynchronizedStatic.class)synchronized static void increase() {for (int j = 0; j < 1000000; j++) {i++;}}

  • public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}锁定非静态方法相当于锁定该对象的实例或 synchronized(this)


package git.snippets.juc;


/**


  • synchronized 锁定方法

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2021/4/15

  • @since*/public class SynchronizedMethod implements Runnable {static SynchronizedMethod instance = new SynchronizedMethod();static volatile int i = 0;

  • @Overridepublic void run() {increase();}void increase() {for (int j = 0; j < 1000000; j++) {synchronized (this) {i++;}}}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}脏读 package git.snippets.juc;


import java.util.concurrent.TimeUnit;


/**


  • 模拟脏读

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2021/4/15

  • @since*/public class DirtyRead {String name;double balance;

  • public static void main(String[] args) {DirtyRead a = new DirtyRead();Thread thread = new Thread(() -> a.set("zhangsan", 100.0));

  • }

  • public synchronized void set(String name, double balance) {this.name = name;

  • }

  • // 如果 get 方法不加 synchronized 关键字,就会出现脏读情况 public /synchronized/ double getBalance(String name) {return this.balance;}}其中的 getBalance 方法,如果不加 synchronized ,就会产生脏读的问题。


可重入锁一个同步方法可以调用另外一个同步方法,


一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁(可重入锁)


子类 synchronized,如果调用父类的 synchronize 方法:super.method(),如果不可重入,直接就会死锁。


package git.snippets.juc;


import java.io.IOException;


/**


  • 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @since*/public class SynchronizedReentry implements Runnable {

  • public static void main(String[] args) throws IOException {SynchronizedReentry myRun = new SynchronizedReentry();Thread thread = new Thread(myRun, "t1");Thread thread2 = new Thread(myRun, "t2");thread.start();thread2.start();System.in.read();

  • }

  • synchronized void m1(String content) {System.out.println(this);System.out.println("m1 get content is " + content);m2(content);}

  • synchronized void m2(String content) {System.out.println(this);System.out.println("m2 get content is " + content);

  • }

  • @Overridepublic void run() {m1(Thread.currentThread().getName());}}程序在执行过程中,如果出现异常,默认情况锁会被释放 ,所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个 web app 处理过程中,多个 Servlet 线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。


示例代码


package git.snippets.juc;


import java.io.IOException;import java.util.concurrent.TimeUnit;


/**


  • 程序在执行过程中,如果出现异常,默认情况锁会被释放

  • 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。

  • 比如,在一个 web app 处理过程中,多个 servlet 线程共同访问同一个资源,这时如果异常处理不合适,

  • 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。

  • 因此要非常小心的处理同步业务逻辑中的异常*/public class SynchronizedException implements Runnable {int count = 0;

  • public static void main(String[] args) throws IOException {SynchronizedException myRun = new SynchronizedException();Thread thread = new Thread(myRun, "t1");Thread thread2 = new Thread(myRun, "t2");thread.start();thread2.start();System.in.read();

  • }

  • @Overridepublic void run() {synchronized (this) {while (true) {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("current thread is " + Thread.currentThread().getName() + " count is " + count);if (count == 5) {count++;int m = 1 / 0;}count++;}}}

  • synchronized void m1(String content) {System.out.println(this);System.out.println("m1 get content is " + content);m2(content);}

  • synchronized void m2(String content) {System.out.println(this);System.out.println("m2 get content is " + content);

  • }


}synchronized 的底层实现在早期的 JDK 使用的是操作系统级别的重量级锁


后来的改进锁升级的概念:


synchronized (Object)


markword 记录这个线程 ID (使用偏向锁)


如果线程争用:升级为 自旋锁


10 次自旋以后,升级为重量级锁 - OS


所以,如果


执行时间短(加锁代码),线程数少,用自旋。


执行时间长,线程数多,用系统锁。


注:synchronized 不能锁定 String 常量,Integer,Long 等基础类型代码示例如下


package git.snippets.juc;


/**


  • synchronized 不能锁定 String 常量,Integer,Long 等基础类型

  • <p>

  • 不要以字符串常量作为锁定对象

  • 在下面的例子中,m1 和 m2 其实锁定的是同一个对象

  • 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,

  • 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,

  • 因为你的程序和你用到的类库不经意间使用了同一把锁

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @since*/public class SynchronizedBasicType implements Runnable {public static Integer i = 0;static SynchronizedBasicType instance = new SynchronizedBasicType();static final String lock = "this is a lock";static final String lock1 = "this is a lock";

  • public static void main(String[] args) throws InterruptedException {m();Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}

  • public static void m() throws InterruptedException {Thread m1 = new Thread(new Runnable() {@Overridepublic void run() {/synchronized (this)/synchronized (lock) {System.out.println("locked ...");try {Thread.sleep(10000);} catch (InterruptedException e) {}System.out.println("unlocked ...");}}});m1.start();Thread.sleep(1000);Thread m2 = new Thread(new Runnable() {@Overridepublic void run() {/synchronized (this)/synchronized (lock1) {System.out.println("locked lock1 ...");System.out.println("unlocked lock1 ...");}}});m2.start();m1.join();m2.join();}

  • @Overridepublic void run() {for (int j = 0; j < 10000000; j++) {synchronized (i) {i++;}}}}锁定某对象 o,如果 o 的属性发生改变,不影响锁的使用; 但是如果 o 指向另外一个对象,则锁定的对象发生改变, 会影响锁的使用,所以应该避免将锁定对象的引用变成另外的对象。


package git.snippets.juc;


import java.util.concurrent.TimeUnit;


/**


  • 锁定某对象 o,如果 o 的属性发生改变,不影响锁的使用

  • 但是如果 o 变成另外一个对象,则锁定的对象发生改变

  • 应该避免将锁定对象的引用变成另外的对象*/public class SyncSameObject {Object object = new Object();

  • public static void main(String[] args) {SyncSameObject t = new SyncSameObject();new Thread(t::m).start();Thread t2 = new Thread(t::m, "t2");//锁对象发生改变,所以 t2 线程得以执行,如果注释掉这句话,线程 2 将永远得不到执行机会 t.object = new Object();t2.start();}

  • void m() {synchronized (object) {while (true) {try {TimeUnit.SECONDS.sleep(2);System.out.println("current thread is " + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}}}以上代码,如果不执行 t.object=new Object() 这句,m2 线程将永远得不到执行。


死锁两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,这就是死锁现象


死锁产生的原因主要有如下几点


系统的资源竞争


程序在执行过程中申请和释放资源的顺序不当


死锁产生的必要条件


互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。


不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。


请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。


循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。


模拟死锁代码


/**


  • 模拟死锁*/public class DeadLock implements Runnable {int flag = 1;static Object o1 = new Object();static Object o2 = new Object();

  • public static void main(String[] args) {DeadLock lock = new DeadLock();DeadLock lock2 = new DeadLock();lock.flag = 1;lock2.flag = 0;Thread t1 = new Thread(lock);Thread t2 = new Thread(lock2);t1.start();t2.start();

  • }

  • @Overridepublic void run() {System.out.println("flag = " + flag);if (flag == 1) {synchronized (o2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1) {System.out.println("1");}}}if (flag == 0) {synchronized (o1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("0");}}}}}如何避免死锁?


1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。


2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。


3、增加时限,比如使用 Lock 类中的 tryLock 方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。


volatile 保持线程之间的可见性(不保证操作的原子性),依赖 MESI 协议


防止指令重排序,CPU 的 load fence 和 store fence 原语支持


CPU 原来执行指令一步一步执行,现在是流水线执行,编译以后可能会产生指令的重排序,这样可以提高性能


关于 volatile 不保证原子性的代码示例


package git.snippets.juc;


/**


  • Volatile 保持线程之间的可见性(不保证操作的原子性)

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2021/4/19

  • @since*/public class VolatileNOTAtomic {volatile static Data data;

  • public static void main(String[] args) {Thread writer = new Thread(() -> {for (int i = 0; i < 10000; i++) {data = new Data(i, i);}});

  • }

  • public static class Data {int a;int b;

  • }}volatile 并不能保证多个线程共同修改 running 变量时所带来的不一致问题,也就是说 volatile 不能替代 synchronized ,


示例代码如下:


package git.snippets.juc;


import java.util.ArrayList;import java.util.List;


/**


  • volatile 并不能保证多个线程共同修改变量时所带来的不一致问题,也就是说 volatile 不能替代 synchronized

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @date 2021/4/19

  • @since*/public class VolatileCanNotReplaceSynchronized {volatile int count = 0;int count2 = 0;

  • public static void main(String[] args) {VolatileCanNotReplaceSynchronized t = new VolatileCanNotReplaceSynchronized();List<Thread> threads = new ArrayList<>();List<Thread> threads2 = new ArrayList<>();for (int i = 0; i < 20; i++) {threads.add(new Thread(t::m));threads2.add(new Thread(t::m2));}threads.forEach(item -> item.start());threads2.forEach(item -> item.start());threads.forEach(item -> {try {item.join();} catch (InterruptedException e) {e.printStackTrace();}});threads2.forEach(item -> {try {item.join();} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.count);System.out.println(t.count2);}

  • void m() {for (int i = 0; i < 1000; i++) {count++;}}

  • synchronized void m2() {for (int i = 0; i < 1000; i++) {count2++;}}}DCL 为什么一定要加 volatile?什么是 DCL,请参考设计模式学习笔记中的单例模式说明。


在 New 对象的时候,编译完实际上是分了三步


对象申请内存,成员变量会被赋初始值


成员变量设为真实值


成员变量赋给对象


指令重排序可能会导致 2 和 3 进行指令重排,导致下一个线程拿到一个半初始化的对象,导致单例被破坏。所以 DCL 必须加 volitile


此外,被 volatile 关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被 volatile 关键字修饰了


示例代码如下


package git.snippets.juc;


import java.util.concurrent.TimeUnit;


/**


  • 被 volatile 关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被 volatile 关键字修饰了

  • @author <a href="mailto:410486047@qq.com">Grey</a>

  • @since 1.8*/public class VolatileRef {volatile M tag = new M();

  • public static void main(String[] args) {VolatileRef t = new VolatileRef();new Thread(t::m, "t1").start();try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}t.tag.n.x.stop = new Boolean(true);}

  • void m() {while (!tag.n.x.stop) {}}}


class M {N n = new N();}


class N {X x = new X();}


class X {public Boolean stop = new Boolean(false);}说明本文涉及到的所有代码和图例

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
Java 多线程:锁(一)_Java_Java-fenn_InfoQ写作社区