并发编程之安全性、活跃性与高性能

前面我们介绍了可见性、原子性与有序性是导致并发问题的根源。那么并发编程有什么指导原则呢?安全性、活跃性与高性能就是宏观上的指导原则。

安全性

相信你经常会提起某某方法是不是线程安全?那么什么是线程安全的呢?其本质就是正确性,正确性指的是线程能够按照我们期望的方式执行。前面我们介绍了可见性、原子性与有序性是导致并发问题的根源。那么是不是所以的代码都要关注可见性、原子性与有序性呢?我们只需要关心那种存在共享且数据会发生变化的场景,也就是存在多个线程同时读写共享变量的场景。那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了嘛。有不少技术方案都是基于这个理论的,例如线程本地存储等
但是实际使用的很多场景下会有不少的情况是有多个线程需要访问共享数据,并且至少有一个线程会修改共享数据,如果不采取措施那么会导致bug,这种情况我们称之为数据竞争。如下的程序当有多个线程同时访问时,结果就不可预测

static int count = 0;
void func()
{
	for(int i=0; i<10000; ++i)
	{
		++count;
	}
}

这个方法我们按如下的方式通过加锁已解决问题

static int count = 0;
void func()
{
	lock();
	for(int i=0; i<10000; ++i)
	{
		++count;
	}
	unlock();
}

那是不是所有的并发问题都通过简单的添加一个锁就可以解决呢?显然不是,比如如下的代码

static int count = 0;
int get()
{
	lock();
	int temp = count;
	unlock();
	return temp;
}
void set(int value)
{
	lock();
	count = value;
	unlock();
}
void func()
{
	set(get()+1);
}

虽然获取与更新count的方法都添加了锁,但是当第一个线程先调用fun函数,第二个线程在调用fun函数,那么结果是2,但是如果2个线程同时调用func函数,当2个线程都先调用了get方法获取值,那么2个线程获取到的值都是0,当各自调用set函数时,都把count更新成了1。显然函数的运行结果依赖于函数的调用顺序,我们把这种执行结构依赖于函数的调用顺序的问题称之为竞态条件。
我们可以通过锁来解决竞态条件与数据竞争的问题。我的高并发编程专栏的博客里面已经有文章介绍锁了,这里就不在赘述。

活跃性

活跃性主要包括三个方面:死锁、活锁与饥饿。

死锁需要满足如下的条件:

互斥,共享资源 X 和 Y 只能被一个线程占用;
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待;
不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X。
我们只需要破坏其中一条就可以避免死锁。
对于第一条互斥,显然是必须满足的,因为对于共享资源,是需要互斥访问的;
对于第二条循环等待,可以通过按固定的顺序申请锁;
对于第三条不可抢占,一般通过设置等锁的超时时间来解决,如果超时则释放已经申请到的锁;
最后一条占有且等待,我们一般通过第三方申请到全部资源后,在申请锁。
占有且等待,由于引入了第三方,会影响系统性能,而多线程就是希望解决性能问题,所以不经常使用,我们一般通过破坏循环等待与不可抢占条件来解决死锁问题。
那么系统如果真的死锁了,应该怎么办呢?我们一般只能通过重启应用程序,在重启前可以用pstack等堆栈分析工具找到死锁代码。

活锁

我们通过超时时间来破解占有且不可抢占的问题,但如果超时时间设置的都一样,那么2个线程可能会同时申请锁,同时超时释放锁,又同时申请锁,从而导致死循环,这就是活锁。为此我们一般通过设置随机超时时间来解决。

饥饿

当系统的资源紧张时,如果不同的线程优先级不同,那么优先级低的线程可能会长时间获取不到锁,从而导致饥饿问题。我们一般通过公平锁来解决这个问题,公平锁通过一个先进先出的队列来实现。

高性能

我们使用多线程编程的目的就是为了支持高并发,为此我们使用锁的时候,一定需要考虑性能问题,并不是遇到共享问题,不管三七二十一就直接加锁。我们知道锁是通过串行化共享资源来实现原子性的。串行化的执行时间越长程序的并发性就越低,故我们加锁时需要提高并发降低串行,以实现更高的性能。
那么我们如何才能提高性能呢?
1、既然锁会降低性能,那么我们能不能不用锁呢?我们可以使用原子变量、线程本地存储、写时复制、常量、无锁编程等技术;
2、使用细粒度的锁,提高并发性,比如自旋锁、读写锁、stampedlock、段锁、行锁等,但是使用更细粒度的锁,编程难度就更大,可能会导致安全性、活跃性问题,故我们还需要综合考虑实际场景,是否真的需要那么高的并发性。
既然锁宏观上需要考虑安全性、活跃性与高性能问题,微观上需要考虑可见性、原子性与有序性,那么用锁时是否有什么规范呢?
1、锁应该跟面向对象的编程一样,需要封装好共享变量与锁,永远只在更新对象的成员变量时加锁,永远只在访问可变的成员变量时加锁,永远不在调用其他对象的方法时加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值