SaaS短链接系统
短链接生成算法
简历内容:通过布隆过滤器完成判断短链接是否已存在,性能远胜分布式锁搭配查询数据库方案
存在哈希冲突怎么办?
如果冲突了,就去重新生成,然后设置一个重设次数,不停的重新生成短链接,如果一直失败,抛出异常。(然后这里判断短链接是否重复生成也是通过布隆过滤器来进行判断的,这样远远比通过分布式锁查询数据库的效率高) 布隆过滤器可能存在一定误判,如果出现误判了,尝试插入数据库,如果出现重复,则抛出异常
但是这样子可能还是有问题,我们可以对每一个链接,给这个链接生成一个分布式ID,然后用这个分布式ID拼接上原始链接作为输入,因为同一时刻可能会有多个相同的url来进行尝试,这样就会造成冲突,使用murmurHash进行生成128bit的哈希值然后转换为62进制的数,添加一个分布式ID是为了减少哈希冲突,但是这样并不能完全避免保证哈希冲突,(加一个分布式ID主要是为了防止某一时刻有多个线程对同一个链接要求生成短链接,那么这样有很大概率产生哈希冲突,为了避免哈希冲突,需要加一个标记,我这里使用的是雪花算法生成的分布式ID,用这个和短链接拼接来进行生成)
什么是分布式ID
然后判断一个短链接是否已经生成了使用布隆过滤器
布隆过滤器
BKDRHash布隆过滤器中使用这种哈希函数,把字符串映射成数字,然后表示各个位置
为了兼容短链接后管用户分页查看短链接功能,在短链接数据分片的基础上增加路由表完成跳转功能。
在这里采用了分库分表的思想,一个相关博客是分库分表描述
所以有个问题,按GID分的最后结果是怎样的?插入是插入到哪个数据库
封装缓存不存在读取功能,通过双重判定锁优化更新或失效场景下大量查询数据库问题
视频位于,短链接跳转原始链接功能(缓存击穿)
if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
((HttpServletResponse) response).sendRedirect("/page/notfound");
return;
}
RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
lock.lock();
try {
originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
if (StrUtil.isNotBlank(originalLink)) { //这里就是双重判定锁,防止一个多线程来取得时候,第一个线程发现缓存没有给添加到数据库中然后给添加到缓存中,后面来的请求依然访问数据库,给数据库带来极大的压力
ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
shortLinkStats(fullShortUrl, null, statsRecord);
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
if (shortLinkGotoDO == null) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
((HttpServletResponse) response).sendRedirect("/page/notfound");
return;
}
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
((HttpServletResponse) response).sendRedirect("/page/notfound");
return;
}
stringRedisTemplate.opsForValue().set(
String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
shortLinkDO.getOriginUrl(),
LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
);
ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
shortLinkStats(fullShortUrl, shortLinkDO.getGid(), statsRecord);
((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
} finally {
lock.unlock();
}
这一部分的功能主要是,首先从redis根据短链接key来看有没有缓存,如果有就直接查出原始链接的缓存了,如果没有这时我们需要去数据库来查找
而此时需要首先加一个锁,再来查找数据库,查完数据库时要把查询出来的数据放到redis中同时设置过期时间,如果不使用DCL双重判定锁,那么可能一个线程执行完成后,释放锁了,另一个线程卡在了获取锁的过程了,此时缓存中已经有了数据,但是这个线程还是获取到了锁,对数据库进行查询了,因此需要再获取锁后,添加一个判断缓存中是否已经有数据的判断,实现双重判定锁,来减少对数据库的查询和开销。
通过异步更新缓存,保障短链接缓存与数据库之间的数据一致性功能
对应八股,如何保持数据库和缓存之间的一致性?
讲解视频
- 延时双删(针对于先删缓存再删数据库的情况),更改数据库后间隔一定时间后删除redis中的内容,这样就可以防止有的线程发现缓存中没有数据后,又把未更改的数据写会缓存中去
- 使用消息队列,修改数据库后,将要修改的内容放入消息队列中,消费者取出后再进行修改,从而实现异步更新。这样能够达到最终一致性但是无法满足强一致性
- 还有一种是订阅mysql的binlog缓存来进行更新
通过 Redis 完成消息队列消费业务下的幂等场景,保障消息在一定时间内消费且仅消费一次
什么是幂等
redis作为消息队列使用
redis解决幂等问题
相关视频位于:消息队列重构短链接功能
幂等指的是方法被重复多次调用的情况,我们要保证产生的影响和第一次调用产生的影响是相同的,这种问题一般发生于用户重复提交和网络通信中数据丢失出发超时重传
那么解决幂等问题有几种方法:
- 第一种使用唯一id,比如向数据库中插入商品订单,那么订单id好是主键唯一,那么不会重复
- 第二种是利用redis和消息队列,为了避免消息队列中的消息重复消费,在redis中对每个消息队列的消息id使用setnx设置到redis中,这样在消费时可以进行判断,是否消息已经消费国
- 第三种是使用状态机
sentinel
RocketMQ
Kafka
抽奖系统
八股记录
mysql构建分布式数据库
jwt
限流算法
分布式ID
sentinel原理
http状态码
https加密过程
https通过SLS进行加密,其中用到了对称加密和非对称加密的形式
- 客户端生成一个随机数发送到服务器
- 服务器端有自己的公钥和私钥对,然后服务器会把自己的公钥发送到客户端上,服务器还会生生成一个随机数,发送到客户端
- 客户端会生成第三个随机数,也叫做预主秘钥,然后客户端会把这个预主秘钥用公钥加密后发送到服务器上
- 服务器收到加密后的预主密钥后,用自己的私钥解密,
- 然后客户端和服务端都用第一随机数,第二随机数和预主密钥计算出会话秘钥,然后双方使用这个会话秘钥,进行信息加密和解密,来进行对称加密的交流(后来因为会频繁交互,使用非对称加密的形式会导致资源消耗比较高,所以采用了这种方法)
TCP释放链接时等待2MSL
java二维数组排序
Arrays.sort(intervals,new Comparator<int[]>(){
@Override
public int compare(int[] a,int[] b){
if(a[0] > b[0]) return 1;
return -1;
}
});
Python实现给定两个[1,7]中的随机数,生成一个[1,9]的等概率随机数
import random
def generate_random_1_to_9():
# 生成两个[1,7]之间的随机数
rand1 = random.randint(1, 7)
rand2 = random.randint(1, 7)
# 将两个[1,7]的随机数映射到[1,9]
mapped_result = (rand1 + rand2) % 9 + 1
return mapped_result
# 测试
for _ in range(10):
print(generate_random_1_to_9())
docker相关
查看线程信息ps -elf
docker文件挂载命令 docker run -v /host/path:/container/path …
docker端口映射命令 docker run -p host_port:container_port …
docker端口映射的原理
Docker 端口映射的原理涉及到 Docker 的网络模型以及 Linux 内核中的网络命名空间和端口转发机制。
Docker 网络模型:
Docker 默认使用的网络模型是“桥接(bridge)”模式。在这种模式下,Docker 创建一个虚拟的网络桥接口(通常是 docker0),容器连接到这个桥接口上,并且可以相互通信。在端口映射中,Docker 会利用 Linux 的网络命名空间和 iptables 规则来实现将主机上的端口映射到容器内部的端口。
网络命名空间:
Docker 会为每个容器创建一个独立的网络命名空间。网络命名空间是 Linux 内核的一个特性,它将网络设备、IP 地址、路由表等网络资源隔离开来,使得每个命名空间中的网络配置都是独立的。这样,容器之间和容器与宿主机之间的网络不会互相干扰。
端口转发:
Docker 使用 Linux 内核的端口转发机制来实现端口映射。具体来说,Docker 会在宿主机上创建一个 iptables 规则,将主机上指定的端口(如 8888)转发到容器内部的对应端口(如 8080)。这样,当主机收到对 8888 端口的请求时,Linux 内核会将请求转发给容器内的 8080 端口,从而实现了端口映射。
总体来说,Docker 端口映射的原理是利用 Linux 内核的网络命名空间和端口转发机制,在宿主机和容器之间建立起端口映射关系,使得容器内的服务能够通过主机上的端口对外提供访问。
python相关
基本数据结构。 列表(lists) 元组(tuple) 集合(set) 字典(dic) 字符串(Stirng)
python偏函数
IO多路复用
进程线程的区别
执行单位:
进程是程序的执行实例,拥有自己的地址空间、内存、文件描述符等资源。每个进程都是独立的,彼此之间不能直接共享数据,进程之间的通信需要借助于操作系统提供的通信机制(如管道、消息队列等)。
线程是进程内部的执行单元,共享进程的地址空间和资源。一个进程中的多个线程可以访问相同的内存和文件描述符,因此线程之间的通信更加简便,可以直接读写共享的数据。
创建和销毁开销:
进程的创建和销毁通常比较耗费资源,因为每个进程都需要分配独立的地址空间、文件描述符等资源。
线程的创建和销毁比进程轻量级,因为它们共享进程的资源,只需要创建线程私有的执行栈等少量资源。
并发性和并行性:
进程之间的并发性体现在它们在同一时刻可以处于运行、就绪、阻塞等状态,操作系统会根据调度算法来进行进程间的切换,以实现多个进程的并发执行。
线程之间的并发性体现在它们可以同时执行,由于线程共享进程的地址空间,因此线程之间的切换更加快速,可以实现更高效的并发。
调度和同步:
进程之间的调度和同步需要操作系统来进行管理,通常使用进程间通信(IPC)机制来实现进程间的同步和数据传输。
线程之间的调度和同步通常是通过线程同步原语(如锁、信号量、条件变量等)来实现的,因为它们共享相同的地址空间,可以直接对共享数据进行访问和操作。
僵尸进程和孤儿进程和守护进程
Mysql索引失效和Mysql事务隔离级别
- 使用左或者左右模糊查询
- 使用where or子句时,如果前一个是索引后一个不是索引会导致索引失效
- 索引进行表达式求值会造成索引失效
- 索引进行隐式类型转换时会导致索引失效
- 索引进行函数计算时会导致索引失效
- 联合索引不满足最左匹配时会导致索引失效
B树和B+树相关
DNS相关
DNS服务器分为根域名服务器,顶级域名服务器,权威域名服务器
然后查询有递归查询和迭代查询两种方法。
输入url的流程
初始化(当一个机器加入到网络中时)首先通过DHCP(使用UDP)请求报文,向DHCP服务器请求一个IP地址,而DHCP服务器会返回分配的IP地址和DNS服务器的IP地址
下面开始输入一个url的转化过程:
- 首先客户端服务器会生成一个DNS查询报文(DNS是使用UDP的,且端口号为53),而此时这个查询报文需要传递到对应的DNS服务器上,但是这个时候可能还不知道DNS服务器的MAC地址,这时就需要通过ARP协议(发送一个广播请求来获得MAC地址),获得DNS服务器的MAC地址,这样就可将报文发送给对应的DNS服务器了。(这里如果DNS服务器和客户机器不在一个网段中,还需要通过BGP协议等来寻找)
- 当客户端获得了url对应的ip地址后,生成TCP套接字,执行三次握手建立TCP链接,这个生成的套接字用来向对应IP地址发送http报文,之后相应的信息通过BGP协议等,在不同的网段中路由转发,最后到达服务器,服务器抽取出http报文,来生成http响应报文,来返回相应数据,最后这些数据被封装后经过路由转发回到了客户端,至此完成了交互的过程。
如果我输入某个域名,想让他不访问这个ip要怎么办
要阻止计算机访问特定域名对应的IP地址,你可以通过修改操作系统的 hosts 文件来实现。Hosts 文件位于操作系统的系统目录下,用于将域名映射到特定的IP地址。通过编辑这个文件,你可以将指定的域名映射到一个无效的IP地址,从而阻止计算机访问该域名。
排序算法
设计模式
设计模式
饿汉单例是线程安全的
懒汉单例是非线程安全的,需要通过加锁写法
责任链模式
当我们有很多权限代码需要校验的时候,整个代码逻辑会被if…else…所充斥,代码可读性大大降低。所以,这时候我们可以采用职责链的设计模式,对多重校验函数进行合理的解耦和组合,实现开闭原则。
责任链设计模式(Chain of Responsibility Design Pattern)是一种行为型设计模式,其核心理念是将请求沿着处理器链传递,直至被处理。这样可以对请求者和处理者进行解耦,让多个处理对象能够按照顺序处理不同的请求。
优点
请求者与处理者解耦: 使用责任链模式可以消除请求者与具体处理器之间的紧密联系,两者的职责分离,实现了低耦合。
可以动态地改变处理链: 责任链模式允许在运行时添加、删除或重新排序处理器,使处理链更具有灵活性。
易于扩展: 新的处理器可以很容易地通过改变处理链来实现,而不需要修改原有代码。
缺点
性能影响: 如果责任链很长,请求需要经过许多处理器,可能会对性能产生负面影响。
不保证每个请求得到处理: 如果没有处理器能够处理请求,请求可能会被丢弃。设计时要对这种情况进行考虑。
package chain
import (
"errors"
"time"
)
// NodeLogic 定义节点校验逻辑类型
type NodeLogic func(data *GiftCheckHandler) error //节点的处理逻辑(算法)
// NodeProxy 责任链节点
// 由一个代理类来代理控制这个过程。
type NodeProxy struct {
checkFunc NodeLogic //可拔插的节点逻辑:校验逻辑只是一个handler,所以直接用function类型作为参数即可。如果节点逻辑中包含了多个方法需要拔插化,则定义一个接口,利用多态即可。
nextNode *NodeProxy //节点结构 (不能存储NodeProxy,不然结构体分配内存的时候,会无限递归,不知道分配多大内存)
}
// ChainForward 代理了链条节点的流转功能
func (n *NodeProxy) ChainForward(data *GiftCheckHandler) error {
//1. 执行当前节点的逻辑
if err := n.checkFunc(data); err != nil { //如果当前节点有报错,则返回错误
return err
}
//2. 判断是否流转到下一个节点代理类
if n.nextNode != nil {
return n.nextNode.ChainForward(data)
}
return nil
}
// SetNextNode 设置链条的下一节点
func (n *NodeProxy) SetNextNode(m *NodeProxy2) *NodeProxy {
n.nextNode = m
return m //返回下一节点的链条
}
// NewOverduetimeNode 过期时间校验节点
func NewOverduetimeNode() *NodeProxy {
timeCheckFunc := func(data *GiftCheckHandler) error {
//1. 过期时间校验
timeNow := time.Now().Unix()
if data.Overduetime < uint32(timeNow) {
//fmt
return errors.New("礼包已过期,请检查!")
}
return nil
}
return &NodeProxy{
checkFunc: timeCheckFunc,
}
}
// NewStatusCheckNode 构造器
func NewStatusCheckNode() *NodeProxy {
statusCheckFunc := func(data *StatusCheckHandler) error {
// 1. 逻辑处理
if data.GetStatus != 0 {
return errors.New("礼包已经下线!")
}
return nil
}
return &NodeProxy{
checkFunc: statusCheckFunc,
}
}
func main() {
//1. 等待校验信息
data := &GiftCheckHandler{}
//2. 构造责任链(固定的组合可聚合为函数)
timeChekerNode := NewOverduetimeNode()
statusChekerNode := NewStatusCheckNode()
timeChekerNode.SetNextNode(statusChekerNode)
//3. 责任链开始流转校验
timeChekerNode.ChainForward(data)
}
红黑树
定义
定义:
- 红黑树的所有节点的颜色要么是红色的,要么是黑色的
- 红黑树中不能有连续两个红色节点
- 红黑树中从任意一个节点出发,到空叶节点经过的黑色节点数相同
- 红黑树的所有空叶节点(也就是外部节点)都是黑色的
- 红黑树的根节点是黑色的
插入
红黑树的插入:
按照BST的方法,找到对应位置插入,然后将这个节点涂成红色
7. 若这个节点是根节点,那么将这个节点颜色染黑即可
8. 若这个节点的父节点为黑色节点,不需要任何操作
9. 若这个节点的父节点为红色节点,叔叔节点为红色节点,那么将叔叔节点和父节点染成黑色,将爷爷节点染成红色,此时将爷爷节点看作是新插入的节点,递归处理
10. 父节点是红色,叔叔节点是黑色时:
(1) (父节点是左孩子,插入节点也是左孩子)或者(父节点是右孩子,插入节点也是右孩子): 将父节点和爷爷节点颜色互换,然后对爷爷节点进行一次左旋
(2) (父节点是右孩子,插入节点是左孩子)或者(父节点是左孩子,插入节点是右孩子):对父节点进行左旋,然后将父节点看做新插入的节点,递归处理
删除
注意外部节点(空叶节点)不算作是儿子节点
- 当删除节点有两个儿子时,不能直接对这个节点进行删除,需要先用这个点的直接前驱或者直接后继来填补这个点,然后转化为对直接前驱或直接后继的删除
- 若这个节点只有左子树或者只有右子树: 直接删除,同时子树替代自己的位置,并染黑色
- 当这个节点没有子树时:
(1).节点是红色时,直接删除
(2).节点是黑色,兄弟是红色时: 交换节点和父节点的颜色,同时对父节点做一次左旋,然后删除
复杂度分析
进程间通信的方式
Shell脚本相关
git常用命令
git reset 版本号
回退到对应版本号后再通过add commit等来合并版本号
git commit --amend
只能合并两个版本号
git rebase
死锁
死锁的必要条件
- 请求和保持
- 非剥夺
- 循环等待
- 互斥占有
go语言八股
Go中的channel
Go中的select
Go中的recover panic defer关键字
Go中的slice
Go中的Context
Go中map的原理
GMP模型
Go中进程线程协程的区别
Go中的垃圾回收
智力题
赛马问题
问题: 25匹马,有5个赛道,选出前三的最优解是什么
解答:
第一步:25匹马分别编号 A1,A2,A3,A4,A5 B1,B2…
每一组进行比试进行排序,每组淘汰后两名(应为只有3个名额)
每组第一名分别为A1,B1,C1,D1,E1 (共比试5次得出)
第二步:每组第一放一组比试一次,得出第一和淘汰最后两名的列(小组第一都进不了前三,所以这一列都不会有前三产生),eg.假设第一就是A1 B1 C1 D1 E1, 然后D1 E1跑到了最后,那么D1 E1那组的所有人都淘汰掉即可(第二步比试一次)
第三步:假设我们第二步中的比赛顺序是A1>B1>C1>D1>E1,然后A1必定是第一名了,接着判断二三名,因为C1比B1慢(所以C列只可能产生第三名),第二三名产生者可能是 A2,A3,B1,B2,C1 (为什么没有B3?因为此时只剩下两个名额),所以第7次由 A2,A3,B1,B2,C1赛跑,选出前两名,这样就得出前三名
综上,总共跑7次即可决策出前三名
多线程交替打印0-99
public class print {
private static int count = 0;
private static final Object lock = new Object();
//多线程交替打印0-99
public static void main(String[] args) {
System.out.println("Start thread print");
Thread even = new Thread(()->{
while(count < 100){
synchronized(lock){
if(count % 2 == 0){
System.out.println("偶数:"+count++);
lock.notifyAll();
}
try{
if(count < 100){
lock.wait();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
},"evenThread");
Thread odd = new Thread(()->{
while(count < 100){
synchronized(lock){
if(count % 2 == 1){
System.out.println("奇数:"+count++);
lock.notifyAll();
}
try{
if(count < 100){
lock.wait();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
},"oddThread");
even.start();
try{
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
odd.start();
}
}
java实现单例模式
- 懒汉式创建(懒汉式是一种在第一次调用时才创建实例的方式)
懒汉式非线程安全版本:
public class Singleton {
private static Singleton instance; // 声明一个私有静态变量用于存储实例
private Singleton() {} // 私有化构造函数,防止外部创建实例
public static Singleton getInstance() { // 公有静态方法获取实例
if (instance == null) { // 判断实例是否已创建
instance = new Singleton(); // 若未创建,则创建新的实例
}
return instance; // 返回实例
}
}
懒汉式线程安全版本:
public class Singleton {
private static Singleton instance; // 声明一个私有静态变量用于存储实例
private Singleton() {} // 私有化构造函数,防止外部创建实例
public static synchronized Singleton getInstance() { // 公有静态方法获取实例
if (instance == null) { // 判断实例是否已创建
instance = new Singleton(); // 若未创建,则创建新的实例
}
return instance; // 返回实例
}
}
- 饿汉式(饿汉式是一种在类加载时就创建实例的方式)
public class Singleton {
private static Singleton instance = new Singleton(); // 在类加载时创建实例
private Singleton() {} // 私有化构造函数,防止外部创建实例
public static Singleton getInstance() { // 公有静态方法获取实例
return instance; // 直接返回已创建的实例
}
}
- 双重校验锁
public class Singleton {
private static volatile Singleton instance; // 声明一个私有静态变量用于存储实例
private Singleton() {} // 私有化构造函数,防止外部创建实例
public static Singleton getInstance() { // 公有静态方法获取实例
if (instance == null) { // 第一次检查实例是否已创建
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查实例是否已创建
instance = new Singleton(); // 若未创建,则创建新的实例
}
}
}
return instance; // 返回实例
}
}
- 静态内部类
静态内部类是一种在类加载时不会初始化实例,只有在第一次调用getInstance()方法时才会初始化实例,并且不存在多线程安全问题的方式
public class Singleton {
private Singleton() {} // 私有化构造函数,防止外部创建实例
private static class SingletonHolder { // 声明一个私有静态内部类
private static final Singleton INSTANCE = new Singleton(); // 创建实例
}
public static Singleton getInstance() { // 公有静态方法获取实例
return SingletonHolder.INSTANCE; // 返回实例
}
}
java实现三个线程交替打印
package org.example;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
private final AtomicInteger count = new AtomicInteger(0);
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private final Object lock3 = new Object();
public static void main(String[] args) throws InterruptedException {
new Main().multiTurning();
}
public void multiTurning() throws InterruptedException {
Thread t1 = new Thread(new MultiTurningRunner(lock2, lock1, "线程1: 1"));
Thread t2 = new Thread(new MultiTurningRunner(lock3, lock2, "线程2: 2"));
Thread t3 = new Thread(new MultiTurningRunner(lock1, lock3, "线程3: 3"));
t1.start();
t2.start();
t3.start();
}
class MultiTurningRunner implements Runnable {
private final Object nextLock;
private final Object currentLock;
private final String content;
public MultiTurningRunner(Object nextLock, Object currentLock, String content) {
this.nextLock = nextLock;
this.currentLock = currentLock;
this.content = content;
}
@Override
public void run() {
while (count.get() <= 100) {
synchronized (nextLock) {
synchronized (currentLock) {
if (count.get() <= 100) {
System.out.println(content);
count.incrementAndGet();
// 唤醒等待当前锁的线程
currentLock.notifyAll();
}
}
try {
// 如果还需要继续执行,则让出下一个线程对应的锁并进入等待状态
if (count.get() <= 100) {
nextLock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
Go实现三个线程交替打印
package main
import (
"fmt"
"sync"
)
func main() {
// 创建三个 channel,用于控制 goroutine 的执行顺序
ch1 := make(chan struct{})
ch2 := make(chan struct{})
ch3 := make(chan struct{})
var wg sync.WaitGroup
wg.Add(3)
// 启动第一个 goroutine
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
<-ch1
fmt.Println("线程1: 1")
ch2 <- struct{}{}
}
}()
// 启动第二个 goroutine
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
<-ch2
fmt.Println("线程2: 2")
ch3 <- struct{}{}
}
}()
// 启动第三个 goroutine
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
<-ch3
fmt.Println("线程3: 3")
ch1 <- struct{}{}
}
}()
// 启动第一个 goroutine
ch1 <- struct{}{}
// 等待所有 goroutine 完成
wg.Wait()
}