并发编程(一)可见性、原子性、有序性 线程生命周期 最佳线程数

参考: https://mp.weixin.qq.com/s/Xv8Ho0Q2gHTi5UQXSIxamQ

本文讲的并发编程,并非指分布式技术中的高并发,采用水平扩展或者垂直拆分实现,将并发流量分到不同的物理服务器上。而是将服务器的服务性能最大化,提升cpu的利用率,从而加快响应速度和增大吞吐量,主要技术包括多线程。

并发编程,绕不开的三个核心问题:分工、同步、互斥

分工,指的是如何高效地拆解任务并分配给各个线程;

同步,指的是线程之间如何协作;

互斥,则是保证同一时刻只允许一个线程访问共享资源;

并发编程是围绕着信号量(Semaphore-pv原语) 和管程(monitor)的,这2者区别在于Semaphore可以允许多个线程访问一个临界区:new Semaphore(size)。 java更倾向于管程技术。这个我们并不陌生,synchronized关键字、wait、notify、nofityAll方法就是管程的重要组成部分。 另外,管程采用的是MESA模型。

我们遇到的很多并发编程,无外乎就是多个线程同时读写同一数据产生的bug。可以最终归为安全性问题,比如方法和类不是线程安全的,导致了程序没有按照我们所期望的那样执行。产生这些并发bug的源头 是可见性、原子性和有序性的问题。为了解决这三者问题,你需要知道了解 java内存模型(如图1)、互斥锁方案、 happens-before规则。

在这里插入图片描述

                                           图1 JMM

可见性问题:

真实案例:假设2核,线程A修改了cpu1上的某变量M,线程B修改了cpu2上的该变量M值,这个时候线程A和B对变量M的修改是相互不可见的,这就导致了结果不是你的预期值。

导致原因:高速/本地缓存

解决方法:synchronized、volatile

原子性问题:

真实案例:int a = 0; 开10个线程,执行 a++; 最终a是小于10的

导致原因:线程切换

解决方法:synchronized、Lock等互斥锁

有序性问题:

真实案例:

单例模式

public class Singleton {

private Singleton() { }

private static Singleton instance;

public Singleton getInstance(){

    if(instance==null){

        synchronized (Singleton.class){

            if(instance==null){

                instance = new Singleton();

            }

        }

    }

    return instance;

}

}

看起来没问题,但是实际上可能导致的NullPointerException

导致原因:编译优化导致的指令重排,1.分配对象的内存空间;2.设置instance指向刚分配的内存地址 3.初始化对象;在步骤2之后,3之前,虽然instance!=null,但是实际去用的时候报错了。

解决方法:synchronized、volatile( private volatile static Singleton instance;)

上面提到的互斥锁,它是解决并发问题的核心工具,但它可能会因为竞争共享资源带来死锁问题,

死锁4个同时条件:互斥,占有且等待,不可抢占,循环等待

当然,我们只要破坏其中之一,即可避免死锁:

互斥,因为共享资源同一时刻只能被一个线程占用,这个不可避免。

占有且等待,可以一次性锁全部共享资源,就不存在等待了(方法有点粗暴~~~)。

不可抢占,类似tryLock方案,等待资源超时就释放已占有资源。

循环等待,synchronized配合wait,notify,notifyAll实现等待-通知机制。或者,给资源id排序,利用顺序锁。

实现并发主要技术是多线程,这里说下java中线程的生命周期(跟通用线程略有点点不同)

  1. 初始化状态:new

  2. 可运行/运行状态 runnable

  3. 休眠状态 blocked、waiting、timed_waiting

  4. 终止状态 terminated

线程的状态之间是可以相互转换的,从而达到并发效果:

new->runnable : extends Thread 、 implements Runnable

runnable->blocked: 等待synchronized隐式锁,此时别的线程正在执行synchronized块

blocked->runnable:获得了synchronized隐式锁后

runnable->waiting: obj.wait() 、ThreadA.join() 、 LockSupport.park

waiting->runnable: notify、nofityAll、ThreadA执行完、LockSupport.unpark

runnable->terminated: run()执行完、ThreadA.interrupt + isInterrupt自检

多线程既然对提升服务器性能这么有用,不过并不是越多越好,因为线程间切换是需要时间的,有个计算线程数公式是这样子的:

最佳线程数 = cpu核数 * (1 + io耗时/cpu耗时)

如果是大批量浮点型计算,1核单线程即可

如果是纯文件读写型,那么线程数可以设置较大。

上面讲到很多概念和理论,也许你会觉得并发编程很难,其实

java sdk中,有专门的并发包:java.util.concurrent,例如

分工: Fork/Join 框架;

同步: CountDownLatch;

互斥: ReentrantLock。

如果我们想要熟练使用,还是得花费很大功夫去好好理解和学习。

下列章节会介绍一些concurrent中常见的并发类和方法,从实践出发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值