iOS多线程知识梳理

线程进程基础概念

进程

进程是指在系统中正在运行的一个应用程序

每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内

线程

1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程,称为主线程)

一个进程(程序)的所有任务都在线程中执行

比较

1.线程是CPU调用(执行任务)的最小单位。

2.进程是CPU分配资源的最小单位。

3.一个进程中至少要有一个线程。

4.同一个进程内的线程共享进程的资源。

多线程概述

1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务

多线程技术可以提高程序的执行效率

原理

同一时间,CPU只能处理1条线程,只有1条线程在工作(执行),多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。

那么如果线程非常非常多,会发生什么情况?

CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,同时每条线程被调度执行的频次也会会降低(线程的执行效率降低)。

因此我们一般只开3-5条线程

优缺点

多线程的优点

能适当提高程序的执行效率

能适当提高资源利用率(CPU、内存利用率)

多线程的缺点

创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间

如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。

程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题

多线程应用

主线程的主要作用

显示\刷新UI界面

处理UI事件(比如点击事件、滚动事件、拖拽事件等)

主线程的使用注意

别将比较耗时的操作放到主线程中

耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验

将耗时操作放在子线程中执行,提高程序的执行效率

iOS 开发中的几种线程知识点

pthread(c)

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32

具体使用

//创建线程
pthread_t thread;
/*
第一个参数pthread_t *restrict:线程对象
第二个参数const pthread_attr_t *restrict:线程属性
第三个参数void *(*)(void *) :指向函数的指针
第四个参数void *restrict:函数的参数
*/
pthread_create(&thread, NULL,run ,NULL);

NSThread

相对 pthread 提供了面向对象的方法使用更加简单

// 获取当前线程
+ (NSThread *)currentThread;
// 创建启动线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
// 判断是否是多线程
+ (BOOL)isMultiThreaded;
// 线程休眠 NSDate 休眠到什么时候
+ (void)sleepUntilDate:(NSDate *)date;
// 线程休眠时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 结束/退出当前线程
+ (void)exit;
// 获取当前线程优先级
+ (double)threadPriority;
// 设置线程优先级 默认为0.5 取值范围为0.0 - 1.0
// 1.0优先级最高
// 设置优先级
+ (BOOL)setThreadPriority:(double)p;
// 获取指定线程的优先级
- (double)threadPriority NS_AVAILABLE(10_6, 4_0);
- (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0); // 设置线程的名字
- (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
- (NSString *)name NS_AVAILABLE(10_5, 2_0); // 判断指定的线程是否是 主线程
- (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
// 判断当前线程是否是主线程
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
// 获取主线程
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0); - (id)init NS_AVAILABLE(10_5, 2_0); // designated initializer
// 创建线程
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
// 指定线程是否在执行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
// 线程是否完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
// 线程是否被取消 (是否给当前线程发过取消信号)
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
// 发送线程取消信号的 最终线程是否结束 由 线程本身决定
- (void)cancel NS_AVAILABLE(10_5, 2_0);
// 启动线程
- (void)start NS_AVAILABLE(10_5, 2_0); // 线程主函数 在线程中执行的函数 都要在-main函数中调用,自定义线程中重写-main方法
- (void)main NS_AVAILABLE(10_5, 2_0); // thread body meth

GCD

GCD的优势

GCD是苹果公司为多核的并行运算提出的解决方案

GCD会自动利用更多的CPU内核(比如双核、四核)

GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

核心:任务和队列

任务

任务:执行什么操作,任务有两种执行方式: 同步函数 和 异步函数,他们之间的区别是

同步:只能在当前线程中执行任务,不具备开启新线程的能力,任务立刻马上执行,会阻塞当前线程并等待 Block中的任务执行完毕,然后当前线程才会继续往下运行

异步:可以在新的线程中执行任务,具备开启新线程的能力,但不一定会开新线程,当前线程会直接往下执行,不会阻塞当前线程

  • 异步操作:dispatch_async

  • 同步操作:dispatch_sync

//async: asynchronous 将任务异步的追加到队列中

dispatch_async(dispatch_get_global_queue(0,0), ^{

NSLog(@"async");

});

//sync: synchronous将任务同步的追加到队列中(等队列中的任务执行完,再将任务追加到队列)

//是同步追加,不是任务同步执行,在串行队列中,任务才同步执行

dispatch_sync(dispatch_get_global_queue(0,0), ^{

NSLog(@"sync");

});

队列

队列:用来存放任务,分为串行队列 和 并行队列

串行队列(Serial Dispatch Queue)

让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

并发队列(Concurrent Dispatch Queue)

可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)

并发功能只有在异步(dispatch_async)函数下才有效

队列代码

dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
  • Serial Dispatch Queue --- 等待现在正在执行的任务处理结束(串行)

  • Concurrent Dispatch Queue --- 不等待现在正在执行的任务处理结束(并行、并发)

并发队列

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);

串行队列

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);

优先级

/**
第一个参数:优先级 也可直接填后面的数字
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
第二个参数: 预留参数 0
*/
dispatch_queue_t quque1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

任务和队列的组合

  1. 任务:同步函数 异步函数

  2. 队列:串行 并行

  • 异步函数+并发队列:会开启新的线程,并发执行
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
  • 异步函数+串行队列:会开启一条线程,任务串行执行
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
  • 同步函数+并发队列:不会开线程,任务串行执行
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
  • 同步函数+串行队列:不会开线程,任务串行执行
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
  • 异步函数+主队列:不会开线程,任务串行执行
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});

*同步函数+主队列:死锁

//1.获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
//2.同步函数
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});

同步异步函数区别

  • 同步函数:立刻马上执行,会阻塞当前线程

  • 异步函数:当前线程会直接往下执行,不会阻塞当前线程

GCD 常用函数用法

1. dispatch_after

延迟执行

/*
第一个参数:延迟时间
第二个参数:要执行的代码
如果想让延迟的代码在子线程中执行,也可以更改在哪个队列中执行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"---%@",[NSThread currentThread]);
});

2. dispatch_once

一次性代码

-(void)once
{
//整个程序运行过程中只会执行一次
//onceToken用来记录该部分的代码是否被执行过
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"-----");
});
} //创建一个单例方法
+ (ShareOnce *)shanreInstence{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[ShareOncealloc] init]; NSLog(@"只执行1次"); }); returninstance; }

3. dispatch_group_async & dispatch_group_notify

组队列

// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并行队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 执行队列组任务
dispatch_group_async(group, queue, ^{
});
//队列组中的任务执行完毕之后,执行该函数
dispatch_group_notify(group, queue, ^{
});
例子
//并行队列执行任务,在并行队列中加入多个串行队列

//disptch_group

dispatch_group_t group =dispatch_group_create();

dispatch_queue_t queue =dispatch_get_global_queue(0,0);

dispatch_group_async(group, queue, ^{

NSLog(@"task 01");

});

dispatch_group_async(group, queue, ^{

NSLog(@"task 02");

});

dispatch_group_async(group, queue, ^{

sleep(6);//等待6秒执行任务3 目的:测试等待时间的dispatch_group_wait这个方法

NSLog(@"task 03");

});

dispatch_group_async(group, queue, ^{

sleep(2);

NSLog(@"task 04");

});

//监视函数

//监视队列中得任务结束,执行block中得任务

dispatch_group_notify(group, queue, ^{

NSLog(@"done");

});

//等待时间

dispatch_time_t time =dispatch_time(DISPATCH_TIME_NOW,5ull*NSEC_PER_SEC);

//dispatch_group_wait 指定时间后,看你一眼queue是否执行完毕

//如果执行完返回0

//没有执行完,返回非0值

long result = dispatch_group_wait(group, time);

if (result == 0) {

NSLog(@"finish");

}else{

NSLog(@"not finish");
}
}

4. dispatch_barrier_async

栅栏函数

栅栏函数可以控制任务执行的顺序,栅栏函数之前的执行完毕之后,执行栅栏函数,然后在执行栅栏函数之后的

5. dispatch_apply

快速迭代

/*
第一个参数:迭代的次数
第二个参数:在哪个队列中执行
第三个参数:block要执行的任务
*/
dispatch_apply(10, queue, ^(size_t index) {
});

6. dispatch_semaphore_create & dispatch_semaphore_signal & dispatch_semaphore_wait

信号量机制

//使用

dispatch_queue_t queue =dispatch_get_global_queue(0,0);

dispatch_semaphore_t dsema =dispatch_semaphore_create(1);

NSMutableArray *array = [NSMutableArrayarray];

for (int i =0; i < 1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(dsema,DISPATCH_TIME_FOREVER);
[arrayaddObject:[NSNumbernumberWithInt:i]];
dispatch_semaphore_signal(dsema); }); } NSLog(@"%@", array);

NSOperation

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能,所以使用起来更加方便易于理解。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。

NSOperation和NSOperationQueue实现多线程的具体步骤

1.将需要执行的操作封装到一个NSOperation对象中

2.将NSOperation对象添加到NSOperationQueue中

系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行

NSOperation

NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

使用NSOperation子类的方式有3种

1.NSInvocationOperation
NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//启动操作
[op start];
2.NSBlockOperation 常用的一种
//1.封装操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
//要执行的操作,在主线程中执行
NSLog(@"1------%@",[NSThread currentThread]);
}];
//2.追加操作,追加的操作在子线程中执行,可以追加多条操作
[op addExecutionBlock:^{
NSLog(@"---download2--%@",[NSThread currentThread]);
}];
[op start];
3.自定义类继承NSOperation,实现内部方法,复用性强
// 重写自定义类的main方法实现封装操作
-(void)main
{
// 要执行的操作
} // 实例化一个自定义对象,并执行操作
CustomOperation *op = [[CustomOperation alloc]init];
[op start];

NSOperationQueue队列的使用

两种队列

主队列:通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行

非主队列:直接alloc init出来的队列。非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行

作用

NSOperation可以调用start方法来执行任务,但默认是同步执行的

如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作

两者结合的方法使用 NSOperation和NSOperationQueue结合使用创建多线程

注:这里使用NSBlockOperation示例,其他两种方法一样
// 1. 创建非主队列 同时具备并发和串行的功能,默认是并发队列
NSOperationQueue *queue =[[NSOperationQueue alloc]init];
//NSBlockOperation 不论封装操作还是追加操作都是异步并发执行
// 2. 封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download1 -- %@",[NSThread currentThread]);
}];
// 3. 将封装操作加入主队列
// 也可以不获取封装操作对象 直接添加操作到队列中
//[queue addOperationWithBlock:^{
// 操作
//}];
[queue addOperation:op1];

NSOperation和NSOperationQueue的重要属性和方法

  • NSOperation的依赖

  • NSOperation监听操作

  • NSOperationQueue

  • maxConcurrentOperationCount 并发数

  • suspended 是否暂停

  • cancelAllOperations 取消所有任务,不再执行,不可逆

关键点:暂停和取消只能暂停或取消处于等待状态的任务,不能暂停或取消正在执行中的任务,必须等正在执行的任务执行完毕之后才会暂停,如果想要暂停或者取消正在执行的任务,可以在每个任务之间即每当执行完一段耗时操作之后,判断是否任务是否被取消或者暂停。如果想要精确的控制,则需要将判断代码放在任务之中,但是不建议这么做,频繁的判断会消耗太多时间

一个例子

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property(nonatomic,strong)UIImage *image1;
@property(nonatomic,strong)UIImage *image2;
@end
@implementation ViewController -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 创建非住队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 下载第一张图片
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
self.image1 = [UIImage imageWithData:data];
}];
// 下载第二张图片
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
self.image2 = [UIImage imageWithData:data];
}];
// 合成操作
NSBlockOperation *combie = [NSBlockOperation blockOperationWithBlock:^{
// 开启图形上下文
UIGraphicsBeginImageContext(CGSizeMake(375, 667));
// 绘制图片1
[self.image1 drawInRect:CGRectMake(0, 0, 375, 333)];
// 绘制图片2
[self.image2 drawInRect:CGRectMake(0, 334, 375, 333)];
// 获取合成图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图形上下文
UIGraphicsEndImageContext();
// 回到主线程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
// 添加依赖,合成图片需要等图片1,图片2都下载完毕之后合成
[combie addDependency:download1];
[combie addDependency:download2];
// 添加操作到队列
[queue addOperation:download1];
[queue addOperation:download2];
[queue addOperation:combie];
}
@end

多线程共享读写资源隐患问题

容易死锁

- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1========%@",[NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2========%@",[NSThread currentThread]);
});
NSLog(@"3========%@",[NSThread currentThread]);
} ThreadDemo[5615:874679] 1========{number = 1, name = main}

原因:任务A等待任务B完成才能继续执行,但作为串行队列的主队列又不能让任务B在任务A未完成之前开始执行,所以任务A等着任务B完成,任务B等着任务A完成

解决方案:

1.普通执行

2.同步队列

3.异步队列

多线程安全隐患的原因:1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。

那么当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。

可以使用互斥锁解决

几种加锁方法

  • synchronized

@synchronized(锁对象) {

// 需要锁定的代码

}

  • NSLock

iOS多线程知识梳理的更多相关文章

  1. iOS 多线程知识梳理

    #iOS多线程知识梳理 ##线程进程基础概念 ###进程 进程是指在系统中正在运行的一个应用程序每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内 ###线程 1个进程要想执行任务,必须 ...

  2. iOS开发知识梳理博文集

    前言 做iOS开发有3年了,从当初的小白到现在,断断续续看过很多资料,之前也写过一些博文来记录,但是感觉知识点都比较凌乱.所以最近准备抽时间把iOS开发的相关知识进行一个梳理,主要分为OC基础.UI控 ...

  3. iOS多线程知识总结--GCD

    iOS多线程知识总结--GCD 1. iOS中苹果提供4钟方案来帮助我们实现多线程: (1) 纯C语言的pthread,偏底层,需要程序员手动管理线程的生命周期,基本不用. (2) OC语言的NSTr ...

  4. IOS多线程知识总结/队列概念/GCD/主队列/并行队列/全局队列/主队列/串行队列/同步任务/异步任务区别(附代码)

    进程:正在进行中的程序被称为进程,负责程序运行的内存分配;每一个进程都有自己独立的虚拟内存空间 线程:线程是进程中一个独立的执行路径(控制单元);一个进程中至少包含一条线程,即主线程 队列 dispa ...

  5. IOS多线程知识总结/队列概念/GCD/串行/并行/同步/异步

    进程:正在进行中的程序被称为进程,负责程序运行的内存分配;每一个进程都有自己独立的虚拟内存空间: 线程:线程是进程中一个独立的执行路径(控制单元);一个进程中至少包含一条线程,即主线程. 队列:dis ...

  6. 复习IOS多线程知识

    线程的注意点 1.不要同时开太多的线程(1~3条线程即可,不要超过5条) 2.线程概念 * 主线程 : UI线程,显示.刷新UI界面,处理UI控件的事件 * 子线程 : 后台线程,异步线程 3.不要把 ...

  7. iOS多线程编程的知识梳理

    多线程编程也称之为并发编程,由于其作用大,有比较多的理论知识,因此在面试中也是受到面试官的青睐.在日常项目开发中,至少网络请求上是需要使用到多线程知识的,虽然使用第三方的框架比如AFNetworkin ...

  8. iOS中多线程知识总结(一)

    这一段开发中一直在处理iOS多线程的问题,但是感觉知识太散了,所以就把iOS中多线程的知识点总结了一下. 1.基本概念 1)什么是进程?进程的特性是什么? 进程是指在系统中正在运行的一个应用程序.   ...

  9. iOS多线程学习

    在 iOS 中其实目前有 4 套多线程方案,他们分别是: Pthreads NSThread GCD NSOperation & NSOperationQueue 所以接下来,我会一一讲解这些 ...

随机推荐

  1. elasticSerach 知识学习

    一 介绍: ElasticSearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口.Elasticsearch是用Java语言开发的, ...

  2. iOS使用Workspace来管理多项目 ( 转 )

    开发中会有一些常用的类或方法,或者是某个特定功能的,比如一个自定义的弹框.一个更容易使用的网络请求库,可以把它们放到一个单独的工程里,通过静态库(library.FrameWork)的方式应用到任何其 ...

  3. 第四章 JavaScript对象及初始面向对象

    创建对象: //方式一 var ower=new Object(); ower.name="长春花"; ower.genera="夹竹挑科 长春花属"; owe ...

  4. UVA-11987

    I hope you know the beautiful Union-Find structure. In this problem, you're to implement somethingsi ...

  5. 深入理解 Java 泛型

  6. Python3 模块基础

    目录 模块 什么是模块 模块的四种形式 导入模块 import 模块 from 模块 import 方法 循环导入 模块搜索路径 Python文件的两种用途 包 什么是包 为什么要有包 包的使用 绝对 ...

  7. Shell排序 C&&C++

    Shell排序   Shell排序是大量数据需要排序时,更为高效的插入排序.它的算法思想基于插入排序的算法思想 流程: (1)将n个元素数组分成n/2个数字序列,第一个数据和第n/2个数据为一对,等等 ...

  8. 常用eslint配置

    "off"或者0 //关闭规则关闭 "warn"或者1 //在打开的规则作为警告(不影响退出代码) "error"或者2 //把规则作为一个 ...

  9. 如何在Tomcat服务器配置CGI运行Python

    想通过请求触发部署在tomcat上的非java应用程序,需要用到Common Gateway Interface(CGI).Tomcat提供了Servlet CGI支持. 修改web.xml web. ...

  10. ubuntu16.04修改ssh的端口

    直接修改文件   /etc/ssh/sshd_config文件,