线程知多少~(下篇)

2024-03-07 19:30
文章标签 线程 下篇 知多少

本文主要是介绍线程知多少~(下篇),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一. 常见的锁策略

首先声明,接下来的锁策略并不仅仅局限于java,任何与"锁"相关的话题,都可能会涉及到接下来的内容.

1.1 乐观锁&悲观锁

悲观锁 :
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

 一般情况下,实现悲观锁需要做更多的工作,乐观锁做的工作就比较少

 举个例子来说明~

对于大学生来说,能进入公司内部实习是一个梦寐以求的机会.同学A认为,面试某讯公司的竞争者并不多,因此他做的准备比较少,这就是"乐观锁";同学B认为,肯定有很多人挤破头想去某讯当实习生,因此为了这次面试,他闭关修炼了大半年,这就是"悲观锁"的实现.

1.2 轻量级锁&重量级锁

锁的核心特性 " 原子性 ", 这样的机制追根溯源是 CPU 这样的硬件设备提供的 .
  • CPU 提供了 "原子操作指令".
  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized ReentrantLock 等关键字和类.

 

重量级锁 : 加锁机制重度依赖了 OS 提供了 mutex.
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.  

 重量级锁基于内核的一些功能来实现,会在内核中做很多事情,比如阻塞等待;轻量级锁只涉及到少量的内核态用户态切换,加锁成本比较低.

大部分情况下,悲观锁也是重量级锁,乐观锁则是轻量级锁.但一种是对阻塞情况的预估,一种是实际锁的开销,要注意区分.

1.3 自旋锁&挂起等待锁

自旋锁:如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.

挂起等待锁:如果获取锁失败,就会阻塞等待,放弃CPU,过了很久之后,再次尝试获取锁

 举个栗子来理解一下~

貂蝉小姐宣布和吕布复合了.她的追求者自成两派~

 滑稽A老铁就相当于一个"自旋锁",他的伪代码是这样的.

while(lock==false){ }

自旋锁是"轻量级锁"的一种实现方式,是由用户态代码实现的.

好处就是,一旦锁被其他线程释放,它就能立刻获取到锁;但是如果其他线程一直占用锁,自旋锁就会持续消耗CPU资源.

挂起等待锁往往是由内核实现的.

如果没有获取到锁,挂起等待锁也不会一直抢占CPU;但是这个线程无法第一时间获取到锁.

1.4 公平锁&非公平锁

公平锁 : 遵守 " 先来后到 ". B C 先来的 . A 释放锁的之后 , B 就能先于 C 获取到锁 .
非公平锁 : 不遵守 " 先来后到 ". B C 都有可能获取到锁 .

举个栗子~

貂蝉小姐果然不孚众望地分手了~

下面是公平锁的做法.

如果是非公平锁...

1.5 可重入锁&不可重入锁

1.5.1 可重入VS不可重入

可重入锁,允许一个线程给一个对象进行多次加锁.

不可重入锁,一个对象只能被加一次锁.

下图为可重入锁~ 

下面是不可重入锁~

 在理解这两者的概念之后,我们来详细谈一下"死锁"问题.

1.5.2 死锁的产生

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.

 举个接地气的例子~滑稽老铁出门的时候,不小心把车钥匙落在了家里,然后悲催地发现自己家里的钥匙在车里...

死锁产生的必要条件如下:

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列.p1等待p2,p2等待p3,p3等待p1...

死锁一般是以下三种情况:

 1. 一个线程想要给一个不可重入锁加锁两次

lock (locker){//第一次加锁

    lock(locker){//第二次加锁

       ...

    }

}

 上面的代码中,假设该线程第一次加锁成功,而该线程第二次尝试加锁时发现locker对象已经被加锁,就会阻塞等待锁释放,但是第一次加锁释放的条件是代码块已被执行完.因此就产生了死锁

2. 两个线程各自拥有一把锁,但是都想要对方手里的那把

比如,两位滑稽老铁都想吃西红柿鸡蛋面,一个只有西红柿,一个只有鸡蛋~就引发了"死锁"

 3. n个线程有m把锁

最经典的例子莫过于哲学家就餐问题~

 

有五位哲学家,每天循环做两件事:思考,吃面。吃面时每人面前都有一个盘子,盘子左边和右边都有一根筷子,他们在吃面之前需要同时拿起两边的筷子,有了一双筷子就可以吃面了.很不幸,当五位哲学家同时想吃面条时,就会发生这样的事故...

 

他们谁也不肯把手里的筷子让给别人,于是只能阻塞等待...也就产生了"死锁". 

1.5.3 解决"死锁"问题

死锁的解决方法有很多种,下面介绍一种最简单的解决办法.

给锁进行编号,确定加锁顺序(比如只能按照从小到大的编号进行加锁)

 按照规则,右下角的那位哲学家只能先拿起编号为1的筷子,因此他左边的哲学家就可以拿起两只筷子吃饭了...

1.6 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥. 如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生.
一个线程对于数据的访问 , 主要存在两种操作 : 读数据 和 写数据 .
  1. 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  2. 两个线程都要写一个数据, 有线程安全问题.
  3. 一个线程读另外一个线程写, 也有线程安全问题
读写锁就是把读操作和写操作区分对待 . Java 标准库提供了 ReentrantReadWriteLock , 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁 . 这个对象提供了 lock / unlock 方法进行
加锁解锁 .
ReentrantReadWriteLock.WriteLock 类表示一个写锁 . 这个对象也提供了 lock / unlock 方法进
行加锁解锁 .
其中 ,
  • 读加锁和读加锁之间, 不互斥.
  • 写加锁和写加锁之间, 互斥.
  • 读加锁和写加锁之间, 互斥.

 简单进行以下代码的演示~

public static void main(String[] args) {ReentrantReadWriteLock lock=new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock=lock.readLock();Thread thread=new Thread(()->{readLock.lock();//...执行代码逻辑readLock.unlock();});thread.start();}

读写锁特别适合于"频繁读,不频繁写"的场景中,但是有个缺点----必须手动释放,程序猿很可能会忘记解锁.

1.7 synchronized实现原理

1.7.1 synchronized的特性

结合上面的锁策略 , 我们就可以总结出 , Synchronized 具有以下特性 ( 只考虑 JDK 1.8):
  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

 1.7.2 synchronized加锁过程

JVM将synchronized分成无锁,偏向锁,轻量级锁,重量级锁状态.会根据冲突情况依次升级,这个过程也被称为"锁膨胀".

 1> 偏向锁,第一个尝试加锁的线程,会进入偏向锁状态.并不是真的加锁,而是做个标记

 2> 轻量级锁,当另外一个线程也尝试对该对象加锁时,就会进入轻量级锁状态(一般是CAS实现的自旋锁)

就像电视剧上的狗血情节一样~

小红和小绿是青梅竹马,也相互喜欢.没有人追求小红之前,小绿从未官宣过小红是他的女友(此时处于偏向锁状态).小蓝对小红表白后,小绿立马发朋友圈"这是我女朋友!"(进入轻量级锁状态).

 3> 重量级锁,更多的线程尝试给该对象加锁,锁竞争激烈,就会膨胀为重量级锁(挂起等待锁)

1.8 其他的锁优化

1.8.1 锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除
在某些代码中,用到了synchronized,但是并没有涉及到多线程
此时每个 append 的调用都会涉及加锁和解锁 . 但如果只是在单线程中执行这个代码 , 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销 .就会触发编译器的优化.

1.8.2 锁粗化

 一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

粒度粗的锁会减少加锁/解锁的开销

 举个栗子~

老板安排了三个任务,你要一个一个地去汇报,还是三个做完了一起汇报?如果我是老板,我更喜欢第二种,因为你每次进来我都得准备给你泡茶...

可以看到 , synchronized 的策略是比价复杂的 , 在背后做了很多事情 , 目的为了让程序猿哪怕啥都不懂, 也不至于写出特别慢的程序.

二. CAS

2.1 什么是CAS

CAS: 全称Compare and swap,字面意思:”比较并交换,一个 CAS 涉及到以下操作:
我们假设内存中的原数据 V ,旧的预期值 A ,需要修改的新值 B
1. 比较 A V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V 。(交换)
3. 返回操作是否成功。

CAS伪代码:(下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解)

boolean CAS ( address , expectValue , swapValue ) {//address为待比较值(存放在内存中),exceptValue为期望的值(存放在寄存器中),swapValue为要更改的新值(存放在寄存器中)
if ( address == expectedValue ) {
address = swapValue ;
return true ;
  }
return false ;
}

当多个线程同时对某个资源进行CAS操作,只有一个线程能操作成功并返回true,其他线程只能返回false.这为我们实现"无锁编程"提供了新思路.

CAS是由硬件提供了支持才得以实现,是一个原子指令

2.2 CAS的应用

 1> 实现原子类

标准库中提供了 java.util.concurrent.atomic , 里面的类都是基于这种方式来实现的 .
典型的就是 AtomicInteger . 其中的 getAndIncrement 相当于 i++ 操作 .
AtomicInteger atomicInteger = new AtomicInteger ( 0 ); //设置初始值为0
atomicInteger . getAndIncrement (); // 相当于 i++

伪代码实现:

class AtomicInteger {

private int value ;//内存中的值
public int getAndIncrement () {
int oldValue = value ;//相当于一个寄存器,存储原始数据
while ( CAS ( value , oldValue , oldValue + 1 ) != true ) {
oldValue = value ;
      }
return oldValue ;
  }
}

来解释一下上面的代码,如果在单线程中,CAS操作是没有必要的,但是如果是多线程...

 

相对于synchronized来说,使用CAS操作不会引起线程阻塞等待问题,并且CAS是一条原子指令,是线程安全的.

2> 实现自旋锁

基于CAS操作的自旋锁伪代码 

public class SpinLock {
private Thread owner = null ;
public void lock (){
// 通过 CAS 看当前锁是否被某个线程持有 .
// 如果这个锁已经被别的线程持有 , 那么就自旋等待 .
// 如果这个锁没有被别的线程持有 , 那么就把 owner 设为当前尝试加锁的线程 .
while ( ! CAS ( this . owner , null , Thread . currentThread ())){
      }
  }
public void unlock (){
this . owner = null ;
  }
}

 哪个线程调用lock方法,就会不停地循环,从而实现自旋锁

2.3 CAS的ABA问题

ABA 的问题:

假设存在两个线程 t1 t2. 有一个共享变量 num, 初始值为 A.
接下来 , 线程 t1 想使用 CAS num 值改成 Z, 那么就需要先读取 num 的值 , 记录到 oldNum 变量中 . 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是 , t1 执行这两个操作之间 , t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A
到这一步,线程t1无法区分当前的变量始终是A,还是经历了yigebianhu

 

可能有的同学会问,这个ABA问题会有什么影响吗?

举个栗子~

假设你手中有100块钱~打算还给小丽同学50,在你转账的时候,不小心按了两下转账,创建了两个-50的线程;就在这时,小明同学将他欠你的50还了回来

 我们期待的是,你使用的ATM机判断出来这是操作失误,只扣款50元

最终你的余额只剩了50,既然都谈到钱了,ABA问题是不是很严重!

 那么该怎么解决ABA问题呢?

给要修改的值, 引入版本号/时间戳. CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期
  • CAS 操作在读取旧值的同时, 也要读取版本号.
  • 真正修改的时候, 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

 于是你的转账流程就变成了这样~

三.  JUC(java.util.concurrent) 的常见类

3.1 Callable接口

Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果.

因为run方法没有返回值,就有了Callable的诞生~下面代码演示下它的使用

public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable=new Callable<Integer>() {//定义Callable的内部逻辑,相当于run方法@Overridepublic Integer call() throws Exception {int sum=0;for(int i=1;i<50;i++){//计算1+2+...+49的和sum+=i;}return sum;}};FutureTask<Integer> futureTask=new FutureTask<>(callable);//用callable构造futureTask对象Thread thread=new Thread(futureTask);//用futureTask构造线程thread.start();//线程开始执行System.out.println(futureTask.get());//用futureTask接收结果}

 怎么用一个通俗的例子解释FutureTask类的作用捏~

就好比你去食堂排队买麻辣烫,将食材挑好之后交给食堂阿姨(Callable对象构造完成),阿姨会给你一个小票(FutureTask对象构建完成),然后阿姨开始制作(线程开始执行),最后你凭着这张小票去取你的麻辣烫(用FutureTask对象接收结果)

如果我们只关心线程的执行,重写run方法即可.

如果我们需要知道线程执行的结果,需要借助Callable类.

3.2 原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

AtomicInteger 举例,常见方法有

addAndGet(int delta);   //i += delta
decrementAndGet(); //--i
getAndDecrement(); //i--
incrementAndGet(); //++i
getAndIncrement(); //i++

3.3 ReentrantLock

可重入互斥锁,与synchronized类似,都是用来保证线程安全的

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
public static void main(String[] args) {ReentrantLock lock=new ReentrantLock();lock.lock();try{//代码逻辑执行 }finally {lock.unlock();}}

 ReentrantLock与synchronized的区别:

1. synchronized使用时不需要手动释放锁,ReentrantLock需要手动释放

2. synchronized申请锁失败时会死等,ReentrantLock可以灵活调整等待时间

3. synchronized是JVM内部(大概率是C++)实现的,ReentrantLock是标准库的一个类,是基于Java实现的

4. ReentrantLock可以实现公平锁(构造方法传入true就可以创建一个公平锁)

3.4 信号量Semaphore 

 信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.

就好像小区里的停车位一样~当前车位有100个,就表示可用资源有一百个,每当一辆车进来,就要用掉一个(称为信号量的P操作);每当一辆车出去,就要释放一个(称为信号量的V操作)

Semaphore的PV操作是原子的,可以直接在多线程环境下使用.

代码示例

public static void main(String[] args) {Semaphore semaphore=new Semaphore(5);//初始设置的信号量为5for(int i=0;i<10;i++){Thread thread=new Thread(()->{//创建线程try {semaphore.acquire(2);//尝试获取2个信号量,不够时会发生阻塞等待System.out.println(Thread.currentThread().getName()+"获取信号量完成");Thread.sleep(1000);//休息1s后释放信号量semaphore.release();System.out.println(Thread.currentThread().getName()+"释放信号量完成");} catch (InterruptedException e) {e.printStackTrace();}});thread.start();}}

3.5 CountDownLatch

同时等待n个任务执行完成.

举个栗子~一场马拉松比赛,只有所有选手都跑到了终点,才算完成

代码示例

 public static void main(String[] args) throws InterruptedException {CountDownLatch latch=new CountDownLatch(10);//一开始有十个运动员for(int i=0;i<10;i++){Thread thread=new Thread(()->{latch.countDown();try {Thread.sleep(1000);//每个运动员休息1s} catch (InterruptedException e) {e.printStackTrace();}});thread.start();}latch.await();//等待10个运动员全部到达}

有些同学会疑惑,这个类有什么用途嘛?

用处可大了,比如让人憎恶的"某度网盘",由于服务器那边的限制,下载速度贼慢,这时候就可以使用"多线程下载(ADM)",把一个文件拆成多份一个线程下载一部分,直到所有线程都下载完毕了才算完成.

四. 线程安全的集合类

原来的集合类 , 大部分都不是线程安全的 .
Vector, Stack, HashTable, 是线程安全的 ( 不建议用,说它安全是因为在一些关键方法上加了锁,是Java上古时期搞出来的类,未必真的线程安全 ), 其他的集合类不是线程安全的 .

4.1 多线程环境下使用ArrayList

1>  手动添加synchronized/ReentrantLock

2> 使用Cooletions.synchronizedList(new ArrayList)

该类的每一个关键操作都加了synchronized

3> 使用CopyOnWriteArrayList

CopyOnWrite 容器即写时复制的容器。
写时复制,修改的时候会先创建一个副本,然后在副本上进行修改,修改完后让副本转正.
这样修改的时候就不会对读操作造成影响.
  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy复制出一个新的容器,然后新的容器里添加元素
  • 添加完元素之后,再将原容器的引用指向新容器

4.2 多线程环境使用队列

  1.  ArrayBlockingQueue 基于数组实现的阻塞队列
  2.  LinkedBlockingQueue 基于链表实现的阻塞队列
  3.  PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
  4.  TransferQueue 最多只包含一个元素的阻塞队列

4.3 多线程环境使用哈希表

HashMap本身是线程不安全的

1> HashTable,在关键方法上加上了synchronized

 这相当于直接对HashTable对象本身加锁

  1. 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
  2. size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  3. 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

如上图,当一个线程修改第一个链表,另一个线程修改第三个链表时,并不会有线程安全问题(抛开size不谈)

2> ConcurrentHashMap

 只给每个链表的对象头加锁

  1. 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁.
  2. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
  3. 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

优化了扩容方式: 化整为零 .发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去. 扩容期间, 新老数组同时存在.

1. 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素. 搬完最后一个元素再把老数组删掉.

2. 这个期间, 插入只往新数组加.

3. 这个期间, 查找需要同时查新数组和老数组

这篇关于线程知多少~(下篇)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/784574

相关文章

Java线程面试题(50)

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。 在典型的Java面试中, 面试官会从线程的基本概念问起, 如:为什么你需要使用线程,

线程池ThreadPoolExecutor类源码分析

Java并发编程:线程池的使用   在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:   如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。   那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

线程Lock

线程Lock   在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问。本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。   也许有朋友会问,既然都可以通过synchronized来实现同步访问了,那么为什么还需要提供Lock?这个问题将在下面进行阐述。本文先从s

线程封装,互斥

文章目录 线程封装线程互斥加锁、解锁认识接口解决问题理解锁 线程封装 C/C++代码混编引起的问题 此处pthread_create函数要求传入参数为void * func(void * )类型,按理来说ThreadRoutine满足,但是 这是在内类完成封装,所以ThreadRoutine函数实际是两个参数,第一个参数Thread* this不显示 解决方法: 第

Linux-笔记 线程同步机制

目录 前言 实现 信号量(Semaphore) 计数型信号量 二值信号量  信号量的原语操作 无名信号量的操作函数 例子 互斥锁(mutex) 互斥锁的操作函数 例子 自旋锁 (Spinlock) 自旋锁与互斥锁的区别 自旋锁的操作函数 例子 前言         线程同步是为了对共享资源的访问进行保护,确保数据的一致性,由于进程中会有多个线程的存在,

jmeter之Thread Group(线程组)

Thread Group(线程组) 1.线程组,或者可以叫用户组,进行性能测试时的用户资源池。 2.是任何一个测试计划执行的开始点。 3.上一篇提到的“控制器”和“HTTP请求”(采集器)必须在线程组内;监听器等其他组件,可以直接放在测试计划下。 线程组设置参数的意义 我们以下图为例,进行详细说明。见下图:  区域1(在取样器错误后要执行的动作) 这个区域的主要作用很明显,在线程内

如何在Android中实现多线程与线程池?

目录 一、Android介绍二、什么是多线程三、什么是线程池四、如何在Android中实现多线程与线程池 一、Android介绍 Android是一种基于Linux内核的开源操作系统,由Google公司领导开发。它最初于2007年发布,旨在为移动设备提供一种统一、可扩展的操作系统。Android系统以其高度的可定制性和丰富的应用生态而受到广泛欢迎,如今已经成为全球最流行的

线程间通信方式(互斥(互斥锁)与同步(无名信号量、条件变量))

1通信机制:互斥与同步 线程的互斥通过线程的互斥锁完成; 线程的同步通过无名信号量或者条件变量完成。 2  互斥 2.1 何为互斥?         互斥是在多个线程在访问同一个全局变量的时候,先让这个线程争抢锁的资源,那个线程争抢到资源,它可以访问这个变量,没有争抢到资源的线程不能够访问这个变量。那这种只有一个线程能够访问到这个变量的现象称之为线程间互斥。 2.2互斥锁API 1.

线程C++

#include <thread>#include <chrono>#include <cmath>#include <mutex>#include <iostream>using namespace std;mutex mtx;void threadCommunicat(){int ans = 0;while (ans<=3){mtx.lock();//上锁cout << "a

线程知识点(一)

文章目录 一、线程是什么?二、进程与线程的关系三、种类内核级线程用户级线程混合型线程 总结 一、线程是什么? 线程是程序最基本的运行单位,真正运行的是进程中的线程。 线程是大多数操作系统支持的调度单位, 执行单元,某些系统不支持线程技术。 是允许应用程序并发执行多个任务的一种机制,同一程序中的所有线程均会独立执行相同程序。 共享同一份全局内存区域,其中包据初始化数据段、未初