技术

如何使用RedisTemplate访问Redis数据结构 MySQL重要知识点 OAuth2认证授授权流程 分布式锁 服务调用 MQ的介绍 SpringCloud 使用链 Eureka 的点对点通信 介绍Eureka RabbitMQ与其它MQ的对比 Springboot 启动过程分析 Springboot 入门 Linux内存管理 自定义CNI IPAM 扩展Kubernetes 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 Kubernetes安全机制 jvm crash分析 Prometheus 学习 Kubernetes监控 Kubernetes 控制器模型 容器日志采集 容器狂占cpu怎么办? 容器狂打日志怎么办? Kubernetes资源调度-scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects之编排对象 源码分析体会 自动化mock AIOps说的啥 从DevOps中挖掘docker的价值 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes整体结构 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 kubernetes实践 线程排队 jib源码分析之细节 从一个签名框架看待机制和策略 跨主机容器通信 jib源码分析及应用 docker环境下的持续构建 docker环境下的持续发布 一个容器多个进程 kubernetes yaml配置 marathon-client 源码分析 《持续交付36讲》笔记 程序猿应该知道的 mybatis学习 无锁数据结构和算法 《Container-Networking-Docker-Kubernetes》笔记 活用linux 命令 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 swagger PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 《深入剖析kubernetes》笔记 精简代码的利器——lombok 学习 java 语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 JVM4——《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 commons-pipeline 源码分析 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS3——论文学习 Unsafe Spark Stream 学习 linux 文件系统 mysql 批量操作优化 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 forkjoin 泛谈 hbase 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 calico 问题排查 bgp初识 mesos 的一些tips mesos 集成 calico calico AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 compensable-transaction 源码分析 硬件对软件设计的影响 elasticsearch 初步认识 mockito简介及源码分析 线上用docker要解决的问题 《Apache Kafka源码分析》——Producer与Consumer 停止容器 dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 java系并发模型的发展 从一个marathon的问题开始的 docker 环境(主要运行java项目)常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 netty中的future和promise 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 一些tricky的code http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 JVM3——java内存模型 java concurrent 工具类 java exception java io涉及到的一些linux知识 network channel network byte buffer 测试环境docker化实践 通用transport层框架pigeon netty(七)netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 从Go并发编程模型想到的 mesos深入 Macvlan Linux网络源代码学习2 《docker源码分析》小结 对web系统的一些理解 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习 Docker网络五,docker网络的回顾 zookeeper三重奏 数据库的一些知识 Spark 泛谈 commons-chain netty(六)netty回顾 Thrift基本原理与实践(三) Thrift基本原理与实践(二) Thrift基本原理与实践(一) Future 回调 Docker0.1.0源码分析 基于spring boot和Docker搭建微服务 通过Docker Plugin来扩展Docker Engine java gc Docker网络四,基于Centos搭建Docker跨主机网络 google guava的一些理解 Jedis源码分析 Redis概述 Docker回顾 深度学习是个什么鬼 Docker网络三,基于OVS实现Docker跨主机网络 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 netty(四)netty对http协议的实现(废弃) netty(三)netty框架泛谈 向Hadoop学习NIO的使用 以新的角度看数据结构 AQS1——并发相关的硬件与内核支持 使用Ubuntu要做的一些环境准备 Docker网络二,libnetwork systemd 简介 那些有用的sql语句 异构数据库表在线同步 spring aop 实现原理简述——背景知识 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 我们编程的那些潜意识 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 一次代码调试的过程 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 docker volume 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM2——JVM和传统OS对比 git spring rmi和thrift maven/ant/gradle使用 再看tcp mesos简介 缓存系统——具体组件 缓存系统 java nio的多线程扩展 多线程设计模式/《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go 常用的一些库 Netty(一)初步了解 java mina Golang开发环境搭建(Windows下) java nio入门 ibatis自动生成类和文件 Python初学 Goroutine 调度模型猜想 一些编程相关的名词 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Hadoop安装与调试 Kubernetes持久化存储 Kubernetes 其它特性 访问Kubernetes上的服务 Kubernetes副本管理 Kubernetes pod 组件 使用etcd + confd + nginx做动态负载均衡 nginx安装与简单使用 在CoreOS集群上搭建Kubernetes 如何通过fleet unit files 来构建灵活的服务 CoreOS 安装 定制自己的boot2docker.iso CoreOS 使用 Go初学 JVM1——jvm小结 硬币和扑克牌问题 LRU实现 virtualbox 使用 os->c->java 多线程 容器类概述 zabbix 使用 zabbix 安装 Linux中的一些点 关于集群监控 ThreadLocal小结 我对Hadoop的认识 haproxy安装 docker快速入门

标签


os->c->java 多线程

2014年10月09日

前言

建议先看下java系并发模型的发展

2018.03.15补充

线程安全,是并发访问(也就是读写)资源导致的,分为读读、读写、写写三种情况,有以下几个点:

  1. 读读无所谓,写写开发者一般有明显的意识要加锁,通常出问题在读写。
  2. 读写通常从代码上看不出来,比如i++,jvm将其编译为8个指令,读取、加1、赋值。
  3. 对于long型数据,jvm约定64位值的赋值操作需要两次32位赋值。
  4. 所以,两个线程对一个变量并发执行i++,都有上万中执行路径
  5. 以上只针对最简单的cpu ==> 内存硬件架构,考虑到cpu 通常还会有多级缓存,读写的问题就更严重了。

所以,并发问题,具体的说就是安全的并发读写。

  1. 跟内存模型有关系,比如cpu是否带有缓存
  2. 跟编译系统有关系,编译器将一条代码翻译成多少条指令,是否夹杂了指令重排序
  3. 跟指令系统有关系,os或vm 将哪些操作作为原子操作

数据库系统中,为了描述并发读写的安全程度,还提出了隔离性的概念。

所以,不准确的说,并发读写是线程安全的核心问题。也正是如此,cas (部分)解决了读写问题,java.util.concurrent 以CAS为基石(实际上线程并没有停止运行),不使用锁(意味着内核陷入,和竞争时的上下文切换),就可以解决大部分并发问题。反过来也说明,并发读写问题是线程安全的核心问题。

2018.10.22 补充:关于线程、并发在硬件、os、jvm等的演变,参见AQS1——并发相关的硬件与内核支持

锁的粒度

public class IntegerIterator implements Iterator<Integer>{
    private Integer nextValue = 0;
    public synchronized boolean hasNext(){
        return nextValue < 10000;
    }
    public synchronized Integer next(){
        if(nextValue = 10000)
            throw new IteratorPastEndException();
         return nextValue++
    }
    public synchronized Integer getNextValue(){
        return nextValue;
    }
}

操作系统中的线程

单线程

我们如何影响线程的行为呢?

  1. 创建它:继承Thread,实现Runnable,实现TimerTask(现在不推荐)
  2. 启动它:start
  3. 暂停它(唤醒它):sleep(自动唤醒),wait和notify
  4. 停止它(取消它):

    a. interrupt,注意这种停止并不是抢占式的,代码中要遵守一定的约定。

    b. 设置一个变量(显式的interrupt)

     class thread{
         public boolean isRun = "true";
         void run(){
              while(isRun){
                  xx
              }
         }
         void stop(){
             isRun = false;
         }
     }
    

多线程

多个线程同时执行时,有以下几种可能:

  1. 乱序执行

    Alt text

  2. 协作执行(部分有序)

    • 某一部分不能被中断

      Alt text

    • 部分有序的

      Alt text

  3. 有序执行(执行完一个,再执行另一个)

    比如java的join方法

So,从乱序、部分有序、到有序执行,这是一个渐近的过程。这也从另一个侧面证明,java的多线程程序,本质上就是在线性程序上加了一些限定。

从另一个角度讲,找出串行化代码的可并行部分,并将其分解到各个线程中,本身就是一种降低程序复杂度的方式。考虑到多线程还有线程切换和同步的开销,串行代码并行化,并不是越多越好。

这比起现在新兴的、原生支持多核和并行化的编程语言(比如Go语言),逊色不少。

那么在串行代码并行化的过程中,带来了一些问题(集中在线程交叉的部位)。因为涉及到java内存模型等方面,这个复杂性并不比当年单道OS改成支持多道任务的OS时低。

  1. 原子性,有些操作一旦开始执行就不能被打断,在多线程领域则是某个时刻只能有一个线程进入临界区。
  2. 可见性,一个线程对变量或对象的改变必须及时让其它线程知道。而每个线程都有自己的缓存,不加处理的话就会破坏这一点。
  3. 有序性。某些操作必须是有序的。比如只有当jvm初始化对象的线程将对象初始化完毕了,才允许其它线程访问该对象。而java代码编译和执行时会进行指令重排序,一个方法内后面的代码有可能跑到前面执行,cpu也可能在还未执行完一个方法时,执行下一个方法的指令(因为cpu只看到一个指令序列,而不是人类看到的代码执行和方法跳转)。

C语言中的多线程

看看C语言下写多线程程序什么感觉

    #include <stddef.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>		//用到了pthread库
    #include <string.h>
    void print_msg(char *ptr);
    int main(){
        pthread_t thread1, thread2;
        int i,j;
        char *msg1="do sth1\n";
        char *msg2="do sth2\n";
        pthread_create(&thread1,NULL, (void *)(&print_msg), (void *)msg1);
        pthread_create(&thread2,NULL, (void *)(&print_msg), (void *)msg2);
        sleep(1);
        return 0;
    }
    void  print_msg(char *ptr){
        int retval;
        int id=pthread_self();
        printf("Thread ID: %x\n",id);
        printf("%s",ptr);
        // 一个运行中的线程可以调用 pthread_exit 退出线程, 参数表示线程的返回值
        pthread_exit(&retval);
    }

pthread_create 四个参数

  1. 线程对象
  2. 线程的属性,比如线程栈大小
  3. 线程运行函数
  4. 线程运行函数的参数

从c语言中线程的代码实例和操作系统的基本原理(进程通常是执行一个命令,或者是fork),我们可以看到,线程可以简单的认为是在并发执行一个函数(pthread_create类似于go 代码中常见的go function(){xxx})。

java提供的多线程

  1. 从代码的感觉上讲,我经常很困惑,比如

     class ThreadA extends Thread{
         public void run(){
             codeA;
             threadb.join();    //threadB.join()的意思是向threadB发送jion消息,加入到当前线程中来,threadB完事执行codeB;
             codeB;
         }
     }
     class MyThread extends Thread{
         public void run(){
             synchronized(b){
                  xx
                  b.wait();    //此处是将当前线程挂起
             }
         }
     }
    

我的直观感觉是:挂起当前线程,应该是当前线程thread应提供一个方法wait,然后thread.wait()。而事实是通过锁对象的wait方法做到的,也就是通过非线程对象的操作改变了线程对象的状态,我在ThreadLocal小结中也提到了类似的情形。原因便是:任何一个方法在执行时都可以通过Thread.currentThread()获取当前线程对象,从而通过线程对象对线程进行一定操作。如果这种感觉不爽,可以显式的使用Condition的await的和signal方法。

java线程池

笔者刚实习的时候,创建一个线程用的是new thread(xx).start()。那么从代码结构的设计等角度,应该减少这类使用,更多应该考虑

  1. 通过线程池控制代码的并发量
  2. 如何优雅的与spring等框架结合,比如使用quartz(这样还可以更好的控制线程的生命周期,以及事件通知等)

对于一个简单的java线程池

  1. 线程池初始化时,即启动其管理的所有线程
  2. 线程池中的线程运行逻辑:

    1. 从任务队列中取任务(取不到任务会阻塞)
    2. 执行任务
    3. 转至步骤1

注意:

  1. 线程池中的线程跟线程池要执行的任务(我们习惯为让任务成为一个Runnable对象,但实际上这个任务是什么类型都可以)运行逻辑不一样。

  2. 线程池中的线程所执行的任务,不是线程池赋予的,而是线程自己“取”的

线程创建的成本

2018.7.7 补充:线程池的原理 我们首先来看,为什么说每次处理任务的时候再创建并销毁线程效率不高?

Thread t = new Thread();	// 此时只是在java 层面创建了一个对象
t.start()	

native 的start 指令做了很多事情

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
{
	MutexLocker mu(Threads_lock);
	if (java_lang_Thread::is_stillborn(JNIHandles::resolve_non_null(jthread)) ||
java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {	
		throw_illegal_thread_state = true;
	} else {
		jlong size =	java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
		size_t sz = size > 0 ? (size_t) size : 0;
		native_thread = new JavaThread(&thread_entry, sz);
		if (native_thread->osthread() != NULL) {
			// Note: the current thread is not being used within "prepare".
			native_thread->prepare(jthread);
		}
	} 
}
Thread::start(native_thread);
JVM_END

这段代码我也不懂,只是想表明, native 做了很多事情。包括但不限于:

  1. 创建一个native 线程
  2. 分配线程栈。jvm 参数-Xss,每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右.小的应用,如果栈不是很深,128k应该是够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。从这里可以看到两点:

    1. 如果xss不显式设置, 新建线程时 os分配1m的空间绝对不是一个很easy的操作
    2. 线程数量 不准确的说 是一个内存耗费问题,在这个角度看,空间和算力有了一个对应关系。
  3. 将java 线程 关联到 native 线程上

从中可以看到:尽管java 线程和 os 线程具备一对一关系,但java 仍在jvm 层面上 为线程 维持了一些 数据结构。就好像 线程池中的线程 不是单纯的 new Thread,java 线程 也不是 单纯的 os 线程。

如果没有这些微观细节,人就很难直观上 感受 线程池的好处。 线程切换的成本

2019.5.27补充:Linux内核基础知识

Java/Jvm 内部工作线程

2018.7.2 补充

java 内部工作线程介绍哪怕仅仅 简单的跑一个hello world ,java 进程也会创建如下线程

"Low Memory Detector" 
"CompilerThread0"
"Signal Dispatcher"
"Finalizer"
"Reference Handler"
"main" 
"VM Thread"
"VM Periodic Task Thread"

笔者有一次,试验一个小程序,main 函数运行完毕后,idea 显示 java 进程并没有退出,笔者还以为是出了什么bug。thread dump之后,发现一个thread pool线程在waiting,才反应过来是因为thread pool 没有shutdown。进而Java中的main线程是不是最后一个退出的线程

  1. JVM会在所有的非守护线程(用户线程)执行完毕后退出;
  2. main线程是用户线程;
  3. 仅有main线程一个用户线程执行完毕,不能决定JVM是否退出,也即是说main线程并不一定是最后一个退出的线程。

这也是为什么thread pool 若没有shutdown,则java 进程不会退出的原因。

java并发小结

java并发的发展历程

  1. 使用原始的synchronized关键字,wait和notify等方法,实现锁和同步。

  2. jkd1.5和jdk1.6提供了concurrent包,包含Executor,高效和并发的数据容器,原子变量和多种锁。更多的封装减少了程序员自己动手写并发程序的场景,并提供lock和Condition对象的来替换替换内置锁和内置队列。

  3. jdk1.7提供ForkJoinTask支持,还未详细了解,估计类似于MapReduce,其本身就是立足于编写可并行执行程序的。

通过阅读《java并发编程实战》全书的脉络如下

  1. 什么是线程安全,什么导致了线程不安全?
  2. 如何并行程序串行化,常见的并行化程序结构是什么?Executor,生产者消费者模式
  3. 如何构造一个线程安全的类(提高竞争效率),如何构造一个依赖状态的类(提高同步效率)?提高性能的手段有哪些? 使用现有工具类 or 扩充已有父类?

性能优化的基本点就是:减少上下文切换和线程调度(挂起与唤醒)操作。从慢到快的性能对比:

  1. synchronized操作内置锁,wait和notify操作内置队列。考虑到现在JVM对其实现进行了很大的优化,其实性能也还好。
  2. AQS及AQS包装类
  3. Lock和Condition(如果业务需要多个等待线程队列的话)

从上到下,jvm为我们做的越少,灵活性越高,更多的问题要调用者自己写在代码里(执行代码当然比劳烦jvm和os效率高很多),使用的复杂性越高。

基于共享内存的数据通信问题

线程安全/对数据进行保护/共享变量副本一致性

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。因为缓存(cpu缓存、基于java机制的线程工作内存)等原因,可见性并不是天然保证的。

线程的运行需要一定的资源,硬件如打印机、磁盘、数据库和显示屏等,软件如变量等数据结构(其实就是某个特定的内存资源),因为大家都在使用,所以并不能确保资源“申请即得到(这里描述为“可以访问”)”,“得到即可用(这里描述为可以使用)”。硬件资源的分配,由操作系统提供。而软件资源的分配与协调则由开发人员通过代码主动控制。

基于内存的共享变量 + CPU 缓存结构 ==> 共享变量存在多个副本/”cpu本地变量” ==> 语言层级的副本一致性问题 ==> CAP 问题 比如

  1. 可见性 volatile 其实就类似主备同步中的副本一致性:只有数据同步到备机了,才算对数据操作成功。
  2. 有序性,其实就是CAP 的操作顺序一致性。编译/运行时重排序 和 网络通信中的“后发先至”异曲同工。

从某种视角来看,共享内存模型下的并发问题都是“共享内存,但不共享寄存器/L1/L2缓存”导致的,线程安全性是某种意义上的副本一致性

几点感觉:

  1. 读的一定要是(自己或别的线程)最近写入的数据
  2. 线程A写入变量时,它还是线程A读取时的值

条件变量

有了互斥锁,为什么还要条件变量?

假如我们没有“条件变量”这个概念,如果一个线程要等待某个“自定义的条件”满足而继续执行,而这个条件只能由另一个线程来满足,比如 T1不断给一个全局变量 x +1, T2检测到x 大于100时,将x 置0,如果我们没有条件变量,则只通过互斥锁则可以有如下实现:

//thread 1:
while(true){
    pthread_mutex_lock(&mutex);
    iCount++;
    pthread_mutex_unlock(&mutex);
}

//thread 2:
while(true){
    pthread_mutex_lock(&mutex);
    if(iCount >= 100){
        iCount = 0;
    }
    pthread_mutex_unlock(&mutex);
}

这种实现下,就算 lock 空闲,thread2需要不断重复<加锁,判断,解锁>这个流程,会给系统带来不必要的开销。有没有一种办法让 thread2先被 block,等条件满足的时候再唤醒 thread2?这就要用到条件变量了:

//thread1 :
while(true){
    pthread_mutex_lock(&mutex);
    iCount++;
    pthread_mutex_unlock(&mutex);
    pthread_mutex_lock(&mutex);
    if(iCount >= 100){
        pthread_cond_signal(&cond);
    }
    pthread_mutex_unlock(&mutex);
}
//thread2:
while(1){
    pthread_mutex_lock(&mutex);
    while(iCount < 100){
        pthread_cond_wait(&cond, &mutex);
    }
    printf("iCount >= 100\r\n");
    iCount = 0;
    pthread_mutex_unlock(&mutex);
}

锁与同步,本质上都是通过程序,人为的改变线程的状态(由运行改为挂起),都是通过线程的挂起和恢复机制

  1. 从表象上看,锁的lock和unlock在一个代码块中,而同步的wait和notify在不同的代码块。锁,侧重于两个线程的竞争。同步,侧重于两个线程的协作
  2. 锁只是负责资源的独占访问,但该资源是否语义上可用,并不保证。比如现在没有其他线程访问资源池,消费者线程可以访问资源池,但资源池没有资源时,消费者线程也是需要等待的。因而锁的应用场景比较通用和固定,程序语言可以方便的进行抽象,比如java的synchronized。
  3. 线程同步,确保了线程在语义上可以使用共享资源。当线程访问共享资源,检测语义不满足(标志位被占用),会被挂起,需要协作线程满足语义后,触发当前线程的继续执行。但因为应用场景多种多样,所以由开发人员手动写入线程挂起和恢复代码。
  4. 但线程同步时判断资源在语义上是否可用之前,必须先锁定(保护)描述语义的变量值。管理状态依赖性的机制必须与确保状态一致性的机制(锁)关联起来(来自《Java并发编程实战》)