并发编程学习——8 学习笔记-多线程影响性能的因素

线程最主要的目的是提高程序的运行性能,提升性能的技术同样会增加复杂性,因此也就增加了安全性和活跃性上发生失败的风险。

错误的线程使用

无效加锁

有时候我们尝试使用方法中的对象做为锁的时候,这个锁其实是没有起作用的。

    public void test() {
        Object o = new Object();
        synchronized (o) {
            // do same
        }
    }

有风险的锁

另外一种情况就是早先我们尝试使用一个固定的字符串来作为锁,因为在较早版本一般认为字符串是放在堆里,一样的字符串会指向同一个地址

    public void test2() {
        synchronized ("lock") {
            // do same
        }
    }

这样看起来是进行了加锁,但是存在一个隐患,当时间日积夜累后,接手的开发者也有这种开发习惯的时候,他尝试加锁用的字符串和你的相同时候,这个时候在未来的某个时刻两个线程同时竞争一把锁,假如碰巧两个线程存在一种依赖关系,此时可能导致系统的死锁。

死锁

每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得所有需要的资源之前都不会放弃已经拥有的资源

JVM在解决死锁问题时,当一组Java线程发生死锁时,这些线程永远不能再使用了。可能造成应用程序完全停止或者某个特定的子系统停止,或者性能降低。恢复应用程序的唯一方式就是中止并重启它。

锁顺序死锁

两个线程试图以不同的顺序来获得相同的锁,如果按照相同的顺序请求锁,那么就不会出现循环的加锁依赖性,因此也就不会产生死锁。

public class DeadLock {
    
    private final Object ONE = new Object();
    private final Object TWO = new Object();

    public void getOne() {
        synchronized (ONE) {
            synchronized (TWO) {
                // doSome
            }
        }
    }

    public void getTwo() {
        synchronized (TWO) {
            synchronized (ONE) {
                // doSome
            }
        }
    }
}

资源死锁

除了多个线程互相持有彼此时会发生死锁,当它们在相同的资源集合上等待时也会发生死锁。

活锁

活锁是另外一种问题,一般是错误的业务处理逻辑。比如:如果不能处理某个任务的时候,处理机制回滚整个事务,并将它重新放入队列前方,假如这个任务因为某种原因导致它必定失败,那么每当线程从队列中取出这个任务,都会错误并回滚,虽然线程没有阻塞,但是其他任务却也无法执行

线程引入的开销

使用多个线程总会引入一些额外的性能开销,这些开销的操作包括:线程之间的协调(加锁、触发信号以及内存同步),增加的上下文切换,线程的创建和销毁,以及线程的调度。过多的线程将会使这些开销超过线程带来的性能提升。

重复的上下文切换

如果有多个线程同时请求锁,那么JVM就需要借助操作系统的功能。如果出现了这种情况,那么一些线程将被挂起并且在稍后恢复允许。当线程恢复执行时,必须等待其他线程执行完他们的时间片之后,才能被调度执行。在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。

如果主线程是唯一的线程,那么它基本上不会被调度出去。如果可运行的线程数大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU。这将导致一次上下文切换:保存当前线程运行的上下文,并将调度进来的线程上下文设置为当前上下文。

当线程由于等待某个发生竞争的锁而被阻塞时,JVM通常会将这个线程挂起,并允许它被交换出去。在程序中发生越多的阻塞与CPU密集型的程序就会发生越多的上下文切换,从而增加调度开销,并因此而降低吞吐量。

内存同步

同步操作的性能开销包括多个方面,在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令。如:栅栏。内存栅栏可以刷新缓存,使缓存无效刷新硬件的写缓冲,以及停止执行通道。它也会抑制一些编译器的优化操作。

阻塞

JVM 在实现阻塞行为时,可以采用自旋等待或者通过操作系统挂起被阻塞的线程。如果等待时间较短,则适合采用自旋等待方式,而如果等待时间较长,则适合采用线程挂起方式。某个条件等待的时候需要被挂起,过程中将包含两次额外的上下文切换,以及所有必要的操作系统操作和缓存操作

性能提升和可伸缩性

线程最主要的目的是提高程序的运行性能,提升性能的技术同样会增加复杂性,因此也就增加了安全性和活跃性上发生失败的风险。

对性能提升的量化标准

  • 服务时间
  • 延迟时间
  • 吞吐率
  • 效率
  • 可伸缩性以及容量

提升性能

减少锁的竞争

两个因素将影响在锁上发生竞争的可能性;

  • 锁的请求频率
  • 以及每次持有该锁的时间

有三种方式可以降低锁的竞争度

  • 减少锁的持有时间
  • 降低锁的请求频率
  • 使用带有协调机制的独占锁3

缩小锁的范围

降低发生竞争的可能性的一种有效方式就是竟可能缩短锁的持有时间。当然尽管缩小同步代码块可能提高可伸缩性,但是同步代码块也不能过小,一些需要原子方式执行的操作必须包含在一个代码块中。

减小锁的粒度

另一种减小锁持有时间的方式是降低线程请求锁的频率,采用多个互相独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况,这些技术能减少锁操作的颗粒度,能实现更高的可伸缩性。当然是用更多的锁也代表着更高的死锁风险。

锁分段

如同并发容器中说的技术,可以将锁分解技术进一步扩展为一组对象上的锁进行分段。是用锁分段保护一组对象的某一部分。当然锁分段的问题在于和使用单个锁实现独占访问时,要获取多个锁来实现独占访问将更加困难并且开销更高。

总结

关于多线程的优化,只能总结这么多了。一个是实际工作中面对的并发压力的确没这么大,第二个是实际工作中需要进行的优化经验也不算多。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值