字节第二次三面

一面

私有继承

保护继承就是把基类除了私有变成保护状态。
私有继承就是把基类除了私有变成私有状态。

注意一点,在类内部才能使用保护权限,到外面是不能使用的(创建一个对象)。

接口

抽象类,纯虚函数。

自旋锁

先介绍一下AtomicCAS


AtomicC++11支持的,它提供的方法具有原子性。
这些方法是不可再分的,获取这些变量的值时,永远获得修改前的值或修改后的值,不会获得修改过程中的中间数值。
方法:
注意——指令是有执行顺序的,接下来的方法有些可以指定内存顺序。
在这里插入图片描述
is_lock_free:指示对象是否无锁,无锁对象在访问时不会导致其他线程被阻塞。
storevoid store (T val, memory_order sync = memory_order_seq_cst)
修改值
loadT load (memory_order sync = memory_order_seq_cst)
读取值
exchangeT exchange (T val, memory_order sync = memory_order_seq_cst)
替换,并返回原来的值
compare_exchange_weakcompare_exchange_strong
都是CAS操作,参数会要求在这里传入期待的数值和新的数值,它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。


自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
实现:

class SpinLock{
public:
    SpinLock():flag_(false){}
    void lock(){
        bool expect = false;
        while(!flag_.compare_exchange_weak(expect,true)){
            expect=false;//这个原因是:失败的时候expect是未知的。
        }
    }
    void unlock(){
        flag_.store(false);
    }
private:
    std::atomic<bool>flag_;
};

自旋锁的优点:
1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)。


问题:
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2、上面实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
可重入锁和不可重入锁:
当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。可重入锁也是递归锁。
为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

二面

C++多线程

并发与并行

并发:如果多个队列可以交替使用某台咖啡机,则这一行为就是并发的。
并行:如果存在多台咖啡机可以被多个队列交替使用,则就是并行。

创建线程
#include <iostream>
#include <thread> // ①

using namespace std; // ②

void hello() { // ③
  cout << "Hello World from new thread." << endl;
}

int main() {
  thread t(hello); // 普通线程创建
  thread t([] {
    cout << "Hello World from lambda thread." << endl;
  });//lambda函数创建
  //也可以传参:
  thread t(hello, "https://paul.pub");//
  t.join(); // ⑤

  return 0;
}
join和detach

join 等待线程完成其执行
detach 允许线程独立执行,目标线程变成守护线程,甚至thread对象销毁,并且无法通信。

管理线程

yield 让出处理器,重新调度各执行线程
get_id 返回当前线程的线程 id
sleep_for 使当前线程的执行停止指定的时间段
sleep_until 使当前线程的执行停止直到指定的时间点

#include <bits/stdc++.h>
using namespace std;

void print_time() {
  auto now = chrono::system_clock::now();
  auto in_time_t = chrono::system_clock::to_time_t(now);

  std::stringstream ss;
  ss << put_time(localtime(&in_time_t), "%Y-%m-%d %X");
  cout << "now is: " << ss.str() << endl;
}

void sleep_thread() {
  this_thread::sleep_for(chrono::seconds(3));
  cout << "[thread-" << this_thread::get_id() << "] is waking up" << endl;
}

void loop_thread() {
  for (int i = 0; i < 100000; i++) {
    cout << "[thread-" << this_thread::get_id() << "] print: " << i << endl;
  }
}

int main(){
  print_time();

  thread t1(loop_thread);
  thread t2(sleep_thread);

  t1.join();
  t2.detach();

  print_time();
  return 0;
}
#include "stdafx.h"
#include <iostream>  
#include <chrono>  
#include <thread> 
#include <atomic>
#include <mutex>

std::mutex g_mutex;
std::atomic<bool> ready(false);

void count1m(int id) {
    while (!ready)// wait until main() sets ready... 
    {             
        //若线程还有没创建的,将当前线程分配的cpu时间片,让调度器安排给其他线程,
        //由于使用了yield函数,在 not Ready 情况下,避免了空循环,在一定程度上,可以提高cpu的利用率
        std::this_thread::yield();
    }
    for ( int i = 0; i < 1000000; ++i) {}
    std::lock_guard<std::mutex> lock(g_mutex);
    std::cout << "thread : "<< id << std::endl;
}

int main(){
    std::thread threads[10];
    std::cout << "race of 10 threads that count to 1 million:\n";
    for (int i = 0; i < 10; ++i){
        threads[i] = std::thread(count1m, i);
    }
    ready = true;               // go!
    for (auto& th : threads)
    {
        th.join();
    }
    std::cout << '\n';
    return 0;
}
一次调用

call_once:即便在多线程环境下,也能保证只调用某个函数一次
once_flag:与call_once配合使用

#include<bits/stdc++.h>
using namespace std;

void init(){
    cout << "Initialing..." << endl;
}

void worker(once_flag* flag){
    call_once(*flag,init);
}

int main(){
    once_flag flag;
    thread t1(worker,&flag);
    thread t2(worker,&flag);
    thread t3(worker,&flag);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}
并发任务

计算Min-Max的平方根之和。
单线程:

#include<bits/stdc++.h>
using namespace std;

static const int MAX = 10e8;
static double sum = 0;

void worker(int min,int max){
    for(int i=min;i<=max;i++){
        sum+=sqrt(i);
    }
}

void serial_task(int min,int max){
    auto start_time = chrono::steady_clock::now();
    sum = 0;
    worker(0,MAX);
    auto end_time = chrono::steady_clock::now();
    auto ms = chrono::duration_cast<chrono::milliseconds>(end_time-start_time).count();
    cout << "Serail task finish, " << ms << " ms consumed, Result: " << sum << endl;
}

int main(){
    serial_task(0,MAX);
}

简单多线程:

void concurrent_task(int min, int max) {
  auto start_time = chrono::steady_clock::now();

  unsigned concurrent_count = thread::hardware_concurrency(); // ①
  cout << "hardware_concurrency: " << concurrent_count << endl;
  vector<thread> threads;
  min = 0;
  sum = 0;
  for (int t = 0; t < concurrent_count; t++) { // ②
    int range = max / concurrent_count * (t + 1);
    threads.push_back(thread(worker, min, range)); // ③
    min = range + 1;
  }
  for (auto& t : threads) {
    t.join(); // ④
  }

  auto end_time = chrono::steady_clock::now();
  auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count();
  cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << sum << endl;
}

答案发现是不对的,而且没有变快。
对于处理器来说:为了加速处理的速度,每个处理器都有自己的高速缓存(cache)。
在这里插入图片描述
处理器在进行计算的时候,高速缓存会参与其中,例如数据的读和写。而高速缓存和系统主存(Memory)是有可能存在不一致的。即:某个结果计算后保存在处理器的高速缓存中了,但是没有同步到主存中,此时这个值对于其他处理器就是不可见的。
事情还远不止这么简单。我们对于全局变量值的修改:sum += sqrt(i);这条语句,它并非是原子的。它其实是很多条指令的组合才能完成。所以每个线程保存的值可能不一样,保存在cache中并没有写到主存中。

竞争条件与临界区

当多个进程或者线程同时访问共享数据时,只要有一个任务会修改数据,那么就可能会发生问题。此时结果依赖于这些任务执行的相对时间,这种场景称为竞争条件
访问共享数据的代码片段称之为临界区

互斥体与锁

主要API:
1、mutex提供基本互斥设施
2、recursive_mutex提供能被同一线程递归锁定的互斥设施
3、shared_mutex提供共享互斥设施
所有的API都支持:
lock()锁定互斥体,如果不可用,则阻塞
try_lock()尝试锁定互斥体,如果不可用,直接返回
unlock解锁互斥体
P S PS PS:这三个方法提供了基础的锁定和解除锁定的功能。使用lock意味着你有很强的意愿一定要获取到互斥体,而使用try_lock则是进行一次尝试。这意味着如果失败了,你通常还有其他的路径可以走。

timed_mutexrecursive_timed_mutexshared_timed_mutex的名称都带有timed,这意味着它们都支持超时功能。它们都提供了try_lock_fortry_lock_until方法,这两个方法分别可以指定超时的时间长度和时间点。如果在超时的时间范围内没有能获取到锁,则直接返回,不再继续等待。

加锁也是有代价的,尽量不要频繁加锁,则有:

#include<bits/stdc++.h>
using namespace std;


static const int MAX = 10e8;
static double sum = 0;

static mutex exclusive;

void concurrent_worker(int min, int max) {
  double tmp_sum = 0;
  for (int i = min; i <= max; i++) {
        tmp_sum += sqrt(i);
  }
  exclusive.lock();
  sum+=tmp_sum;
  exclusive.unlock(); // ②
}

void concurrent_task(int min, int max) {
  auto start_time = chrono::steady_clock::now();

  unsigned concurrent_count = thread::hardware_concurrency();
  cout << "hardware_concurrency: " << concurrent_count << endl;
  vector<thread> threads;
  min = 0;
  sum = 0;
  for (int t = 0; t < concurrent_count; t++) {
    int range = max / concurrent_count * (t + 1);
    threads.push_back(thread(concurrent_worker, min, range)); // ③
    min = range + 1;
  }
  for (int i = 0; i < threads.size(); i++) {
    threads[i].join();
  }

  auto end_time = chrono::steady_clock::now();
  auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count();
  cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << sum << endl;
}

int main(){
    concurrent_task(0,MAX);
}

我们用锁的粒来描述锁的范围。细粒度是指锁保护较小的范围,粗粒度是指锁保护较大的范围。出于性能的考虑,我们应该保证锁的粒度尽可能的细。并且,不应该在获取锁的范围内执行耗时的操作,例如执行IO。如果是耗时的运算,也应该尽可能的移到锁的外面。

死锁

死锁是指:两个或以上的运算单元,每一方都在等待其他方释放资源,但是所有方都不愿意释放资源。结果是没有任何一方能继续推进下去,于是整个系统无法再继续运转。
避免死锁:
使用lock函数,一次性创建多个互斥锁,标准库保证不会发生死锁。

lock(*accountA->getLock(), *accountB->getLock());    // ①
lock_guard<mutex> lockA(*accountA->getLock(), adopt_lock);  // ②
lock_guard<mutex> lockB(*accountB->getLock(), adopt_lock);  // ③
通用互斥管理

lock_guard 实现严格基于作用域的互斥体所有权包装器
unique_lock 实现可移动的互斥体所有权包装器
shared_lock 实现可移动的共享互斥体所有权封装器
scoped_lock 用于多个互斥体的免死锁RAII封装器

defer_lock 类型为 defer_lock_t,不获得互斥的所有权
try_to_lock 类型为try_to_lock_t,尝试获得互斥的所有权而不阻塞
adopt_lock 类型为adopt_lock_t,假设调用方已拥有互斥的所有权

RALL

RAII是一种C++编程技术,它将必须在使用前请求的资源(例如:分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物)的生命周期与一个对象的生存周期相绑定。
在这里插入图片描述

lock(*accountA->getLock(), *accountB->getLock());
lock_guard lockA(*accountA->getLock(), adopt_lock);
lock_guard lockB(*accountB->getLock(), adopt_lock);
unique_lock lockA(*accountA->getLock(), defer_lock);
unique_lock lockB(*accountB->getLock(), defer_lock);
lock(*accountA->getLock(), *accountB->getLock());
scoped_lock lockAll(*accountA->getLock(), *accountB->getLock());

这三个是等价的,第一个没有自己上锁的能力,第二个有。

条件变量

condition_variable 提供与 std::unique_lock 关联的条件变量
condition_variable_any 提供与任何锁类型关联的条件变量
notify_all_at_thread_exit 安排到在此线程完全结束时对 notify_all 的调用
cv_status 列出条件变量上定时等待的可能结果
每一次操作都要正确执行,如果条件不满足就停下来等待,直到条件满足之后再继续。而不是直接返回。

条件变量提供了一个可以让多个线程间同步协作的功能。这对于生产者-消费者模型很有意义。在这个模型下:

生产者和消费者共享一个工作区。这个区间的大小是有限的。
生产者总是产生数据放入工作区中,当工作区满了。它就停下来等消费者消费一部分数据,然后继续工作。
消费者总是从工作区中拿出数据使用。当工作区中的数据全部被消费空了之后,它也会停下来等待生产者往工作区中放入新的数据。
从上面可以看到,无论是生产者还是消费者,当它们工作的条件不满足时,它们并不是直接报错返回,而是停下来等待,直到条件满足。

修改银行转账钱不够不是直接返回的问题。

  void changeMoney(double amount) {
    unique_lock lock(mMoneyLock); // ②
    mConditionVar.wait(lock, [this, amount] { // ③
      return mMoney + amount > 0; // ④
    });
    mMoney += amount;
    mConditionVar.notify_all(); // ⑤
  }

condition_variable mConditionVar;
1、这里声明了一个条件变量,用来在多个线程之间协作。
2、这里使用的是unique_lock,这是为了与条件变量相配合。因为条件变量会解锁和重新锁定互斥体。
3、这里是比较重要的一个地方:通过条件变量进行等待。此时:会通过后面的lambda表达式判断条件是否满足。如果满足则继续;如果不满足,则此处会解锁互斥体,并让当前线程等待。解锁这一点非常重要,因为只有这样,才能让其他线程获取互斥体。
4、这里是条件变量等待的条件。如果你不熟悉lambda表达式,请自行网上学习,或者阅读我之前写的文章。
5、此处也很重要。当金额发生变动之后,我们需要通知所有在条件变量上等待的其他线程。此时所有调用wait线程都会再次唤醒,然后尝试获取锁(当然,只有一个能获取到)并再次判断条件是否满足。除了notify_all还有notify_one,它只通知一个等待的线程。wait和notify就构成了线程间互相协作的工具。
详情:见阿里

lambda

在这里插入图片描述

如何实现无锁队列

C++模板

typenameclass的区别在于,typename可以适用于嵌套类型来表示类。(貌似现在class也行了)

class MyArray
{
public:
    typedef int LengthType;
};
template<class T>
void MyMethod(T myarr)
{
    typedef typename T::LengthType LengthType;
    LengthType length = 5;
    printf("%d",length);
}

不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行。


类模板

    template<class  形参名,class 形参名,…>
    class 类名{ ... };
    template<class T1,class T2> void A<T1,T2>::h(){}//在类外声明函数
    A<int,double> a;//不能使用A<2,3.0>
	   template<class T, int a> class B{}//a为非类型形参(只允许,整形/指针/引用)
	   //调用的时候,必须保证编译时能计算出这个a

函数模板用非模板形参,h<int,3>(a)来调用。
可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。


模板的实例化:
隐式实例化在运行时才生成实例。
显式实例化在编译期间就会生成实例,方法如下:

[cpp] view plaincopyprint?
template void swap<int>(int &a,int &b);  

特化:

[cpp] view plaincopyprint?
template <> 
void swap<job>(job a,job b){...}  

偏特化:
局部特化,加const也算局部。

模板有两种特化,全特化和偏特化(局部特化)
模板函数只能全特化,没有偏特化(以后可能有)


模板类的继承包括四种:
1.(普通类继承模板类):
class Derived:public TBase<int>
2.(模板类继承了普通类(非常常见)):
template<class T> class TDerived:public TBase
3.(类模板继承类模板)
template<class T1,class T2> class TDerived:public TBase<T1>
4.(模板类继承类模板,即继承模板参数给出的基类)
template<class T> class Derived:public T

在我们使用类模板时,只有当代码中使用了类模板的一个实例的名字,而且上下文环境要求必须存在类的定义时,这个类模板才被实例化。

事务管理

生命周期
开始[ → \to 调度执行 → \to ]事务初始状态;
while(1){SQL操作成功 → \to 事务正常状态} → \to 事务提交状态
SQL操作失败,事务失败状态[ → \to 回滚操作 → \to ]事务回滚状态

事务被DBMS调度执行后,就进入事务初始状态。当事务的SQL操作语句被成功执行后,事务就进入事务正常状态。如果事务的所有SQL操作语句都成功执行,事务将执行Commit(提交)操作语句,并进入事务提交状态。
在事务提交状态下,系统将所有操作语句对数据的修改都更新到数据库文件中,并将所有数据操作记录到数据库事务日志(Log)文件,以便数据库出现故障时,事务所做的更新操作能通过日志数据进行恢复。当事务提交操作完成后,事务程序退出并结束。
事务在执行期间,即使进入事务正常状态后,仍有可能遇到意外导致执行失败。这时事务进入事务失败状态,使用Rollback(回滚)操作语句,并进入事务回滚状态,系统撤销该事物对数据库所有的数据修改或删除操作,使数据库恢复到事务执行之前的数据状态。

ACID特性:
1、<原子性>
事务原子性指事务的数据库操作序列必须在一个原子工作单元中,要么全部正确执行,要么全都不执行。
2、<一致性>
事务一致性指事务执行的结果使数据库从一种正确数据状态变迁到另一种正确数据状态。(比如银行转账,前后一定要总金额相等)
3、<隔离性>
事务隔离性指当多个事务并发执行时,一个事务的执行不能被其他事务干扰。
4、<持续性>
事务持续性指一个事务一旦提交,它对数据库中数据的改变应该是永久性的。这是因为事务提交后,数据修改被写入数据文件中,可持久保存。

事务的并发执行:
1、改善系统的资源利用率(IO)
2、减少事务执行的平均等待时间

并发控制:
1、<脏读>:读了被修改的数据,但修改失败了回滚,数据未被提交。
除非对业务逻辑带来错误,否则没必要对脏读处理(只要后续没有使用和处理),因为可以提高并发性。
2、<不可重复读>: 一个事务对同一共享数据先后重复读取两次,但是发现原有数据改变或丢失。(并发的时候某个事务对共享数据进行了修改或删除操作)
3、<幻象读>: 一个事务对同一共享数据重复读取两次,第二次增加了一些数据。(并发的时候某个进行了添加操作)
4、<丢失更新>: 多个事务并发,某个事务对共享数据进行了更新,并改变了前面事务的更新值。

并发事务的调度:
当事务调度顺序的执行结果与事务串行执行的数据结果一样时,该并发事务调度才能保证数据库的一致性,符合这样效果的调度被称为可串行化调度。
<排他锁>:可以封锁其他事务对共享数据的任何加锁操作,限制修改、删除、读取操作(Lock-X)
<共享锁>:只封锁其他事务对加锁数据的修改或删除操作,但可以允许其他事务对加锁数据进行共享数据读操作。(Lock-S)

如果一个事务被实施了共享锁,其他事务只能添加共享锁定,不能添加排他锁定。
如果是排它锁,就不能进行加锁操作。

一级加锁协议
任何事务修改(更新、删除)共享数据对象之前,必须对该共享数据单元执行排它锁指令,直到处理完成,才执行解锁指令。
可以解决"丢失更新"。
二级加锁协议
在一级基础上,针对并发事务对共享数据进行读操作之前,必须对该数据执行共享锁定指令,读完数据后即可释放共享锁定。
可以解决"脏读"
三级加锁协议
在一级加锁协议基础上,针对并发事务对共享数据进行读操作前,必须先对该数据执行共享锁定指令,直到该事务处理结束来释放共享锁定。
可以解决"不可重复读"

两阶段锁定协议:
保证能进行可串行化调度。
增长阶段:对共享数据进行加锁申请
缩减阶段:对已有的锁定进行释放。

并发事务死锁:
条件:1、互斥 2、请求与保持 3、不可剥夺 4、环路等待
解决:1、预防 2、其中一个事务释放资源

隔离级别:
1、读取未提交(脏读、不可重复读、幻读、丢失更新)
2、读取已提交(解决了脏读)
3、可重复读(解决了脏读、不可重复读)
4、可串行化(都不可能)

数据库锁机制:
在这里插入图片描述


乐观锁和悲观锁的区别在于 是否认为并发问题一定会存在
按锁的粒度划分(即每次上锁的对象是表,行还是页):表级锁,行级锁,页级锁
按锁的级别划分:共享锁、排他锁
按加锁方式分:自动锁(存储引擎自行根据需要施加的锁)、显式锁(用户手动请求的锁)
按操作划分:DML锁(对数据进行操作的锁)、DDL锁(对表结构进行变更的锁)
最后按使用方式划分:悲观锁、乐观锁


悲观锁
认为并发问题总会出现,所以每次一个事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。

排它锁(写,X锁)和共享锁S
对于以上两个锁来说,我们可以针对粒度对其进行进一步分类,分为表锁和行锁:
1、行锁为给某一行上锁(如果是 X 锁则类似于修改某一篇文章)
2、表锁则为给一个表加上锁(如果是 X 锁则类似于为了更换博客系统而将整个博客下线了),通常用在 DDL 语句中,如 DELETE TABLE,ALTER TABLE 等,由于表锁影响整个表的数据,并发性不如行锁好。

为了表锁和行锁而存在的意向锁:
行锁和表锁显然会冲突,但是加表锁的时候不可能一行一行去查找是否有行锁,所以加意向锁表示有行被锁住了即可。

更新锁:
更新锁在的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成的死锁现象(共享锁都要升级成独占锁,但都需要别人释放共享锁才能升级)
允许读取,和共享锁是兼容的,但是最多只有一把更新锁。
更新锁可以升级。


乐观锁:
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做,一般来说可以使用版本号机制和CAS算法实现。

<版本号机制>:
在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改的时候,version会+1 ,当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据。
<CAS算法>:
compare and swap,是一种比较有名的无锁算法。
非阻塞同步,涉及到三个操作数:
1、需要读写的内存值 V
2、进行比较的值 A
3、拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试,与 version 事务机制类似,CAS 事务也是一种细粒度的锁。然而,version 为行级锁,粒度过大, 而 CAS 事务为列级锁,粒度更小。根据锁机制的一般原则,粒度越小,并发性能越高。
[缺点]:
ABA 问题
比如说一个线程 T1 从内存位置V中取出 A,这时候另一个线程 T2 也从内存中取出 A,并且 T2 进行了一些操作变成了 B,然后 T2 又将 V 位置的数据变成 A,这时候线程 T1 进行 CAS 操作发现内存中仍然是A,然后 T1 操作成功。
循环时间长开销大
自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS无法保证操作的原子性。


索引

(如果没有说明具体的数据库和存储引擎,默认指的是MySQL中的InnoDB存储引擎)
MySQL的基础存储结构是页。
每个数据页可以组成一个双向链表,每个数据页中的记录可以组成一个单向链表。


每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。


索引降低增删改的速度,如果用平衡树实现,因为要多维护信息。
或者哈希索引(不支持范围查询,容易产生碰撞问题,不支持最左匹配原则)。
InnoDB是自适应哈希索引,由存储引擎自动优化创建。

聚集和非聚集索引

聚集索引就是以主键创建的索引,在叶子节点存储的是表中的数据
非聚集索引就是以非主键创建的索引,在叶子节点存储的是主键和索引列
,使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)

无锁队列

线程同步导致性能下降:

1、cache颠簸
线程是"操作系统可以调度运行的最小的执行单元",进程拥有一组指令集(代码)和一段内存,线程执行一个代码片段但与包含它的进程共享内存空间。
Linux来说,线程是另一种"执行上下文"。

每当一个任务被抢占,Cache中的内容都需要被接下来获得运行权的进程覆盖,这意味着,接下来运行的进程需要花费一定的时间来将Cache预热才能达到良好的运行效率。
2、在同步机制上的争抢队列
队列可被应用到大多数多线程解决方案中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值