2019-03-19 15:39:54 qq_22054285 阅读数 1276
  • 分布式实战之SpringBoot实战实现

    本课程将介绍分布式架构下、微服务项目分布式锁的实现方式,包括数据库级别锁、基于Redis的原子操作、基于Zookeeper的实现、基于Redisson的实现等方式。 在实战完各种方式后,我也分享介绍了两个很实用的实际业务场景:“重复提交”、“CRM系统销售人员抢单”(抢单系统抢单),将之前所学到的分布式锁实战实现方式进行了充分的利用,更进一步的巩固了分布式锁的理解!

    1649 人正在学习 去看看 钟林森

在多线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区代码的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

所谓的锁,说白了就是内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。

看起来很简单,大家有没有想过,OS是怎样保证这个锁操作本身的原子性呢?举个例子,在多核环境中,两个核上的代码同时申请一个锁,两个核同时取出锁变量,同时判断说这个锁是空闲状态,然后有同时修改为上锁状态,同时返回成功。。。两个核同时获取到了锁,这种情况可能吗?废话,当然是不可能,可能的话,我们使用锁还有啥意义。但是,咦?等等,虽然我知道肯定不可能,但是你刚才说的貌似还有点道理,看来OS实现这个锁还不是看起来这么简单,还是有点道道的。

为了弄明白锁的实现原理,我们首先看看如果OS不采用任何其他手段,什么情况下会导致上锁失败?假如我们把加锁过程用如下伪码表示:
1、read lock;
2、判断lock状态;
3、如果已经加锁,失败返回;
4、把锁状态设置为上锁;
5、返回成功。
明白汇编的同学一看就明白上述每一步都能对应到一条汇编语句,所以我们可以认为每一步本身是原子的。

那么什么情况能够导致两个线程同时获取到锁呢?
1、中断:假设线程A执行完第一步,发生中断,中断返回后,OS调度线程B,线程B也来加锁并且加锁成功,这时OS调度线程A执行,线程从第二步开始执行,也加锁成功。
2、多核:当然了,想想上面举的例子,描述的就是两个核同时获取到锁的情况。

既然明白锁失败的原因,解决手段就很明确了:
先考虑单核场景:
1、既然只有中断才能把上锁过程打断,造成多线程操作失败。我先关中断不就得了,在加锁操作完成后再开中断。
2、上面这个手段太笨重了,能不能硬件做一种加锁的原子操作呢?能,大名鼎鼎的“test and set”指令就是做这个事情的。(怎么,test and set是干什么的?同学,看来你上课时不够专心啊,赶紧回头复习复习)

通过上面的手段,单核环境下,锁的实现问题得到了圆满的解决。那么多核环境呢?简单嘛,还是“test and set”不就得了,这是一条指令,原子的,不会有问题的。真的吗,单独一条指令能够保证该指令在单个核上执行过程中不会被中断打断,但是两个核同时执行这个指令呢?。。。我再想想,硬件执行时还是得从内存中读取lock,判断并设置状态到内存,貌似这个过程也不是那么原子嘛。对,多个核执行确实会存在这个问题。怎么办呢?首先我们得明白这个地方的关键点,关键点是两个核会并行操作内存而且从操作内存这个调度来看“test and set”不是原子的,需要先读内存然后再写内存,如果我们保证这个内存操作是原子的,就能保证锁的正确性了。确实,硬件提供了锁内存总线的机制,我们在锁内存总线的状态下执行test and set操作,就能保证同时只有一个核来test and set,从而避免了多核下发生的问题。

总结一下,在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等等等)。所以要想理解OS的各种同步手段,首先需要理解本文介绍的内容,这时最原点的机制,所有的OS上层同步手段都基于此。

2017-08-08 15:04:07 sinat_32393077 阅读数 254
  • 分布式实战之SpringBoot实战实现

    本课程将介绍分布式架构下、微服务项目分布式锁的实现方式,包括数据库级别锁、基于Redis的原子操作、基于Zookeeper的实现、基于Redisson的实现等方式。 在实战完各种方式后,我也分享介绍了两个很实用的实际业务场景:“重复提交”、“CRM系统销售人员抢单”(抢单系统抢单),将之前所学到的分布式锁实战实现方式进行了充分的利用,更进一步的巩固了分布式锁的理解!

    1649 人正在学习 去看看 钟林森

  在多线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区代码的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

  所谓的锁,说白了就是内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。

  看起来很简单,大家有没有想过,OS是怎样保证这个锁操作本身的原子性呢?举个例子,在多核环境中,两个核上的代码同时申请一个锁,两个核同时取出锁变量,同时判断说这个锁是空闲状态,然后有同时修改为上锁状态,同时返回成功。。。两个核同时获取到了锁,这种情况可能吗?废话,当然是不可能,可能的话,我们使用锁还有啥意义。但是,咦?等等,虽然我知道肯定不可能,但是你刚才说的貌似还有点道理,看来OS实现这个锁还不是看起来这么简单,还是有点道道的。

  为了弄明白锁的实现原理,我们首先看看如果OS不采用任何其他手段,什么情况下会导致上锁失败?假如我们把加锁过程用如下伪码表示:
  1、read lock;
  2、判断lock状态;
  3、如果已经加锁,失败返回;
  4、把锁状态设置为上锁;
  5、返回成功。
  明白汇编的同学一看就明白上述每一步都能对应到一条汇编语句,所以我们可以认为每一步本身是原子的。

  那么什么情况能够导致两个线程同时获取到锁呢?
  1、中断:假设线程A执行完第一步,发生中断,中断返回后,OS调度线程B,线程B也来加锁并且加锁成功,这时OS调度线程A执行,线程从第二步开始执行,也加锁成功。
  2、多核:当然了,想想上面举的例子,描述的就是两个核同时获取到锁的情况。

  既然明白锁失败的原因,解决手段就很明确了:
  先考虑单核场景:
  1、既然只有中断才能把上锁过程打断,造成多线程操作失败。我先关中断不就得了,在加锁操作完成后再开中断。
  2、上面这个手段太笨重了,能不能硬件做一种加锁的原子操作呢?能,大名鼎鼎的“test and set”指令就是做这个事情的。(怎么,test and set是干什么的?同学,看来你上课时不够专心啊,赶紧回头复习复习)

  通过上面的手段,单核环境下,锁的实现问题得到了圆满的解决。那么多核环境呢?简单嘛,还是“test and set”不就得了,这是一条指令,原子的,不会有问题的。真的吗,单独一条指令能够保证该指令在单个核上执行过程中不会被中断打断,但是两个核同时执行这个指令呢?。。。我再想想,硬件执行时还是得从内存中读取lock,判断并设置状态到内存,貌似这个过程也不是那么原子嘛。对,多个核执行确实会存在这个问题。怎么办呢?首先我们得明白这个地方的关键点,关键点是两个核会并行操作内存而且从操作内存这个调度来看“test and set”不是原子的,需要先读内存然后再写内存,如果我们保证这个内存操作是原子的,就能保证锁的正确性了。确实,硬件提供了锁内存总线的机制,我们在锁内存总线的状态下执行test and set操作,就能保证同时只有一个核来test and set,从而避免了多核下发生的问题。

  总结一下,在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等等等)。所以要想理解OS的各种同步手段,首先需要理解本文介绍的内容,这时最原点的机制,所有的OS上层同步手段都基于此。

向大神致敬,原文地址:http://blog.sina.com.cn/u/1978709325

2020-02-21 11:45:05 sinat_41567654 阅读数 39
  • 分布式实战之SpringBoot实战实现

    本课程将介绍分布式架构下、微服务项目分布式锁的实现方式,包括数据库级别锁、基于Redis的原子操作、基于Zookeeper的实现、基于Redisson的实现等方式。 在实战完各种方式后,我也分享介绍了两个很实用的实际业务场景:“重复提交”、“CRM系统销售人员抢单”(抢单系统抢单),将之前所学到的分布式锁实战实现方式进行了充分的利用,更进一步的巩固了分布式锁的理解!

    1649 人正在学习 去看看 钟林森

锁是并发实现的必要机制,计算机体系结构的指令集内有不同的硬件原语,可以实现锁机制。评价锁实现的效果有三个标准:

  • 互斥实现:锁的基本任务就是实现临界区的互斥访问
  • 公平性:保证每一个线程都能够有机会抢到锁
  • 性能:锁的使用将会增加时间开销,要求其对性能的影响降到最低

锁的实现有以下几种方式:

1. 控制中断

控制中断是最早的互斥解决方案之一,即在临界区关闭中断,保持线程对临界区的持续访问,该方案是为单处理器系统开发的。

  • 控制终端的方法较为简单,但是要求允许所有调用的线程执行特权级操作,然而程序往往是不被信任的,关闭中断对应用程序要求太多。
  • 其次这种方案不适用于多处理器。
  • 控制中断会导致中断丢失,可能会导致严重的系统问题。例如磁盘完成IO操作但是cpu却胡烈了这一请求。
  • 在操作系统内部可以通过控制中断的方式实现临界区互斥访问,因为在操作系统内不存在信任问题。

2.简单标志位

  • 用flag标志锁是否被使用,线程进入临界区时调用lock,检查标志位flag,检查标志是否为1,非1则置1,表明线程持有锁。结束临界区访问时,线程调用unlock,清除标志位。
  • 当已有线程A占用临界区时,另一个线程B调用lock,在while中自旋,直到线程A结束占用unlock清空标志位,线程B退出while设置标志位为1,开始访问临界区代码。
  • while循环空转被称为自旋锁(spin lock),自旋锁在单cpu中无法使用,自旋锁永远不会放弃cpu。自旋锁无法保证公平性,会导致线程饿死。自旋锁的性能开销很大。

typedef struct lock_t { int flag; } lock_t;
void init(lock_t *mutex) {
	// 0 -> lock is available, 1 -> held
	mutex->flag = 0;
}

void lock(lock_t *mutex) {
	while (mutex->flag == 1) // TEST the flag
		; // spin-wait (do nothing)
	mutex->flag = 1; // now SET it!
}
void unlock(lock_t *mutex) {
	mutex->flag = 0;
}

3. 测试并设置(test-and-set)

int TestAndSet(int *ptr, int new) {
	int old = *ptr; // fetch old value at ptr
	*ptr = new; // store ’new’ into ptr
	return old; // return the old value
}
typedef struct __lock_t {
	int flag;
} lock_t;

void init(lock_t *lock) {
	// 0 indicates that lock is available, 1 that it is held
	lock->flag = 0;
}

void lock(lock_t *lock) {
	while (TestAndSet(&lock->flag, 1) == 1)
	; // spin-wait (do nothing)
}

void unlock(lock_t *lock) {
	lock->flag = 0;
}

4. 比较并交换(compare-and-swap)

  • sparc和x86系统上提供了这一指令
  • 实际上和test-and-set的思路一致
//比较并交换指令的一般形式
int CompareAndSwap(int *ptr, int expected, int new) {
	int actual = *ptr;
	if (actual == expected)
		*ptr = new;
	return actual;
}

只需要用下列code替代lock函数即可

void lock(lock_t *lock) {
	while (CompareAndSwap(&lock->flag, 0, 1) == 1)
		; // spin
}

5.链接的加载和条件式存储(load-link and store-conditional)

  • 在MIPS架构中,链接的加载和条件式存储可以配合使用
int LoadLinked(int *ptr) {
	return *ptr;
}

int StoreConditional(int *ptr, int value) {
	if (no one has updated *ptr since the LoadLinked to this address) {
	*ptr = value;
	return 1; // success!
} else {
	return 0; // failed to update
}
}
void lock(lock_t *lock) {
	while (1) {
		while (LoadLinked(&lock->flag) == 1)
			; // spin until it’s zero
		if (StoreConditional(&lock->flag, 1) == 1)
			return; // if set-it-to-1 was a success: all done
				// otherwise: try it all over again
	}
}
void unlock(lock_t *lock) {
	lock->flag = 0;
}

6.获取并增加(fetch-and-add)

  • 不同与之前的方法,能保证所有线程都能抢到锁(标志位不断增加)
int FetchAndAdd(int *ptr) {
	int old = *ptr;
	*ptr = old + 1;
	return old;
}
typedef struct __lock_t {
	int ticket;
	int turn;
} lock_t;
void lock_init(lock_t *lock) {
	lock->ticket = 0;
	lock->turn = 0;
}
void lock(lock_t *lock) {
	int myturn = FetchAndAdd(&lock->ticket);
	while (lock->turn != myturn)
		; // spin
}
void unlock(lock_t *lock) {
	FetchAndAdd(&lock->turn);
}

必须显示提供某种控制机制,决定释放锁,以及那个线程能够抢到锁
使用队列放置不能获取锁的线程,并让出cpu

2018-03-17 14:58:39 xqhadoop 阅读数 732
  • 分布式实战之SpringBoot实战实现

    本课程将介绍分布式架构下、微服务项目分布式锁的实现方式,包括数据库级别锁、基于Redis的原子操作、基于Zookeeper的实现、基于Redisson的实现等方式。 在实战完各种方式后,我也分享介绍了两个很实用的实际业务场景:“重复提交”、“CRM系统销售人员抢单”(抢单系统抢单),将之前所学到的分布式锁实战实现方式进行了充分的利用,更进一步的巩固了分布式锁的理解!

    1649 人正在学习 去看看 钟林森

前言:前几天阿里电面被问到了关于jvm锁机制的底层原理,说实话当时只是简单的了解基础api,真正内部实现确实不懂。所以在查阅大量资源的情况下,简单记录下我学习这方面的资料。

一.操作系统级别的锁机制实现原理
我们知道jvm是属于运行在操作系统上的程序虚拟机。要想真正理解jvm是如何实现锁机制的,底层操作系统的锁机制是必须要了解的。
我们知道在多线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区资源的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区,从而保证临界区中操作数据的一致性。

锁可以理解为内存中的一个变量标识,拥有两种状态:unlock,locked.任何想要访问获得锁的线程必须执行以下指令流


load lock #将lock内存值加载进cpu
read lock state
if lock.state=="unlock"
set lock.state to "locked" #将lock状态设置会内存
return true
return false


我们假设2个线程同时想获得锁,当线程一执行完第二条指令之后,突然被中断。那么cpu去执行线程二的指令流,假设线程二成功执行以上指令并获得锁。这时cpu再切换回线程一继续执行,那么这时,线程一操作的lock是已经缓存在寄存器中的lock,该lock状态为free,所以也去获得这把锁,这也就意味着这2个线程可以同时去操作临界资源,这也就意味着该机制是没有起作用的。

1.中断机制

如果我们能将以上多个指令流变成一个不可中断的原子操作,问题不就解决了?所以我们在执行执行这些指令时,可以通过开关中断来保证锁机制指令流的原子性。中断是系统级别的指令,最好不允许用户操作,如果代码中没有正确的开关中断,容易引发系统死机。

2.CAS机制

CAS是Compare And Set的一个简称。
对于一个线程来说,从内存load线程上下文到处理器的高速缓存,这里的线程上下文包括共享变量;然后执行线程代码,这里包括共享变量的读操作和写操作,因为写操作会导致多个处理器处理的数据不一致,所以CAS上场了,它要求写操作的时候,检查要写的共享变量cache和内存是否一致,若一致则继续执行写操作,若不一致则重新load线程上下文,重新执行。

这里写图片描述

但是这一个过程实际上是分为两步:比较(compare)和写回(swap).所以可能存在临界点,需要硬件锁来保障原子性,一种典型的方法就是总线锁,在处理器连接的总线上发出一个Lock信号,阻塞其他处理器操作内存。

这里写图片描述

我们通过源码可以看到AtomicInteger类compareAndSet通过原子操作实现了CAS操作。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

private volatile int value;//保证可见性
public final int get() {
        return value;
   }
public final int incrementAndGet() {
  for (;;) {
        //获取当前值
        int current = get();
        //设置期望值
        int next = current + 1;
//调用Native方法compareAndSet,执行CAS操作
   if (compareAndSet(current, next))
 //成功才返回值,否则重复执行循环(自旋)
       return next;
      }
}

value是一个volatile变量,不同线程对这个变量进行操作时具有可见性,修改与写入操作都会存入主存中,并通知其他cpu中该变量缓存行无效,保证了每次读取都是最新的值。

但是该CAS也存在一些问题,但是会导致一个ABA。如线程一和线程二都取出了主存中的数据为A,这时候线程二将数据修改为B,然后又修改为A,此时线程一再次去进行compareAndSet的时候仍然能够匹配成功,而实际对的数据已经发生了变更了,只不过发生了2次变化将对应的值修改为原始的数据了,并不代表实际数据没有发生变化。这个可以通过引入对应修改的版本号来确定修改的操作。

3.内存锁机制
以上是在单核cpu下执行没有问题,但是在多核cpu下呢?开关中断是针对当前cpu,所以也没法保证原子性。我们主要到以上锁机制是需要读写内存操作的。如果我们将只运行一个线程同时操作内存,那不就行了。我们就想到cpu与内存进行通信的总线。即我们锁住总线即可。

在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;其他所有的同步机制都是基于这些基本的锁机制。经过以上内容的介绍,相信对操作系统中锁机制有了全新认识。

4.三种机制总结
1) 中断机制其实是一种悲观锁(只允许一个进程操作临界资源,其他进程处于阻塞状态
2)CAS是一种乐观锁,即先不要担心其他线程会修改共享变量,在适当的时候检查其他线程是否已经修改共享变量,如果未修改则说明可以进行操作。CAS一般适用于计数;多线程编程也适用。
3)内存锁机制是应用非常多的,比如上述CAS机制部分也是利用了内存锁机制

2018-04-03 10:19:00 dgsuggv886715 阅读数 8
  • 分布式实战之SpringBoot实战实现

    本课程将介绍分布式架构下、微服务项目分布式锁的实现方式,包括数据库级别锁、基于Redis的原子操作、基于Zookeeper的实现、基于Redisson的实现等方式。 在实战完各种方式后,我也分享介绍了两个很实用的实际业务场景:“重复提交”、“CRM系统销售人员抢单”(抢单系统抢单),将之前所学到的分布式锁实战实现方式进行了充分的利用,更进一步的巩固了分布式锁的理解!

    1649 人正在学习 去看看 钟林森
在写代码时经常会使用到锁,很多的锁是需要操作系统支持的,系统层面的锁是如何实现的呢?

 其实系统的锁实现和平时我们自己实现锁的过程是一样的:

1.读取锁标志位read lock

2.判断标志位锁状态lock status

3.上锁(如果已经上锁则返回失败)

4.返回成功

5.执行程序

6.释放锁

  

但是这个过程不得不考虑的是当有两个线程同时拿到标志位都是0怎么办,很简单,直接把1,2,3作为一个原子操作就行了,也就是操作系统层面的关闭中断来保证123是一个原子操作。这样的话就不会有两个线程同时get lock了,但是问题又来了这是在单核上没有问题,可是现在计算机都是多核该如何处理呢,这就涉及到计算机的硬件总线支持了。

总线

总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。在计算机系统中,各个部件之间传送信息的公共通路叫总线,微型计算机是以总线结构来连接各个功能部件的。(以上摘抄百度)

多核计算机上提供了锁总线的支持来实现锁,然后再配合关闭中断的原子操作就能保证锁的正确性了。

最后,由于计算机的硬件机制和OS的配合就能实现锁,基于锁就可以实现各种各样的同步功能了。

转载于:https://www.cnblogs.com/foolever/p/8706760.html

没有更多推荐了,返回首页