java学习2 Java常用类最新教程 通俗易懂

千锋教育-2020新版 Java常用类最新教程 通俗易懂

Java常用类最新教程 通俗易懂

  • 1.内部类
  • 2.包装类
  • 3.Object类
  • 4.String类
  • 5.BigDecimal类
  • 6.system类

1.内部类

成员内部类、静态内部类(static)、局部内部类(在方法内)、匿名内部类(常用在接口或抽象类)

概念:在一个类的内部再定义一个完整的类

特点:

  • 编译之后可生成独立的字节码文件
  • 内部类可直接访问外部类私有成员,而不破坏封装
  • 可为外部类提供必要的内部功能组件

Outer$Inner.class

Outer.class

1
2
3
4
5
6
// 身体
public class Body{ //生成1个class文件
// 头部
class Header{ // 会生成另一个class文件,文件名用$分隔外和内 类名
}
}

1.1.成员内部类

  • 在类的内部定义,与实例变量、实例方法同级别的类
  • 外部类的一个实例部分,创建内部类对象时,必须依赖外部类对象(先创建外部类对象才能创建内部类对象)
  • 当外部类、内部类存在重名属性时,会优先访问内部类属性
  • 成员内部类里不能定义静态成员、可以包含静态常量(final)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//Outer.java
public class Outer{// 外部类
//实例变量
private String name = "张三";
private int age = 20;

class Inner{//内部类
private String address = "北京";
private String phone = "110";
private String name = "李四";
//private static String country = "中国"; //编译报错,不能定义静态成员
private static final String country = "中国"; //编译通过,可以包含静态常量

public void show(){
//打印外部类属性 此时有重名属性name
//访问外部类属性用 Outer.this.name
sout(Outer.this.name); // 张三
sout(age); //建议外部类属性全部加Outer.this.
//打印内部类中的属性
sout(name); // 李四
sout(this.address); //建议给所有属性加this
sout(this.phone);
}
}
}

//Test.java
public class Test{
psvm(String[] args){
Outer outer = new Outer();// 创建外部类对象
Inner inner = outer.new Inner();// 创建内部类对象
//Inner inner = new Outer().new Inner();//也可把上面2行改成这样
inner.show();
}
}

1.2.静态内部类

  • 不依赖外部类对象,可直接创建或通过类名访问,可声明静态成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//Outer.java
public class Outer{// 外部类,不能用static修饰
//实例变量
private String name = "xxx";
private int age = 20;

static class Inner{// 静态内部类,和外部类相同级别
private String address = "上海";
private String phone = "111";
private static int count = 1000;// 静态成员

public void show(){
// 访问外部类的属性
//sout(name); //编译报错,不能直接访问,当作外部类同级的类去访问
Outer outer = new Outer();// 1. 先创建外部类对象
sout(outer.name);//xxx 2. 调用外部类对象的属性
sout(outer.age);//20
// 调用静态内部类的属性和方法
sout(address);//上海
sout(phone);//111
sout(Inner.count);//1000 调用静态内部类的静态属性
}
}
}

//Test.java
public class Test{
psvm(String[] args){
Outer.Inner inner = new Outer.Inner();// 直接创建静态内部类对象
inner.show();
}
}

1.3.局部内部类

  • 定义在外部类方法中,作用范围和创建对象范围仅限于当前方法
  • 局部内部类访问外部类当前方法中的局部变量时,因无法保障变量的生命周期与自身相同,变量必须修饰为final
  • 限制类的使用范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//Outer.java
public class Outer{// 外部类
//实例变量
private String name = "刘德华";
private int age = 35;

//方法
public void show(){
String address = "sz";// 定义局部变量
//private String address = "sz";//编译报错 不能加private

// 局部内部类:注意不能加任何访问修饰符
//private class Inner{//编译报错 不能加private之类
class Inner{ //只能在show()方法中使用
private String phone = "114";
private String email = "humble_zh@163.com";
//private static int count = 2000;//编译报错 不能定义static变量
private final static int count = 2000;//编译通过 能定义常量

public void show2(){
//访问外部类的属性
//无重名,相当于Outer.this.name,如果show2()是static的,就不能直接访问
sout(name); //刘德华
sout(Outer.this.age); //35
// 访问内部类的属性
sout(phone); //114
sout(this.email); //humble_zh@163.com

// 访问局部变量 jdk1.7要求必须常量final、jdk1.8自动添加final
sout(address); //sz //自动帮 String address = "sz"; 加了final
//address = "bj"; //编译报错 final不能修改
//为啥指定final?
//show().address属于栈内,方法返回后变量就回收了
//new Inner()的对象在堆,show()返回后,inner对象未回收,不能访问已被回收的address
}
}

//如果没有下面2行,则没有任何输出
Inner inner = new Inner();//创建局部内部类对象
inner.show2();
}
}

//Test.java
public class Test{
psvm(String[] args){
Outer outer = new Outer();// 创建外部类对象
outer.show();
}
}

1.4.匿名内部类

  • 没有类名的局部内部类(一切特征都与局部内部类相同)
  • 必须继承一个父类或者实现一个接口
  • 定义类、实现类、创建对象的语法合并,只能创建一个该类的对象
  • 优点:减少代码量
  • 缺点可读性较差
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//Usb.java
public interface Usb{ void service(); }

//Mouse.java
public class Mouse implements Usb{
@Override
public void service(){ sout("成功连接电脑,Mouse开始工作"); }
}

//TestUsb.java
public class TestUsb{
psvm(String[] args){
/*
Usb usb = new Mouse();//创建接口类型变量
usb.service();//成功连接电脑,Mouse开始工作
*/

/* //局部内部类
class Fan implements Usb{
@Override
public void service(){ sout("成功连接电脑,Fan开始工作") }
}

//使用局部内部类创建对象
Usb usb = new Fan();
usb.service(); //成功连接电脑,fan开始工作
*/

//上面使用局部内部类,用完之后就不再用了,没必要定义一个带名字的类
//使用匿名内部类优化(相当于创建了一个局部内部类)
//Usb usb = new Usb(); //编译报错 interface 不能实例化
Usb usb = new Usb(){ //Usb() 可以是 interface, 抽象类, 父类
@Override
public void service(){ sout("成功连接电脑,xxx开始工作") }
};
usb.service(); //成功连接电脑,xxx开始工作
}
}

2.Object 类

  • 超类、基类,所有类的直接或间接父类,位于继承树的最顶层
  • 任何类,如没有书写extends显示继承某个类,都默认直接继承Object类,否则为间接继承
  • Object类中所定义的方法,是所有对象都具备的方法
  • Object类型可以存储任何对象
    • 作为参数,可接受任何对象
    • 作为返回值,可返回任何对象

clone(),equals(),finalize(),getClass(),hashCode(),notify(),notifyAll(),toString(),wait(),wait(timeout)

getClass() 方法

  • public final Class<?> getClass(){}
  • 返回引用中存储的实际对象类型
  • 应用:通常用于判断两个引用中实际存储对象类型是否一致
1
2
3
4
5
6
7
8
Student s1=new Student("aaa",20);
Student s2=new Student("bbb",22);
// getClass返回 class类型
Class class1 = s1.getClass();
Class class2 = s2.getClass();
// 判断s1 和 s2是不是同一个类型
if(class1 == class2){ sout("true"); }
else{ sout("false"); }

hashCode()方法

  • public int hashCode(){}
  • 返回该对象的哈希码值
  • 哈希值根据对象的地址字符串数字使用hash算法计算出来的int类型的值
  • 一般情况下相同对象返回相同哈希码
1
2
3
4
5
6
Student s1=new Student("aaa",20);
Student s2=new Student("bbb",22);
sout(s1.hashCode()); //s1的hash和s2不相等
sout(s2.hashCode()); //因为是两个地址
Student s3 = s1; //s3和s1变量在栈内,但对象实例在堆
sout(s3.hashCode());//此时s3的hashCode与s1相同

toString()方法

  • public String toSring(){}
  • 返回该对象的字符串表示(表现形式)
  • 可以根据程序需求覆盖该方法,如:展示对象各个属性值
1
2
3
4
5
6
7
8
9
10
11
12
sout(s1.toString()); // 直接打印包+类名+哈希值(hex)

//在Student.java内重写toString()
class Student{
...
// 重写 alt + enter + s
@override
public String toString(){ return "Student [name = " + name + ", age = " + age + "]"; }
...
}

sout(s1.toString()); // 打印override后的格式

equals()方法

  • public boolean equals(Object obj){}
  • 默认实现为(this == obj), 比较两个对象地址是否相同
  • 可进行覆盖,比较两个对象的内容是否相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 判断两个对象是否相等
sout(s1.equals(s2)); // false

Student s4 = new Student("小明", 17);
Student s5 = new Student("小明", 17);
sout(s4.equals(s5)); // false 堆中地址不同

// 不想比较地址,那就在Student.java重写该方法为:比较内容
/* 步骤
1. 比较两个应用是否指向同一个对象
2. 判断obj是否为null
3. 判断两个引用只想的实际对象类型是否一致
4. 强制类型转换
5. 依次比较各个属性值是否相同
*/
@override
public boolean equals(Object obj){
// 1.
if(this == obj){ return true; }
// 2.
if(obj == null){ return false; }
// 3.判断是否同一个类型
//if(this.getClass() == obj.getClass()){ }
//不想用getClass()可以用instanceof 判断对象是否是某种类型
if(obj instanceof Student){
// 4.强制类型转换
Student s = (Student)obj;
// 5. 比较属性
if(this.name.equals(s.getName()) && this.age == s.getAge()){ return true; }
}
return false;
}

//重写equals()之后下面调用就返回true
sout(s4.equals(s5));

//TODO
if(obj instanceof Student)if(this.getClass() == obj.getClass())这两个方法效果相同,一般用左边的。

但我觉得这两个方法判断的效果是不是不相同啊。
举个例子,Object——Person——Student这样的继承关系
Object obj = new Student();
Person person = new Person();
System.out.println(person.equals(obj));
如果用if(obj instanceof Person),那应该是True,因为obj是Student的实例,与Person有继承关系。

如果用if(this.getClass() == obj.getClass()),那应该是False,因为obj引用的是Student类,而person是Person类

感觉这里是不是还是用if(this.getClass() == obj.getClass())判断更好一点啊
还有后面有个小笔误,整数缓冲区那里integer3,4,后面是100,不是new Integer(100)
sout(content.length()); // 10
sout(content.charAt(content.length() - 1)); // 言
唔这里应该是20和1
是100;自动装箱相当于Integer.valueOf(100);如果是new,不会进缓冲区,会打印false;
if(obj instanceof Person)这里的需求是需要判断两个类之间是否有直接或间接继承关系,不是判断是否一致;只有有继承关系,才能进行强制类型转换

finalize() 方法

  • 当对象被判定为垃圾对象时,由JVM自动调用此方法,用以标记垃圾对象,进入回收队列
  • 垃圾对象:没有有效引用指向此对象时,为垃圾对象
  • 垃圾回收:由gc销毁垃圾对象,释放数据存储空间
  • 自动回收机制:JVM的内存耗尽,一次性回收所有垃圾对象
  • 手动回收机制:使用System.gc();通知JVM执行垃圾回收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Student.java
@Override
protected void finalize() throws Throwable{ sout(this.name + "对象被回收了"); }

//Test.java
psvm(String[] args){
Student s1 = new Student("aaa", 20); // 不是垃圾
new Student("bbb", 20); // 是垃圾 会被回收
System.gc();//手动调用,回收垃圾
sout("回收垃圾");
// 打印出
//回收垃圾
//bbb对象被回收了”
}

3.包装类

  • 基本数据类型所对应的引用数据类型
  • Object 可统一所有数据,包装类的默认值是null

基本数据类型在栈,自己没有方法调用(因为不是类)。包装成引用数据类型之后就是类,实例存在于堆。

基本数据类型 包装类型
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

类型转换与装箱、拆箱

装箱:基本 转 引用 (栈->堆);拆箱相反。

  • 8种包装类提供不用类型间的转换方式
    1. Number父类中提供的6个共性方法
    2. parseXXX( )静态方法
    3. valueOf( )静态方法
  • 注意:需保证类型兼容,否则抛出NumberFormatException异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
psvm(String[] args){
// 装箱, 基本类型 -> 引用类型
int num1 = 18;// 基本类型
// 使用Integer类创建对象
Integer integer1 = new Integer(num1); //构造方法
Integer integer2 = Integer.valueOf(num1);

// 拆箱, 引用类型 -> 基本类型
Integer integer3 = new Integer(100); //创建引用类型
int num2 = integer3.intValue(); //转成基本类型

// 上述为jdk1.5之前方法,之后提供了自动装箱拆箱
int age = 30;
Integer integer4 = age; // 自动装箱 编译器自动补全为 integer4 = age.valueOf();
int age2 = integer4; // 自动拆箱 编译器自动补全为 age2 = integer4.intValue();

// 基本类型和字符串之间转换
// 1. 基本类型转成字符串
int n1 = 100;
// 1.1 使用+号
String s1 = n1 + "";
// 1.2 使用Integer中的toString()方法
String s2 = Integer.toString(n1);
String s2 = Integer.toString(n1, 16);//Integer.toString(n1, x); // x为进制要求

// 2. 字符串转成基本类型
String str = "150";
int n2 = Integer.parseInt(str); // 使用Integer.parseXXX();
//int n2 = Integer.parseInt("150o"); // 编译报错 只能是数字字符不能是字母或其他

// boolean 字符串形式转成基本类型,只有"true" -> true,非“true" -> false
String str2 = "true";
boolean b1 = Boolean.parseBoolean(str2);
}

整数缓冲区

  • Java预先创建了256个常用的证书包装类型对象
  • 在实际应用当中,对已创建的对象进行复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
psvm(String[] args){
// 面试题
Integer integer1 = new Integer(100);
Integer integer2 = new Integer(100);
sout(integer1 == integer2); // false 栈里面两个变量存的地址不相等

Integer integer3 = 100; // 自动装箱 编译器自动补全为 =Integer.valueOf(100); //new Integer(100);
Integer integer4 = 100; //编译器自动补全为 =Integer.valueOf(100);
sout(integer3 == integer4); // true

Integer integer5 = 200; // 自动装箱 编译器自动补全为 =Integer.valueOf(200); //new Integer(200);
Integer integer6 = 200; //编译器自动补全为 =Integer.valueOf(200);//new Integer(200);
sout(integer5 == integer6); // false

// 100在缓存区数组 [-128, 127] 内,故valueOf()返回的地址相同(共/复用同一个堆内的对象)
// 200不在缓存区数组 [-128, 127] 内,故valueOf()返回 new Integer();的地址
}

//源码
public static Integer valueOf(int i){
//堆:IntegerCache常量数组,范围[-128,127],若i在范围内,直接返回数组对应元素的地址
if(i >= IntegerCache.low && i <= IntegerCache.high) //low:-128 high:127
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

4.String 类

  • 字符串是常量,创建之后不可改变
  • 字符串字面值存储在字符串池中,可以共享(内存分:栈、堆、方法区。字符串存在方法区的字符串池里)
  • String s = "Hello";产生一个对象,字符串池中存储
  • String s = new String("Hello");产生两个对象,堆、池各一个
1
2
3
4
5
6
7
8
9
10
11
String name = "Hello"; //name在栈,"Hello"在方法区的字符串池内
name = "zhangsan"; //name在栈,更换指向为方法区的字符串池内的"zhangsan",此时"Hello"成为了垃圾
String name2 = "zhangsan"; //name2在栈和name指向同为方法区的字符串池内的"zhangsan"

String s = "Hello"; //产生一个对象,字符串池中存储
String s = new String("Hello"); //产生两个对象,堆和池 各一个;但堆内的对象指向的是方法区里的对象

String str1 = new String("java"); //产生两个对象,堆和池 各一个 浪费空间
String str2 = new String("java"); //产生两个对象,堆和池 各一个 浪费空间
sout(str1==str2); //false 因为堆内是两个实例,栈内的str1和str2保存的地址不一样
sout(str1.equals(str2)); //true

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//String content = "java是世界上最好的编程语言";
String content = "java是世界上最好的java编程语言,java真香";
sout(content.length()); // 15 // 1.int length(); 返回字符串长度
sout(content.charAt(0)); // j // 2.char charAt(int index); 返回某个位置的字符
sout(content.charAt(content.length() - 1)); // 言
//sout(content.charAt(content.length())); //编译报错 访问越界
sout(content.contains("java")); // true // 3.boolean contains(String str); 判断是否包含某个字符串

sout(Arrays.toString(content.toCharArray())); //[j,a...] //4.char[] toCharArray(); 返回字符串对应数组
sout(content.indexOf("java")); //0 //5.int indexOf(String); 返回子字符串首次出现的下标,失败-1
sout(content.indexOf("java", 4)); //从索引4开始找 返回11
sout(content.lastIndexOf("java")); //12//6.int lastIndexOf(); 返回字符串最后一次出现的下标,失败-1

String ct = " hello World ";
sout(ct.trim()); // "hello World" // 7. trim(); //去掉字符串前后空格
sout(ct.toUpperCase()); // HELLO WORLD // 8. toUpperCase(); toLowerCase(); 转换大小写
sout(ct.toLowerCase()); // hello world
sout(ct.endWith("World")); // true // 9. endWith(str); startWith(str); 判断是否以str 结尾、开头
sout(ct.startWith("hello")) // true

// 10. replace(char old, char new); 用new的字符或字符串替换old的字符或字符串
sout(content.replace("java", "php")); // php是最好的语言...

String say = "java is the best programing language,java xiang";
String[] arr = arr.say.split("[ ,]+ "); // "[ ,]+" 表示空格 逗号切分 +号表示切分可以多个 比如多个空格 // 11. split(); 对字符串拆分
sout(arr.length); // 5
for(String string : arr){ sout(string); }
// 打印出
//java
//is
//the
//best
//programing
//language
//java
//xiang

// 补充两个equals返回bool,compareTo()比较差值
String s1 = "hello";
String s2 = "HELLO";
sout(s1.equals(s2));// false
sout(s1.equalsIgnoreCase(s2));//true 忽略大小写比较true

// compareTo(); 两字符不同时比较字符字典序的ascii码
String s3="abc"; //a:97
String s4="xyz"; //x:120
sout(s3.compareTo(s4));//-23

String s5="abc";
String s6="abcxyz";
sout(s5.compareTo(s6));//-3// 字符相同时返回长度差

案例演示

需求:

  1. 已知String str = “this is a text”;
  2. 将str中的单词单独获取
  3. 将str中的text替换成practice
  4. 在text前面插入一个easy
  5. 将每个单词的首字母改为大写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
psvm(String[] args){
String str = "this is a text";

String[] arr = str.split(" "); //1
for(String s : arr){ sout(s); }

String str2 = str.replace("text", "practice"); //2
String str3 = str.replace("text", "easy text"); //3

for(int i = 0; i < arr.length; i++){ //4
char first = arr[i].charAt(0);
char upperfirst = Character.toUpperCase(first);
String newstr = upperfirst + arr[i].substring(1);
}
}

可变字符串

  • StringBuffer : 可变长字符串,运行效率比String快,比StringBuilder慢、线程安全
  • StringBuilder : 可边长字符串、运行快、线程不安全

效率都比String高且节省内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
psvm(String[] args){
//StringBuffer sb = new StringBuffer();// StringBuffer 和 StringBuilder 用法一致
StringBuilder sb = new StringBuilder();// StringBuffer 和 StringBuilder 用法一致
sb.append("java no1");// 1. append(); 追加
sb.insert(0, "我在最前面");//我在最前面java no1 //2. insert(); 添加、插入
sb.replace(0, 5, "hello"); //hellojava no1 //左闭右开// 3.replace(); 替换
sb.delete(0, 5); //java no1 //左闭右开// 4. delete(); 删除
sb.delete(0, sb.length());// 5. 清空
}

//验证StringBuilder效率比String高
psvm(String[] args){
long start = System.currentTimeMillis();
/*
String string = "";
for(int i=0; i < 99999; i++){ string+=i; }
sout(string); //总共用时47475ms
*/

StringBuilder sb = new StringBuilder();
for(int i=0; i < 99999; i++){ sb.append(i); }
sout(sb.toString()); //总共用时308ms

long end = System.currentTimeMillis();
sout(end-start);
}

5.BigDecimal 类

  • 位置 java.math 包中
  • 作用 精确计算浮点数
  • 创建方式 BigDecimal bd = new BigDecimal("1.0");
1
2
3
4
5
6
7
//double在内存里存的是近似值,在要求精度比较高的场景(银行余额)就无法使用
double d1=1.0;
double d2=0.9;
sout(d1-d2);//0.0999999...

double result=(1.4-0.5)/0.9;
sout(result);//0.999999...
1
2
3
4
5
6
7
8
9
10
11
12
13
BigDecimal bd1 = new BigDecimal("1.0"); //传参需用字符串,因为数字本身是double就有可能不精确
BigDecimal bd2 = new BigDecimal("0.9");

BigDecimal r1 = bd1.subtract(bd2);// 减法
sout(r1); // 0.1

BigDecimal r2 = bd1.add(bd2);//1.9 加法

BigDecimal r3 = bd1.multiply(bd2);//0.90 乘法


BigDecimal r4 = new BigDecimal("1.4").subtract(new BigDecimal("0.5")).divide(new BigDecimal("0.9"));//1 除法
BigDecimal r5 = new BigDecimal("20").divide(new BigDecimal("3"), 2, BigDecimal.ROUND_HALF_UP);//6.67 除不尽时 2:保留位数 ROUND_HALF_UP:为四舍五入

Date 类

Date表示特定的瞬间,精确到毫秒。Date类中的大部分方法都已经被Calendar类中的方法所取代

时间单位:1s = 1,000ms = 1,000,000 μs = 1,000,000,000 = ns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
psvm(String[] args){
Date date1 = new Date();// 1 创建Date对象 用java.util,java.sql是继承java.util的
sout(date1.toString()); //SAT Jan 23 22:25:23 CST 2021
sout(date1.toLocaleString()); // 已过时 但也能用 2021-1-23 22:25:24

// 创建昨天的
Date date2 = new Date(date1.getTime() - (60*60*24*1000));
sout(date2.toLocaleString()); //2021-1-22 22:25:24

// 2 方法after before
boolean b1 = date1.after(date2);
sout(b1); //true
boolean b2 = date1.before(date2);
sout(b2); //false

int d = date1.compareTo(date2);// 比较compareTo();
sout(d); // 大为1,小为-1,等为0

boolean b3 = date1.equals(date2);// 比较是否相等 equals()
sout(b3); // false
}

Calendar

  • Calendar提供了获取或设置各种日历字段的方法
  • 构造方法 protected Calendar(); 由于是protected 所以无法直接创建
  • 其他方法
方法名 说明
static Calendar getInstance() 使用默认时区和区域获取日历
void set(int year, int month, int date, int hourofday, int minute, int second) 设置日历的年、月、日、时、分、秒
int get(int field) 返回给定日历字段的值。字段比如年、月、日
void setTime(Date date) 用给定的date设置此日历时间
Date getTime() 返回一个date表示此日历的时间
void add(int field, int amount) 按照日历的规则,给指定字段添加或减少时间量
long getTimeInMilles() 毫秒为单位返回该日历的时间值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
psvm(String[] args){
Calendar calendar = Calendar.getInstance();// 1. 创建 Calendar 对象
sout(calendar.getTime().toLocaleString());//2021-01-23 16:16:05
sout(calendar.getTimeInMillis());
// 2. 获取时间信息
int year = calendar.get(Calendar.YEAR);// 获取年
int month = calendar.get(Calendar.MONTH);// 获取月 从 0 - 11
int month = calendar.get(Calendar.DAY_OF_MONTH);// 日
int hour = calendar.get(Calendar.HOUR_OF_DAY);// HOUR_OF_DAY(24时制),HOUR(12时制)
int minute = calendar.get(Calendar.MINUTE);// 分钟
int second = calendar.get(Calendar.SECOND);// 秒
// 3. 修改时间
Calendar calendar2 = Calendar.getInstance();
calendar2.set(Calendar.DAY_OF_MONTH, 22); //时间改为今月的22号
// 4. add修改时间
calendar2.add(Calendar.HOUR, 1); // 1为加1小时 -1为减1小时
// 5. 补充方法
int max = calendar2.getActualMaximum(Calendar.DAY_OF_MONTH);//31,月末一天
int min = calendar2.getActualMinimum(Calendar.DAY_OF_MONTH);//1,月初一天
}

SimpleDateFormat

  • SimpleDateFormat是一个以与语言环境有关的方式来格式化和解析日期的具体类
  • 进行格式化(日期->文本)、解析(文本->日期)
  • 常用的时间模式字母
字母 日期或时间 示例
y 2019
M 年中月份 08
d 月中天数 10
H 一天中小时(0-23) 22
m 分钟 16
s 59
S 毫秒 356
1
2
3
4
5
6
7
8
psvm(String[] args) throws Exception{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 1. 创建对象
Date date = new Date();// 2. 创建Date
String str = sdf.format(date);//格式化date(日期->字符串)
sout(str);//2021-01-23 16:28:49
Date date2 = sdf.parse("2021-01-22 16:28:49");//解析(字符串->时间)若字符串不匹配创建对象时设定的格式就会抛出异常
sout(date2);//Fri Jan 22 16:28:49 CST 2021
}

6.System类

主要用于获取系统的属性数据和其他操作,构造方法私有的

方法名 说明
static void arraycopy(…) 复制数组
static long currentTimeMillis(); 获取当前系统时间,返回毫秒值
static void gc(); 建议jvm赶快启动垃圾回收期器回收垃圾
static void exit(int status); 退出jvm 如果参数是0表示正常退出jvm 非0表示异常退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
psvm(String[] args){
//arraycopy 复制
//src-原数组 srcPos-从哪个位置开始复制0 dest-目标数组 destPos-目标数组的位置 length-复制的长度
int[] arr = {20, 18, 15, 8, 35, 26, 45, 90};
int[] dest = new int[8];
//System.arraycopy(arr, 0, dest, 0, 8);
System.arraycopy(arr, 4, dest, 4, 4)
for(int i=0; i<dest.length; i++){ sout(dest[i]); }//0, 0, 0, 0, 35, 26, 45, 90

//Arrays.copyOf(original, newLength)底层就是调用System.arraycopy(),System.arraycopy()再底层就是用C/C++实现的

sout(System.currentTimeMillis());//可用于计算代码用时

System.gc(); //前面 finalize() 方法已经演示

System.exit(0);
}

java学习1 Java零基础学习视频通俗易懂

【狂神说Java】Java零基础学习视频通俗易懂

HelloWorld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# li @ evpower in ~/humble/blog/source/_posts/java on git:master x [18:27:56]
$ cat Hello.java
public class Hello{
public static void main(String[] args){ System.out.print("Hello World!"); }
}

# li @ evpower in ~/humble/blog/source/_posts/java on git:master x [18:28:02]
$ javac Hello.java

# li @ evpower in ~/humble/blog/source/_posts/java on git:master x [18:28:12]
$ ls
Hello.class Hello.java

# li @ evpower in ~/humble/blog/source/_posts/java on git:master x [18:28:13]
$ java Hello
Hello World!%
# li @ evpower in ~/humble/blog/source/_posts/java on git:master x [18:28:18]
$

Java数据类型分类

基本数据类型primitive type

1
2
3
4
5
6
7
8
byte num1 = 20;
short num2 = 20;
int num4 = 20;
long num5 = 20L; //可以用L或l,但l容易跟1弄混
float num6 = 50.1F;
double num7 = 3.141592653;
char num3 = 'a';
boolean flag = true; //false

引用数据类型reference type(类,接口,数组)

1
String name = "Lion";

进制

1
2
3
0b10010010 //二进制(0b)
050 //八进制(0)
0x1A //十六进制(0x)

避免使用浮点数比较,建议用类(BigDecimal)

1
2
3
4
5
6
7
float f = 0.1F;
double d = 1.0/10;
if(f == d) //false

float d1 = 123123123123123123F;
float d2 = d1 + 1;
if(d1 == d2) //true

类型转换

1
2
3
4
5
6
7
8
//byte,short,char -> int -> long -> float -> double //低->高 (高->低:强制类型转换 低->高:自动类型转换)
int i = 128;
byte b = (byte)i; //-128 强制转换 内存溢出
double d = i; //128 自动转换
int j = (int)23.7; //23
int k = (int)-45.89f; //-45
int money = 10_0000_0000; //1000000000, JDK7新特性,可以用下划线分割数字
long total = money * 12; //int型溢出,改为 long total = (long)money * 12L;

注意:

  • 不能对boolean进行转换
  • 不能把对象类型转成不相干的类型(把猪转人-no,把男转女-yes)
  • 把高容量转低容量时,强制转换
  • 转换时可能存在内存溢出或精度问题

变量

  • 类变量:(static)初始默认值是null
  • 实例变量:从属于对象,若不初始化,默认值是 0/0.0
  • 局部变量:必须声明和初始化
1
2
3
4
5
6
7
8
public class Variable{
static int allClicks = 0; //类变量
String str="hello world"; //实例变量

public void method(){
int i=0; //局部变量
}
}

常量

1
2
3
4
5
final double PI=3.14; //常量名建议使用全大写

/* 修饰符关键字不分先后顺序 */
static final double PI=3.14;
final static double PI=3.14;

变量的命名规范

  • 类成员变量:首字母小写驼峰命名法
  • 局部变量:首字母小写驼峰命名法
  • 常量:大写字母和下划线
  • 类名:首字母大写驼峰命名法
  • 方法名:首字母小写驼峰命名法

运算符

  • 算数运算符+,-,*,/,%,++,--
  • 赋值运算符=
  • 关系运算符>,<,>=,<=,==,!=,instanceof
  • 逻辑运算符&&,||,!
  • 位运算符&,|,^,~,>>,<<,>>>
  • 条件运算符? :
  • 扩展赋值运算符+=,-=,*=,/=

IDEAtips:Ctrl+d是复制当前行到下一行

短路运算

1
2
int c=5;
boolean d = (c<4)&&(c++<4); //d是false,c是5

字符串连接符号+

1
2
3
4
int a=10;
int b=20;
""+a+b //"1020"
a+b+"" //"30"

包机制

是为了更好地组织类,并且区别类名的命名空间。常用公司域名倒置作为包名:com.baidu.www。package用来封装包,import用来导入包(import需要放置在package后面)

1
2
package pkg1[. pkg2[. pkg3... ]];
import package1[.package2...].(classname|*); //*是通配符,表示导入包下所有的类

IDEAtips:需要用外部包的变量或方法时报错忘记import,按Alt+Enter可查询包方便导入

JavaDoc

javadoc命令是用来生成自己API文档的

1
2
3
4
5
6
7
参数信息
@author 作者名
@version 版本号
@since 指明需要最早适用的jdk版本
@param 参数名
@return 返回值情况
@throws 异常抛出情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.humble.base;

/**
* @author humble
* @version 1.0
* @since 1.8
*/
public class Hello {
String name;

/**
* @param name
* @return
* @throws Exception
*/
public String test(String name) throws Exception{ return name; }
}

在idea里右键Hello的类,Show in Files,在当前目录打开命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [23:19:05]
$ ls
Hello.java

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [23:19:07]
$ javadoc -encoding UTF-8 -charset UTF-8 Hello.java
正在加载源文件Hello.java...
正在构造 Javadoc 信息...
标准 Doclet 版本 1.8.0_265
正在构建所有程序包和类的树...
正在生成./com/humble/base/Hello.html...
Hello.java:17: 警告 - @return 标记没有参数。
正在生成./com/humble/base/package-frame.html...
正在生成./com/humble/base/package-summary.html...
正在生成./com/humble/base/package-tree.html...
正在生成./constant-values.html...
正在构建所有程序包和类的索引...
正在生成./overview-tree.html...
正在生成./index-all.html...
正在生成./deprecated-list.html...
正在构建所有类的索引...
正在生成./allclasses-frame.html...
正在生成./allclasses-noframe.html...
正在生成./index.html...
正在生成./help-doc.html...
1 个警告

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [23:19:47]
$ ls
allclasses-frame.html Hello.java package-list
allclasses-noframe.html help-doc.html script.js
com index-all.html stylesheet.css
constant-values.html index.html
deprecated-list.html overview-tree.html

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [23:21:36]
$

用浏览器打开index.html即可

Scanner对象

用于获取用户输入

Demo01:使用next()

  • 1.一定要读取到有效字符后才可以结束输入
  • 2.对输入有效字符前遇到的空白,next()方法会自动将其去掉
  • 3.只有输入有效字符后才将其后面输入的空白作为分隔符或结束符
  • 4.next()不能得到带有空格的字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.humble.scanner;
import java.util.Scanner;

public class Demo01 {
public static void main(String[] args) {
//创建扫描器对象,用于接收键盘数据
Scanner scanner = new Scanner(System.in);

System.out.println("使用next方式接收:");

if(scanner.hasNext()){ //判断用户有没有输入字符串
//使用next方式接收
String str = scanner.next(); //程序会等待用户输入完毕
System.out.println("输入内容为:" + str);
}

//凡是属于IO流的类,如果不关闭会一直占用资源,要养成良好习惯,用完就关
scanner.close();
}
}

运行Demo01(只输出了空格前的单词,再调用next()可获得下一个单词)

1
2
3
4
5
使用next方式接收:
hello world!
输入内容为:hello

Process finished with exit code 0

Demo02:使用nextLine()

  • 1.以Enter为结束符,即返回的是回车之前的全部字符
  • 2.可以获得空白字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.humble.scanner;
import java.util.Scanner;

public class Demo02 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("使用nextline方式接收:");
if(scanner.hasNextLine()){
//使用nextline方式接收
String str = scanner.nextLine(); //程序会等待用户输入完毕
System.out.println("输入内容为:" + str);
}
scanner.close();
}
}

运行Demo02(输出整行)

1
2
3
4
5
使用nextline方式接收:
hello world!
输入内容为:hello world!

Process finished with exit code 0

反编译

Project Structure-Project Settings-Project-Project comcompiler output
打开对应目录,子目录里面会有.class文件,把class文件拷贝到项目目录,然后导入到idea即可看到源码

IDEAtips:输入100.for即可补全for循环100次

方法的重载

重载就是在一个类中,有相同的方法名称,但形参不同的方法。

重载规则:

  • 1.方法名称必须相同
  • 2.参数列表必须不同(个数不同、类型不同、参数排列不同等)
  • 3.方法的返回类型可以相同也可不同
  • 4.仅仅返回类型不同不足以成为方法的重载

实现理论:方法名称相同时,编译器会根据调用方法的参数个数、参数类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。

命令行传参

1
2
3
4
5
6
7
8
9
package com.humble.base;

public class Hello {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("arg[" + "] is " + args[i]);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [16:19:41]
$ ls
Hello.java

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [16:19:41]
$ javac Hello.java

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [16:19:47]
$ ls
Hello.class Hello.java

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src/com/humble/base [16:20:07]
$ cd ../../../

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src [16:20:12]
$ ls
com

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src [16:20:13]
$ java com.humble.base.Hello

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src [16:20:28]
$ java com.humble.base.Hello this is argv
arg[] is this
arg[] is is
arg[] is argv

# li @ evpower in ~/IdeaProjects/StudyJAVA/Hello/src [16:20:38]
$

可变参数

  • JDK1.5开始,java支持:给一个方法传递同类型的可变参数
  • 在方法声明中,在指定参数类型后面加一个省略号(…)
  • 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。

递归

递归结构包括两个部分:

  • 递归头:什么时候不调用自身方法。如果没有头,将陷入死循环。
  • 递归体:什么时候需要调用自身方法。

数组

1
2
3
dataType[] arrayRefVar; //首选
dataType arrayRefVar[]; //只是为了方便C/C++程序员
dataType[] arrayRefVar = new dataType[arraySize];

三种初始化

1
2
3
4
5
6
7
8
9
10
//静态初始化
int[] a = {1,2,3};
Man[] mans = {new Man(1,1), new Man(2,2)};

//动态初始化
int[] a = new int[2];
a[0] = 1;
a[1] = 2;

//数组默认初始化:数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中每个元素也被按照实例变量同样的方式被隐式初始化。

Arrays类

数组本身没有什么方法提供调用,但API中提供了工具类java.util.Arrays对数据对象进行一些基本操作。Arrays类中的方法都是Static修饰的静态方法,使用时可以直接用类名调用,不需要使用对象来调用(想用对象也可以)。

  • 给数组赋值用fill方法
  • 对数组排序用sort方法,按升序
  • 比较数组用equals,比较元素值是否相等
  • 查找数组元素用binarySearch,对排好序的数组进行二分发查找

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int[] sort(int[] a){
int tmp = 0;
boolean flag = false; //假设已经排好序,即无需位置交换

for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - 1 - i; j++) {
if(a[j+1] < a[j]){
tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; //表示有位置交换
}
}
/* 确实没有发生位置交换,表示已经排好序 */
if(!flag){ break; }
}
return a;
}

稀疏数组

当数组中大部分元素为0,或者为同一值时,可以使用稀疏数组来保存。处理方式是:

  • 记录数组一共几行几列几个非0值
  • 把具有不同值的元素和行列及值记录在一个小规模数组中,从而压缩数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Hello {
public static void main(String[] args) {
int[][] array1 = new int[11][11];
array1[1][2] = 1;
array1[2][3] = 2;
for (int[] ints : array1) { //打印原数组
for (int anInt : ints) { System.out.print(anInt); }
System.out.println();
}

System.out.println("==开始转为稀疏数组==");
int sum = 0;
for (int[] ints : array1) { //统计非0总数
for (int anInt : ints) {
if(anInt != 0) { sum++; }
}
}
//总长、总宽、非0总数
int[][] array2 = new int[sum+1][3];
array2[0][0] = array1[0].length;
array2[0][1] = array1.length;
array2[0][2] = sum;

int count = 0;
for (int i = 0; i < array1.length; i++) {
for (int j = 0; j < array1[i].length; j++) {
if(array1[i][j] != 0){
count++;
array2[count][0] = i;
array2[count][1] = j;
array2[count][2] = array1[i][j];
}
}
}
System.out.println("cow\trow\tvalue");
for (int[] ints : array2) { System.out.println(Arrays.toString(ints)); }

System.out.println("==开始还原数组==");
int[][] array3 = new int[array2[0][0]][array2[0][1]];
for (int i = 1; i < array2.length; i++) {
array3[array2[i][0]][array2[i][1]] = array2[i][2];
}
for (int[] ints : array1) {
for (int anInt : ints) { System.out.print(anInt); }
System.out.println();
}
}
}

运行输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
00000000000
00100000000
00020000000
00000000000
00000000000
00000000000
00000000000
00000000000
00000000000
00000000000
00000000000
==开始转为稀疏数组==
cow row value
[11, 11, 2]
[1, 2, 1]
[2, 3, 2]
==开始还原数组==
00000000000
00100000000
00020000000
00000000000
00000000000
00000000000
00000000000
00000000000
00000000000
00000000000
00000000000

面向对象

  • 对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。但是具体到微观操作,仍然需要面向过程的思路取处理。
  • 面向对象编程(Object-Oriented Programming),本质是:以类的方式组织代码,以对象的方式组织/封装数据。
  • 三大特性:封装、继承、多态。
  • 从认识的角度考虑是先有对象后有类。对象是具体的事物;类是针对对象的抽象。
  • 从代码运行角度考虑是先有类后有对象。类是对象的模板。

static方法

  • static:方法在类定义时就已经加载,所以其它地方可以直接用类名调用方法。
  • static:方法在对象定义时才加载,所以其它地方必须先实例化才能用实例(对象)调用方法。
  • 定义类时,在static方法内调用自己的非static方法会失败;在非static方法内可以直接调用自己的非static方法。

值传递和引用传递

java里面调用方法都是值传递,不是引用传递

1
2
3
4
5
6
7
8
9
10
11
12
public class Hello {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.name); //未定义所以打印 null
Hello.change(person); //传了对象(引用传递,址传递)
System.out.println(person.name); //打印出 world
}

public static void change(Person person){ person.name = "world"; }
}

class Person{ String name; }

类和对象

  • 类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但不能代表某一个具体的事物。
  • 对象是抽象概念的具体实例。
  • 使用new关键字创建对象时,除了分配内存空间,还会给对象进行默认的初始化以及对类中构造器的调用。

构造器

  • 类中的构造器也称构造方法,是在进行创建对象的时候必须要调用的,目的在于初始化值。
  • 没有人为初始化则默认为:数字(0,0.0),char(u0000),boolean(false),引用(null)
  • 构造器有两个特点:1.必须和类的名字相同,2.必须没有返回类型,也不能写void。
1
2
3
4
5
6
7
8
9
10
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
//Person humble = new Person(); //Person()实际上是构造方法
Person humble = new Person("Humble");
System.out.println(humble.name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Filename: Person.java */
package com.oop.demo;

public class Person {
//即使留空,默认也会存在一个无参构造方法Person()

/*
String name;
//显式定义无参构造方法
public Person(){ this.name = "hello"; }
*/

String name;
/*
当底下定义了带参构造方法,那其它地方就无法使用无参构造方法来创建实例
如果想保留无参构造方法,只需显式定义一下无参构造方法即可
*/
public Person(){ }

//定义有参构造方法
public Person(String name){ this.name = name; }
}

IDEAtips:Alt+Insert-Constructor-选择属性或不选属性-OK即可光标处快速生成构造方法

创建对象内存分析

1
2
3
4
5
6
内存
├── 栈(stack)
└── 堆(heap)
   ├── new分配区
   └── 方法区
      └── 静态方法区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Hello {
public static void main(String[] args) {
//new会在堆的new分配区申请一片内存并返回地址
//person变量保存在栈,它的值是一个指向堆的地址
Person person = new Person();
System.out.println(person.name); //未定义所以打印 null
Hello.change(person); //传了对象(引用传递,址传递)
System.out.println(person.name); //打印出 world
}

public static void change(Person person){
//person虽然是值传递,但传的却是地址值(引用值)
//地址指向的是堆内new分配区的对象空间
person.name = "world";
}
}

class Person{ String name; }

封装

  • 程序设计应该“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用。
  • 信息隐藏:禁止别人直接访问对象中的数据,而应该通过操作接口来访问。
  • 属性私有:get()或set()

封装的好处:

  1. 提高程序的安全性,保护数据
  2. 隐藏代码实现细节
  3. 统一接口
  4. 增加系统可维护性

IDEAtips:Alt+Insert-Getter and Setter-选择属性或不选属性-OK即可光标处快速生成get()set()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.oop.demo;

public class Student {
private String name;
private int age;
private int id;

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public int getAge() { return age; }
public void setAge(int age) { //用set修改私有属性可以保护数据合法性
if(age > 120 || age < 0){ this.age = 3; }
else { this.age = age; }
}

public int getId() { return id; }
public void setId(int id) { this.id = id; }
}

继承

  • 继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。
  • extends意思是扩展。子类是父类的扩展。
  • java中类只有单继承,没有多继承。
  • 继承关系的两个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示。子类和父类之间,从意义上讲应该具有”is a”的关系。
  • 继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等。
1
2
3
4
public class Student extends Person { } //Student继承Person
public class Student { //Student和person是组合关系
Person person;
}

继承例程

1
2
3
4
5
6
7
8
9
10
11
12
/* Filename: Person.java */
package com.oop.demo;

public class Person {
//public:可以被子类继承
//protected: 可被子继承
//default:(不加修饰就是这个)
//private:不可被子类继承(提供public get()/set())

protected int money = 10_0000_0000;
public void say() { System.out.println("I am a Person"); }
}
1
2
package com.oop.demo;
public class Student extends Person { }
1
2
3
4
5
6
7
8
9
10
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
Student s1 = new Student();
System.out.println(s1.money); //1000000000
s1.say(); //I am a Person
}
}

IDEAtips:Ctrl+h可以打开继承树

java中所有的类都默认直接或间接继承Object类

super

  • super()调用父类的构造方法,必须在构造方法的第一行
  • super只能出现在’子类’的方法或构造方法中
  • super和this不能同时调用构造方法

用super调用父类

1
2
3
4
5
6
7
/* Filename: Person.java */
package com.oop.demo;

public class Person { //如果修饰改为private,那么子类就无法用super直接调用
protected String name = "person1";
public void print(){ System.out.println("Person.print"); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Filename: Student.java */
package com.oop.demo;

public class Student extends Person {
private String name = "student1";

public void test(String name){
System.out.println(name); //${name}
System.out.println(this.name); //student1
System.out.println(super.name); //person1
}

public void print(){ System.out.println("Student.print"); }
public void test1(){
print(); //Student.print
this.print();//Student.print
super.print();//Person.print
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
Student s1 = new Student();
s1.test("hello");
s1.test1();
}
}

/*运行输出
hello
student1
person1
Student.print
Student.print
Person.print
*/

super构造器

1
2
3
4
5
6
7
/* Filename: Person.java */
package com.oop.demo;

public class Person {
public Person(){ System.out.println("Person.Person"); }
public Person(String name){ System.out.println("Person.Person:"+name); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/* Filename: Student.java */
package com.oop.demo;

public class Student extends Person {
public Student() {
//隐藏调用,默认会先调用父类构造器super()。
//如果想显式调用super()则必须放在子类构造器第一行。
//若父类构造器有参,那么调用super()要带参,除非父类构造器重载
//super();
super("hello");
System.out.println("Student.Student");
}
}
1
2
3
4
5
6
7
8
9
10
11
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) { Student s1 = new Student(); }
}

/*运行输出
Person.Person:hello
Student.Student
*/

方法重写

为什么需要方法重写?(父类的功能,子类不一定需要或不一定满足)

方法重写注意:

    1. 需要继承关系,子类重写父类的方法;
    1. 只针对方法,不能是属性,并且是‘对象的方法(即非static修饰的)’,不是‘类的方法’,;
    1. 方法名必须相同;
    1. 返回值与参数列表及类型必须相同(否则就是重载了);
    1. 修饰符:范围可以扩大但不能缩小(即父用public修饰那么子重写时就不能用protected或更小的来修饰),public>protected>default>private;正常情况不会是private,因为这个方法不能用对象去调用,所以只要不是private都可以被子重写。
    1. 抛出的异常:范围可以被缩小但不能扩大;ClassNotFoundException(小)–>Exception(大)

例子1:带static方法(类的方法)该例子不是方法重写

1
2
3
4
5
6
/* Filename: B.java */
package com.oop.demo;

public class B {
public static void test(){ System.out.println("B.test"); }
}
1
2
3
4
5
6
/* Filename: A.java */
package com.oop.demo;

public class A extends B {
public static void test(){ System.out.println("A.test"); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
//类里面的static方法应该用类去调用,这里用对象去调用是为了测试
A a = new A();
a.test(); //最终调用类的方法

//父类的引用指向子类
B b = new A();
b.test(); //最终调用类的方法
}
}

/*运行输出如下,说明方法的调用只和定义的数据类型有关,跟new A()无关
A.test
B.test
*/

例子2:不带static方法(对象的方法)该例子是方法重写

1
2
3
4
5
6
/* Filename: B.java */
package com.oop.demo;

public class B {
public /*static*/ void test(){ System.out.println("B.test"); }
}
1
2
3
4
5
6
7
8
9
10
/* Filename: A.java */
package com.oop.demo;

public class A extends B {
@Override //@表示注解,是有功能的注释。Override表示重写
public void test() {
//super.test(); //默认是调用父类的方法,现在重写
System.out.println("A.test");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
A a = new A();
a.test(); //最终调用对象的方法

B b = new A();
b.test(); //最终调用对象的方法
}
}

/*运行输出如下,只要是new A()就是调用重写后的方法
A.test
A.test
*/

IDEAtips:Alt+InsertOverride Methods

类的方法优先级高于对象的方法。

  • 例子1里面,static修饰的是类方法。b.test()调用的是B类test(),(堆里面静态方法区)
  • 例子2里面,无static修饰是对象方法。b.test()调用的是对象btest(),但对象bA类new出来的,(堆里面new分配区)

重载与重写区别:

  • 重载是同一类里,方法名相同,参数个数或类型不同。
  • 重写是子父类间,子类重写父类的方法,方法名及参数一模一样,方法体不一样。并且不能是类方法,即不能用static修饰。

多态

  • 同一个方法根据调用对象的不同而采取不同的行为方式。
  • 其中调用对象的类型需要在执行过程中才能决定。
  • 一个对象的实际类型是确定的(堆的new分配区),但是指向它的引用(栈)可以是其他类型(父类或关系类)

注意事项:

  • 1.多态是针对方法的,不能是属性。
  • 2.必须有父子关系(继承),否则引用不能转换,比如String s4 = new Student(); 转换异常ClassCastException
  • 3.存在条件:必须父子关系(继承);方法需要重写(只有运行时才知道调用父方法还是子重写的方法);父类引用指向子类;

不能被子重写的方法:

  • 1.static方法,属于类,不属于实例
  • 2.final常量,
  • 3.private方法(因为不能继承)
1
2
3
4
/* Filename: Person.java */
package com.oop.demo;

public class Person { public void run(){ System.out.println("Person.run"); } }
1
2
3
4
5
6
7
8
9
/* Filename: Student.java */
package com.oop.demo;

public class Student extends Person {
@Override
public void run() { System.out.println("Student.run"); }

public void eat(){ System.out.println("Student.eat"); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
Student s1 = new Student();
Person s2 = new Student();
Object s3 = new Student();
//String s4 = new Student(); //报错,引用只能是同类或父类

//如果Student没有Override run方法,那两个都是Person.run
//如果Student有Override run方法,那两个都是Student.run
s1.run();
s2.run();
//s3.run(); //报错,Object没有run方法

s1.eat();
//s2.eat(); //报错,因为父(Person)没有子(Student)的方法(eat)所以不能调,可以先强转为Student再调用

/**
* 所以,左边定义的类里有什么方法就只能调这些方法(自己的或继承的)
* 如果这些方法已被子类重写那调用就是重写后的方法
*/
}
}

instanceof

X instanceof Y,Y 必须是 引用X所指向的实例 的父类/同类才为真

1
2
3
/* Filename: Person.java */
package com.oop.demo;
public class Person { public void run(){ System.out.println("Person.run"); } }
1
2
3
/* Filename: Student.java */
package com.oop.demo;
public class Student extends Person { }
1
2
3
/* Filename: Teacher.java */
package com.oop.demo;
public class Teacher extends Person { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
Object o = new Student();
System.out.println(o instanceof Student); //true
System.out.println(o instanceof Person); //true
System.out.println(o instanceof Object); //true
System.out.println(o instanceof Teacher); //false
System.out.println(o instanceof String); //false
System.out.println("=====");
Person p = new Student();
System.out.println(p instanceof Student); //true(说Person是Student不成立,但引用p指向的实例是Student)
System.out.println(p instanceof Person); //true
System.out.println(p instanceof Object); //true
System.out.println(p instanceof Teacher); //false
//System.out.println(p instanceof String); //编译报错
System.out.println("=====");
Student s = new Student();
System.out.println(s instanceof Student); //true
System.out.println(s instanceof Person); //true
System.out.println(s instanceof Object); //true
//System.out.println(s instanceof Teacher); //编译报错
//System.out.println(s instanceof String); //编译报错
}
}

类的转换

  • 1.类的转换是发生在栈上,也就是转换引用而已
  • 2.把子类‘直接’转为父类,父类的引用(栈)指向子类的对象(堆),但是会丢失子类的方法
  • 3.把父类‘强制’转为子类,子类的引用(栈)指向父类的对象(堆),编译报错或运行报错,因为子类引用(栈)里有些方法在父类对象(堆)内找不到
  • 4.类的转换方便方法的调用,减少重复的代码
1
2
3
/* Filename: Person.java */
package com.oop.demo;
public class Person { public void run(){ System.out.println("Person.run"); } }
1
2
3
/* Filename: Student.java */
package com.oop.demo;
public class Student extends Person { public void go(){ System.out.println("Student.go"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Filename: Application.java */
package com.oop.demo;

public class Application {
public static void main(String[] args) {
Person p = new Person();
//Student conversion2s1 = p; //编译报错,因为堆内的对象为Person(父),不能用栈内Student(子)引用去指向它,需要强转
//Student conversion2s2 = (Student) p; //编译通过但运行报错,原因同上

Student s = new Student();
Person conversion2p = s; //子类转父类
//conversion2p.go(); //编译报错,因为栈内引用Person没有go方法,也就是丢失了Student(子)的方法go
Student reset2s = (Student)conversion2p;
reset2s.go(); //打印Student.go,证明转为父类会丢失子类方法go,再转回子类又恢复了方法go
}
}

static

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Filename:Student.java */
package com.oop.demo;

public class Student {
private static int age;
private double score;

public static void main(String[] args) {
Student s1 = new Student();

Student.age = 10; //类成员
s1.age = 20; //此处访问的也是类成员
System.out.println(s1.age); //20
System.out.println(Student.age); //20

//Student.score = 10; //编译报错,类被加载时不会分配非staitc的属性,非static属性是属于对象的
s1.score = 20;
}
}

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Filename:Student.java */
package com.oop.demo;
public class Student {
public void run(){
go(); //对象方法内可以调用类的方法
}
public static void go(){
//run(); //编译报错:类的方法内不可以调用对象的方法
}
public static void main(String[] args) {
//run(); //编译报错:类的方法内不可以调用对象的方法
go(); //类可以直接调用类自己的方法
}
}

static代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Filename:Person.java */
package com.oop.demo;

public class Person {
{ //2 :可以在里面赋初值
System.out.println("匿名代码块");
}

static{ //1 静态代码块只执行/初始化一次
System.out.println("静态代码块");
}

public Person() { //3
System.out.println("构造方法");
}

public static void main(String[] args) {
Person p1 = new Person();
System.out.println("====");
Person p2 = new Person();
/**运行输出
* 静态代码块
* 匿名代码块
* 构造方法
* ====
* 匿名代码块
* 构造方法
*/
}
}

外部package的函数导入

1
2
3
4
5
6
7
8
9
10
11
/* Filename:Test.java */
package com.oop.demo;
import static java.lang.Math.random;
import static java.lang.Math.PI;
public class Test {
public static void main(String[] args) {
System.out.println(Math.random()); //用package去调用random
System.out.println(random()); //也可以先import static java.lang.Math.random之后就可以直接调用
System.out.println(PI);
}
}

final类

final修饰的类是没有子类的。

抽象类

  • abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么就是抽象方法;修饰类那就是抽象类
  • 抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类。
  • 抽象类不能用new关键字来创建对象,它是用来让子类继承的。
  • 抽象方法,只有方法的声明,没有方法的实现,它是用来让子类实现的。
  • 子类继承抽象类,那么就必须要实现抽象类里没有实现的抽象方法,否则该子类也要声明为抽象类。
1
2
3
4
5
6
7
8
9
10
11
/* Filename:Action.java */
package com.oop.demo;

//抽象(abstract)类,不能被new出来,只能被继承
public abstract class Action {
//抽象(abstract)方法如果:
// 被"abstract修饰的子类"继承可以不写方法体;
// 被"非abstract修饰的子类"继承需要写方法体。
public abstract void doSomething(); //抽象方法目的在于约束子类必须实现
public void go(){ System.out.println("Action.go"); }
}
1
2
3
4
5
6
7
8
9
/* Filename:A.java */
package com.oop.demo;
public abstract class A extends Action {
//因为doSomething()是抽象方法,所以子类(A)必须重写/实现出来,如果子类(A)是抽象类则不用重写/实现
//@Override
//public void doSomething() { }

//Action.go()不是抽象方法,所以子类(A)可以重写/实现,也可以不重写/实现
}

javaTips:类是单继承,接口可以多继承

思考题:1.抽象类不能new,那它有构造器吗?2.抽象类存在意义是什么?(游戏角色抽象出来,让子类继承去实现。提高开发效率,可扩展性高)

接口

  • 普通类:只有具体实现

  • 抽象类:具体实现和规范(抽象方法)都有

  • 接口:只有规范(使得约束和实现分离,项目组长约定好接口,员工面向接口编程)

  • 接口就是规范,定义是一组规则,体现了现实世界中“如果你是…则必须能…”的意思。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。

  • 接口本质是契约,就像我们人间的法律一样。制定好后大家都遵守。

  • OO的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(如C+++,java,C#等),就是因为设计模式所研究的,实际上就是如何合理去抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Filename:UserService.java */
package com.humble.oop;
public interface UserService {
//接口内的方法默认是public abstract,可省略不写
///*public abstract*/ void run(String name);
void add(String name);
void delete(String name);
void update(String name);
void query(String name);

//接口内的属性默认是常量 public static final(一般不这样定义使用)
/*public static final*/ int AGE=99;
}
1
2
3
/* Filename:TimeService.java */
package com.humble.oop;
public interface TimeService { void timer(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Filename:UserServiceImp.java */
package com.humble.oop;

//接口都需要有实现类,它不能直接被实例化,因为没构造方法
//如果类 implements 了接口,则必须重写里面所有的方法
//类是多继承(extends)的,接口是多实现(implements)的,或者说,类可以集成实现多个接口(interface)
public class UserServiceImp implements UserService,TimeService{
@Override
public void add(String name) { }

@Override
public void delete(String name) { }

@Override
public void update(String name) { }

@Override
public void query(String name) { }

@Override
public void timer() { }
}

内部类

内部类就是在一个类内部再定义一个类。比如A类中定义一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了。

  • 1.成员内部类
  • 2.静态内部类
  • 3.局部内部类
  • 4.匿名内部类

成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Filename:Outer.java */
package com.humble.oop;
public class Outer {
private int id = 10;
public void out(){ System.out.println("这是外部方法"); }

public class Inner { //成员内部类
public void in(){ System.out.println("这是内部方法"); }

//直接访问外部类的私有属性
public void getID(){ System.out.println("id = " + id); }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
/* Filename:Application.java */
package com.humble.oop;

public class Application {
public static void main(String[] args) {
Outer outer = new Outer();
//通过外部类的'实例'去new 内部类
Outer.Inner inner = outer.new Inner();
inner.in(); //这是内部方法
inner.getID(); //id = 10
}
}

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Filename:Outer.java */
package com.humble.oop;
public class Outer {
private int id = 10;
public void out(){ System.out.println("这是外部方法"); }

public static class Inner { //静态内部类
public void in(){ System.out.println("这是内部方法"); }

//编译报错:Inner在静态区,id属性不在静态区,除非用static修饰id,否则必须实例化才能访问id
//public void getID(){ System.out.println("id = " + id); }
}
}
1
2
3
4
5
6
7
8
9
10
11
/* Filename:Application.java */
package com.humble.oop;

public class Application {
public static void main(String[] args) {
Outer outer = new Outer();
//直接new 外部类里的内部类
Outer.Inner inner = new Outer.Inner();
inner.in(); //这是内部方法
}
}

注意:

1
2
3
4
5
6
7
8
9
10
11
/* Filename:outer.java */
package com.humble.oop;

public class Outer { }

//一个java文件中可以有多个class,但只有一个public class
class A{
public static void main(String[] args) {
System.out.println("A.main");
}
}

局部内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Filename:outer.java */
package com.humble.oop;

public class Outer {
public void method(){
//局部内部类
class Inner {
public void in(){
System.out.println("Inner.in");
}
}
}
}

匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Filename:Test.java */
package com.humble.oop;

public class Test {
public static void main(String[] args) {
//不需要变量保存实例,没有名字初始化类(匿名)
new Apple().eat(); //Apple.eat

//也是匿名类
UserService userService = new UserService() {
@Override //运行的时候才重写/实现接口的方法,也就等于定义了新的类,并且用new实例化了,保存在userService中
public void hello() {
System.out.println("Test.hello");
}
};
userService.hello();//Test.hello
}
}

class Apple {
public void eat(){ System.out.println("Apple.eat"); }
}

interface UserService{
void hello();
}

异常

  • 运行过程中出现状况:用户输入不合法;打开一个文件可是文件不存在或格式不合法;读数据库却是空的;内存耗光或硬盘满了……
  • 软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,叫做异常(Exception),翻译是例外,可以理解为例外的情况就是异常情况。
1
2
3
4
5
6
7
/* Filename:Application.java */
package com.humble.oop;
public class Application {
public static void main(String[] args) {
System.out.println(11/0); //Exception in thread "main" java.lang.ArithmeticException: / by zero
}
}

3种类型的异常:

  • 1.检查性异常,最具代表的检查性异常是用户错误输入引起的异常,这是开发者无法预见的。如:打开一个不存在的文件就会引发异常。这个异常在编译时不能被简单地忽略。
  • 2.运行时异常,最容易被避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 3.错误,错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如:当栈溢出的错误没办法在编译时就检查到。

Java把异常当作对象处理,并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception

Error

  • Error类对象由Java虚拟机生成并抛出,大多数错误跟代码编写者所写的无关。
  • Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
  • 还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且大多数是程序运行时不允许出现这样的情况。

Exception

Exception分支中有一个重要的自类RuntimeException(运行时异常)

  • ArrayIndexOutOfBoundsException(数组下标越界)
  • NullPointerException(空指针异常)
  • ArithmeticException(算术异常)
  • MissingResourceException(丢失资源)
  • ClassNotFoundException(找不到类)

  • 这些是不检查异常,程序中可以选择捕获处理,也可不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

ErrorException的区别:

  • Error通常是灾难性、致命性的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程。
  • Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常

捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Filename:Test.java */
package com.humble.oop;

public class Test {
public static void main(String[] args) {
int a = 1;
int b = 0;

//try catch 需要有,finally可以没有。finally一般是需要在异常后关闭IO、释放资源的情况写
try{ //try监控区域
System.out.println(a / b);
}catch (ArithmeticException e){ //catch 捕获到异常后的处理区
System.out.println("捕获异常,除数不能为0");
}finally { //处理善后区,不管有无捕获异常都会处理,
System.out.println("finally");
}
}
}

捕获多个异常需要从小到大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Filename:Test.java */
package com.humble.oop;

public class Test {
public static void main(String[] args) {
int a = 1;
int b = 0;

try{
System.out.println(a / b);
}
catch (Error e){
System.out.println("Error");
}
catch (Exception e){
System.out.println("Exception");
}
catch (Throwable e){
System.out.println("Throwable");
}
finally {
System.out.println("finally");
}
}
}

IDEAtips:选中代码,按Ctrl+Alt+t可弹出补全逻辑

抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Filename:Test2.java */
package com.humble.oop;

public class Test2 {
public static void main(String[] args) {
try {
new Test2().test(1,0);
} catch (ArithmeticException e) {
e.printStackTrace();
}
}

//注意 throw 和 throws 两关键字
public void test(int a, int b) throws ArithmeticException{
if (b == 0) {
throw new ArithmeticException(); //throw 一般是在方法内处理不了,于是主动往更高级抛出异常
}
System.out.println(a / b);
}
}

自定义异常

使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常(只需继承Exception类即可)

自定义异常类步骤:

  • 1.创建自定义异常类
  • 2.在方法中通过throw关键字抛出异常对象
  • 3.如果在当前抛出异常的方法中处理异常,则可使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作
  • 4.在出现异常方法的调用者中捕获并处理异常

IDEAtips:双击Shift可弹出搜索框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Filename:MyException.java */
package com.humble.oop;

//自定义异常类
public class MyException extends Exception{
//传递数字>10
private int detail;

public MyException(int detail) {
this.detail = detail;
}

//toString:异常打印信息
@Override
public String toString() {
return "MyException{" + "detail=" + detail + '}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Filename:Test.java */
package com.humble.oop;

public class Test {
static void test(int a) throws MyException {
System.out.println("a = " + a);
if (a > 10){
throw new MyException(a);//抛出
}
System.out.println("Test.test");
}

public static void main(String[] args) {
try {
test(11);
} catch (MyException e) {
System.out.println("e = " + e);
}
}
}

/** 运行输出
* a = 11
* e = MyException{detail=11}
*/
  • 处理运行时异常时,采用逻辑去合理规避同时辅助try-catch处理
  • 在多重catch块后面,可以加一个catch(Exception)来处理可能会被遗漏的异常
  • 对于不确定的代码,可以加上try-catch,处理潜在的异常
  • 尽量去处理异常,切忌只是简单地调用printStackTrace()去打印输出
  • 具体如何处理异常,要根据不同的业务需求和异常类型去决定
  • 尽量添加finally语句块去释放占用的资源

C语言语法笔记

打印限定长度字符串

1
2
3
char *s = "This my world.";
printf("Hello, %.5s!\n", s+8); printf("Hello, %.*s!\n", 5, s+8);
int minlen=5,maxlen=5; printf("Hello, %*.*s\n", minlen, maxlen, s+8);

打印后回填长度至参数’%n’

1
char *str = "world!"; int len = 0; printf("Hello %s\n%n", str, &len); printf(" = %d\n", len); // = 13

打印double格式’%f’

1
2
float f = 1.4; printf("%f", f);  //正确,f会被提升到double再用%f打印
double d = 1.4; printf("%f", d); //正确,若指定"%lf",则l无意义

implicit declaration of function ‘strdup’

#include <string.h>前面添加下面的其中任一宏定义

1
2
3
_SVID_SOURCE || _BSD_SOURCE || _XOPEN_SOURCE >= 500
|| _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED
|| /* Since glibc 2.12: */ _POSIX_C_SOURCE >= 200809L

或者在gcc编译选项内加上

1
-D_SVID_SOURCE

或者

1
-D_POSIX_C_SOURCE=200809L

编译链接动态库找不到libtcmalloc_minimal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dpkg -L libtcmalloc-minimal0
/.
/usr
/usr/lib
/usr/lib/libtcmalloc_minimal.so.0.0.0
/usr/lib/libtcmalloc_minimal_debug.so.0.0.0
/usr/share
/usr/share/doc
/usr/share/doc/libtcmalloc-minimal0
/usr/share/doc/libtcmalloc-minimal0/TODO
/usr/share/doc/libtcmalloc-minimal0/AUTHORS
/usr/share/doc/libtcmalloc-minimal0/copyright
/usr/share/doc/libtcmalloc-minimal0/changelog.gz
/usr/share/doc/libtcmalloc-minimal0/README.gz
/usr/share/doc/libtcmalloc-minimal0/changelog.Debian.gz
/usr/lib/libtcmalloc_minimal.so.0
/usr/lib/libtcmalloc_minimal_debug.so.0

编译使用-lxxx默认会去环境变量对应库目录找xxx.so后缀的库文件,找不到libtcmalloc_minimal.so所以编译失败。下面有4个解决方法:

  1. 安装libtcmalloc_minimal0-dev之后就会有libtcmalloc_minimal.so文件
  2. 进入库目录,手动生成符号链接文件ln -s libtcmalloc_minimal.so.0.0.0 libtcmalloc_minimal.so
  3. 编译时指定真实文件绝对路径gcc test /usr/lib/libtcmalloc_minimal.so.0.0.0
  4. 编译时用-l指定文件全名-l:libtcmalloc_minimal.so.0.0.0

ctime使用

printf("%s", ctime(&time(NULL))); 本来以为可以省掉一个中间变量timer,而且显得高大上。编译时给出错误: 单目‘&’的操作数必须是左值,这段代码想直接对time()的返回值取地址,这是绝对不正确的,因为函数返回值是匿名变量,只能当右值,不可以做左值

打印带颜色和属性的字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// foreground colours
printf("\033[30m black \033[m\n");
printf("\033[31m red \033[m\n");
printf("\033[32m green \033[m\n");
printf("\033[33m yellow \033[m\n");
printf("\033[34m blue \033[m\n");
printf("\033[35m magenta \033[m\n");
printf("\033[36m cyan \033[m\n");
printf("\033[37m white \033[m\n");

// background colours
printf("\033[40m black \033[m\n");
printf("\033[41m red \033[m\n");
printf("\033[42m green \033[m\n");
printf("\033[43m yellow \033[m\n");
printf("\033[44m blue \033[m\n");
printf("\033[45m magenta \033[m\n");
printf("\033[46m cyan \033[m\n");
printf("\033[47m white \033[m\n");

// other
printf("\033[0m default \033[m\n");
printf("\033[1m bold \033[m\n");
printf("\033[2m faint \033[m\n");
printf("\033[3m italic \033[m\n");
printf("\033[4m underlined \033[m\n");
printf("\033[5m slowblink \033[m\n");
printf("\033[6m rapidblink \033[m\n");
printf("\033[7m negative \033[m\n");

查看内存泄露

1
cat /proc/`pidof hello`/status |grep 'VmData:'

make传参

1
make CFLAGS=-m32 LDFLAGS=-m32  # 编译链接32位

64位系统编译32位libpcap

  • 报错/usr/bin/ld: 当搜索用于 /usr/lib/libnl-3.so 时跳过不兼容的 -lnl-3就安装lib32-libnl
  • 报错/usr/bin/ld: 找不到 -ldbus-1: 没有那个文件或目录就安装lib32-dbus

交叉编译术语

3种机器

  • Build machine: where the toolchain is built(用来构建gcc工具链的机器)
  • Host machine: where the toolchain will be executed(运行gcc工具链的机器)
  • Target machine: where the binaries created by the toolchain are executed(gcc工具链创建的二进制文件运行时所在的机器)

4种构建方式

  • Native build:BUILD==HOST==TARGET(三个都是x86)本机构建
  • Cross-build:BUILD==HOST!=TARGET(交叉构建BUILD==HOST==x86 TARGET==arm)在x86上编译出gcc工具链,在x86上使用该gcc编译出arm的程序
  • Cross-native build:BUILD!=HOST==TARGET(跨本机构建BUILD==x86 HOST==TARGET==arm)在x86编译出gcc工具链,把gcc工具链拷贝到arm,在arm用gcc编译出arm的程序
  • Canadian toolchain:BUILD!=HOST!=TARGET(加拿大工具链BUILD==x86 HOST==mac TARGET==arm)在x86上编译出gcc工具链,拷贝到mac,在mac上使用该gcc编译出arm的程序

configure参数

1
2
3
--build=BUILD           configure for building on BUILD [BUILD=HOST]
--host=HOST configure for HOST [guessed]
--target=TARGET configure for TARGET [TARGET=HOST]

如你在使用arm-none-linux-gnueabi-gcc,可设置--host=arm-none-linux-gnueabi --target=arm-none-linux-gnueabi,而--build会自动被设置

指定编译器

1
./configure CC=/tmp/arm/arm-linux-gcc CXX=/tmp/arm/arm-linux-g++

修改环境变量

1
2
3
4
export C_INCLUDE_PATH=${HOME}/path/to/include:$C_INCLUDE_PATH
export CPLUS_INCLUDE_PATH=${HOME}/path/to/include:$CPLUS_INCLUDE_PATH
export LIBRARY_PATH=${HOME}/path/to/lib:$LIBRARY_PATH #编译时在这些路径下进行查找库
export LD_LIBRARY_PATH=${HOME}/path/to/lib:$LD_LIBRARY_PATH #运行时在这些路径下查找库

openssl

在第一个参数位置指定目标架构,-–cross-compile-prefix指定了命令前缀

1
2
./Configure linux-aarch64 --cross-compile-prefix=aarch64-himix100-linux- --prefix=/tmp/result && make && make install
#./Configure linux-armv4 --cross-compile-prefix=arm-himix200-linux- --prefix=/tmp/result && make && make install

若报错

1
2
3
4
crypto/aes/aes-x86_64.s: Assembler messages:
crypto/aes/aes-x86_64.s:2: Error: unrecognized symbol type ""
crypto/aes/aes-x86_64.s:3: Error: alignment too large: 15 assumed
crypto/aes/aes-x86_64.s:5: Error: bad instruction `xorl 0(%r15),%eax'

从错误打印看到,错误出现在编译 .s 文件(汇编文件)时,且后面还打印了汇编代码。原来 openssl 在编译时会默认使用汇编代码来加速编译过程,但只针对 x86平台,而 x86平台 的汇编代码和 arm平台 的汇编代码是不同的,所以会报错。加no-asm解决,表示不使用汇编代码加速编译

1
./Configure linux-aarch64 no-asm --cross-compile-prefix=aarch64-himix100-linux- --prefix=/tmp/result && make && make install

若报错

1
cc1: error: unrecognized command line option "-m64"

删除Makefile-m64

1
sed -i "s/ -m64//g" Makefile

在64位机器编译32位的openssl

setarch i386 ./config -m32./Configure -m32 linux-generic32./Configure linux-generic32 --shared

编译参数

-CFLAGS:C 编译器的选项,如指定头文件(.h文件)的路径,CFLAGS=-I/usr/include -I/path/include
-CXXFLAGS:C++ 编译器的选项。
-LDFLAGS:编译器会用到的一些优化参数,也可以在里面指定库文件的位置。LDFLAGS=-L/usr/lib -L/path/to/your/lib
-LIBS:告诉链接器要链接哪些库文件,如LIBS = -lpthread -liconvLDFLAGS告诉链接器库文件在哪个目录,LIBS是告诉链接器要链接哪些库文件

运行时找不到库,就需要在编译时给LDFLAGS增加-Wl,RLDFLAGS = -L/var/xxx/lib -L/opt/mysql/lib -Wl,R/var/xxx/lib -Wl,R/opt/mysql/lib

如果在./configure前就设置了环境变量export LDFLAGS="-L/var/xxx/lib -L/opt/mysql/lib -Wl,R/var/xxx/lib -Wl,R/opt/mysql/lib" ,那么在./configure后,Makefile将会设置LDFLAGS这个选项,链 接时会有这个参数,编译出来的可执行程序的库文件搜索路径就得到扩展了。

ldd查看程序运行缺的依赖库

1
2
3
4
$ ldd hello
...
libzlog.so.1 => not found
...

readelf查看库的soname

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ readelf -d /usr/lib/libzlog.so.1.2

Dynamic section at offset 0x18db8 contains 25 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000e (SONAME) Library soname: [libzlog.so.1]
0x000000000000000c (INIT) 0x4000
0x000000000000000d (FINI) 0x12884
0x0000000000000019 (INIT_ARRAY) 0x19da8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x19db0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x300
0x0000000000000005 (STRTAB) 0x18f0
0x0000000000000006 (SYMTAB) 0x6f0
0x000000000000000a (STRSZ) 2977 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x1a000
0x0000000000000002 (PLTRELSZ) 3672 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x2858
0x0000000000000007 (RELA) 0x26a8
0x0000000000000008 (RELASZ) 432 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x2618
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x2492
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0

apue高级IO

apue高级IO

非阻塞 I/O

之前讨论的所有函数都是阻塞的,如read(2)函数读取设备时,设备中如果没有充足的数据,那么read(2)函数就会阻塞等待,直到有数据可读再返回。当 IO 操作时出错时,需要判断是真的出错还是假错。

两种假错的情况:

  • EINTR:被信号打断,阻塞时会遇到。
  • EAGAIN:非阻塞形式操作失败。

遇到这两种假错的时候我们需要重新再操作一次(占用CPU),所以通常对假错的判断是放在循环中的。例如read(2)函数使用非阻塞方式读取数据时,如果没有读取到数据,errnoEAGAIN,表明是非阻塞方式读取返回了,并非设备有问题或读取失败。

阻塞与非阻塞是使用的同一套函数,flags特殊要求指定为O_NONBLOCK就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fd = open("/etc/service", O_RDONLY | O_NONBLOCK); //以非阻塞方式打开
/* if error */

while (1) {
size = read(fd, buf, BUFSIZE);
if (size < 0) {
if (EAGAIN == errno) {
continue; //发生假错,重复read
}
perror("read()");
exit(1);
}
}
// do sth...

IO多路转接

IO密集型的任务,可以使用IO多路转接。select(),poll(),epoll()

先布置监视任务(设置自己关心/感兴趣的事件/文件描述符),监视多个IO(文件描述符),当某个文件描述符发生了我们感兴趣的事情(读/写)的时候再去操作(读/写)。

select(2)移植性高,各个平台都支持它,这也是它相对于poll(2)唯一的优点。

select(2)关心的是事件,poll()关心的是文件描述符。

select(2)poll()都是可移植的,epoll()是Linux平台实现的方言。epoll()是利用poll()封装的。

select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//IO多路转接(IO复用)
//fd_set 文件描述符集合
//nfds:你所监视fd_set中最大的那个再加1(如:监视1,4,那nfds取5,因为内核用的是<nfds,不加1就无法监视最大那个)
//readfds:你关心哪些文件描述符的可读事件
//writefds:你关心哪些文件描述符的可写事件
//exceptfds:你关心哪些文件描述符的异常事件
//timeout:阻塞的超时时间,如果不设置则阻塞等到关心事件发生才返回
//返回:
// 表示有多少个文件描述符发生了你关心的事件,发生的事件回填到了readfds,writefds,exceptfds里面(下次又得重新设置这3个再调用select)
// 出错返回-1,并设置errno(若设置了超时,超时过后返回-1,errno设为EINTR(假错))
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set); //从set中删除fd
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set中
void FD_SET(int fd, fd_set *set); //把fd加入set中
void FD_ZERO(fd_set *set); //把set清空

select(2)可相当于一个安全可靠的休眠。某些平台的sleep()用的是信号,不是很安全。

查看代码

1
2
./io/adv/select/relay.c
#define use_select (1)

poll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//等待某个事件在指定的文件描述符上发生
//fds:结构体数组
//nfds:结构体数组长度(有多少个文件描述符)
//timeout:超时设置(毫秒),0-非阻塞,-1-阻塞
//返回:
// 正数,有多少个事件发生了
// 0,没有发生事件,超时返回
// -1,出错并设置errno(EINTR是假错)
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; //file descriptor 文件描述符
short events; // requested events 关心的事件
short revents; // returned events 发生的事件
};

关心的事件和发生的事件是分开的两个成员,所以关心的事件发生一次后不用重新设置。

epoll

1
2
3
4
5
6
7
//创建一个epoll文件描述符(创建数组)
//size:忽略(随便填个正数)
//返回:
// 正数,成功,表示有多少个文件描述符。
// -1,出错并设置errno
int epoll_create(int size);
int epoll_create1(int flags);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//控制epoll文件描述符
//对epfd实例用的fd进行op操作event
//epfd:用epoll_create创建的实例
//op:EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL
//fd:目标文件描述符
//event:目标的事件
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);

typedef union epoll_data {
void *ptr;
int fd; //目标文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events; // Epoll events 关心的事件(位图)
epoll_data_t data; // User data variable
};
1
2
3
4
5
6
7
8
9
10
11
12
13
//往外取正在发生的事件
//epfd:用epoll_create创建的实例
//event:取出来的事件回填的地址(数组)
//maxevents:想要取多少个事件
//timeout:超时设置(毫秒),0-非阻塞,-1-阻塞
//返回:正数,有多少个文件描述符发生了关心的事件
// 0,没有发生事件超时返回
// -1,出错并设置errno(EINTR是假错)
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);

poll是自己定义一个数组,每个元素就是一个文件描述符以及其关心和发生的事件,epoll是内核帮你维护了这样一个数组。epoll_create()就是创建这个数组,再用其他函数去访问/修改该数组。

select&poll&epoll

select()缺点:

1.fd_set使用数组实现,fd_size有限制 1024bitmapfd[i] = accept()
2.fdset不可重用,新的fd进来,重新创建
3.用户态和内核态拷贝bitmap产生开销
4.O(n)时间复杂度的轮询(发生事件后的轮询)

poll基于结构体存储fd,解决了select的缺点1,2

1
2
3
4
5
struct pollfd{
int fd;
short events;
short revents; //可重用
}

epoll:用户态和内核态不拷贝bitmap;返回结果不需要轮询,时间复杂度为O(1),解决select的缺点1,2,3,4

epoll_create 创建一个白板 存放fd_events
epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符

两种触发模式:
LT:水平触发
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。
下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,
因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

apue线程

apue线程

1.线程的概念

线程就是一个正在运行的函数。

实际项目中多线程用得比较多,因为多线程是先有标准后有实现的,所以不会向多进程那样在不同平台上有许多不同的情况。

C 语言线程有很多标准,POSIX 是其中的一种(还有比如openmp标准线程)。POSIX 是一套标准,而不是一种实现。所以 POSIX 只是规定了 pthread_t 作为线程标识符,但是并没有规定它必须是由什么类型组成的。在有的平台上它可能是 int,有些平台上它可能是 struct,还有些平台上它可能是 union,所以不要直接操作这个类型,而是要使用 POSIX 规定的各种线程函数来操作它。

有木有觉得像标准 IO 里 FILE 的赶脚?没错,标准制定出来的很多东西都是这种风格的,它为你提供一个数据类型而不让你直接对这个类型操作,要通过它定义的一系列函数来实现对这个类型的操作,这样就在各个平台上实现统一的接口了,所以这样做才能让标准制定出来的东西具有较好的可移植性。

pthread_t 是个很重要的东西,我们所有使用 POSIX 标准的线程操作都是围绕着它来进行的,通过它配合各种函数就可以对线程进行各种花样作死的玩了。:)

线程没有父子关系,是兄弟关系,可以相互收尸(别说主线程,建议说main线程)。线程间比进程间通讯简单,因为线程共用一片地址空间。

查看线程

ps axf 查看进程关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# li @ evpower in ~ [14:31:02]
$ ps axf
PID TTY STAT TIME COMMAND
2 ? S 0:00 [kthreadd]
3 ? I< 0:00 \_ [rcu_gp]
4 ? I< 0:00 \_ [rcu_par_gp]
6 ? I< 0:00 \_ [kworker/0:0H-kblockd]
9 ? I< 0:00 \_ [mm_percpu_wq]
...
1 ? Ss 0:03 /sbin/init splash
689 ? Ss 0:00 avahi-daemon: running [evpower.local]
738 ? S 0:00 \_ avahi-daemon: chroot helper
701 ? Ss 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
829 ? Ssl 0:00 /usr/sbin/gdm3
1610 ? Sl 0:00 \_ gdm-session-worker [pam/gdm-password]
1712 tty2 Ssl+ 0:00 \_ /usr/lib/gdm3/gdm-x-session --register-session --run-script i3
1714 tty2 Sl+ 2:09 \_ /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xa
1745 tty2 S+ 0:02 \_ i3
1824 ? Ss 0:00 \_ /usr/bin/ssh-agent /usr/bin/im-launch i3

ps axm 查看进程详细信息,包括线程(- -表示线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    PID TTY      STAT   TIME COMMAND
1 ? - 0:04 /sbin/init splash
- - Ss 0:04 -
2 ? - 0:00 [kthreadd]
- - S 0:00 -
3 ? - 0:00 [rcu_gp]
- - I< 0:00 -
...
693 ? - 0:03 /usr/sbin/NetworkManager --no-daemon
- - Ssl 0:03 -
- - Ssl 0:00 -
- - Ssl 0:00 -
700 ? - 0:00 /usr/sbin/irqbalance --foreground
- - Ssl 0:00 -
- - Ssl 0:00 -
701 ? - 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
- - Ss 0:00 -

ps ax -L也可以查进程与线程(PID是进程id,LWP是轻量级进程id,常称线程id,也占一个PID)

1
2
3
4
5
6
7
8
9
10
11
12
    PID     LWP TTY      STAT   TIME COMMAND
1 1 ? Ss 0:04 /sbin/init splash
2 2 ? S 0:00 [kthreadd]
3 3 ? I< 0:00 [rcu_gp]
...
708 708 ? Ssl 0:00 /usr/lib/policykit-1/polkitd --no-debug
708 715 ? Ssl 0:00 /usr/lib/policykit-1/polkitd --no-debug
708 769 ? Ssl 0:00 /usr/lib/policykit-1/polkitd --no-debug
710 710 ? Ssl 0:00 /usr/sbin/rsyslogd -n -iNONE
710 764 ? Ssl 0:00 /usr/sbin/rsyslogd -n -iNONE
710 765 ? Ssl 0:00 /usr/sbin/rsyslogd -n -iNONE
710 766 ? Ssl 0:00 /usr/sbin/rsyslogd -n -iNONE

Linux以线程为维护单位,也就是PID,如果线程没有被收尸,那尸体就占用一个PID。

pthread_equal

1
2
//比较两个线程标识符是否相同,相同返回非0,不同则返回0。
int pthread_equal(pthread_t t1, pthread_t t2);

为什么不能使用 if (t1 == t2) 的方式比较两个线程标识符呢?因为各系统实现不一样,你不知道 pthread_t 是什么类型的,所以永远不要自己直接操作它。

pthread_self

1
2
//获得当前线程 ID,永远不出错
pthread_t pthread_self(void);

2.线程的创建&终止&取消&栈清理

pthread_create

1
2
3
4
5
6
7
8
//创建线程,成功返回0,失败返回errno
//某些平台上 errno 是全局变量,为避免在多线程的情况下出现竞争,POSIX 指定在失败的时直接返回 errno
//创建后的线程id回填到thread
//attr指定创建线程具有的属性(常用NULL就够用)
//start_routine 指定函数也就是线程主体
//arg指定传给start_routine的参数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

pthread_exit

线程终止有3中方式,pthread_exit 只是其一

  1. 线程从启动例程返回,返回值就是线程退出码
  2. 线程可以被同一进程中的其他线程取消
  3. 线程调用pthread_exit()
1
2
//终止线程,并通过retval返回一个值
void pthread_exit(void *retval);

pthread_join

1
2
3
//收thread的尸体,并把thread返回的值回填到retval
//成功返回0,失败返回errno
int pthread_join(pthread_t thread, void **retval);

类似进程里面的wait()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* Filename: creat1.c
* No.83-84.线程-线程创建&线程终止和栈清理
* $ ps axm
* $ ps ax -L
* Description:
* $ gcc -Wall creat1.c -o creat1 -lpthread
* Last modified: humble 2020-09-01 15:29:48
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
//#include <unistd.h>

static void *func(void *p)
{
printf("func %d\n", *(int *)p);
pthread_exit(3);
//pthread_exit(NULL);
//return NULL; //pthread_exit可以实现线程清理,retuan NULL不可以
}

int main(int argc, char **argv)
{
int i = 2;
int retval = -1;
pthread_t tid;

puts("begin");
int err = pthread_create(&tid, NULL, func, (void *)&i);
if(err){
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}

//线程的调度取决于调度器的策略
//调用pthread_create后,可能main线程先往下运行,也可能func线程先运行
//所以"func 7"和"main"打印顺序可能不确定

//sleep(1);
puts("main");

//为tid收尸,并用retval接收tid返回值
pthread_join(tid, &retval); //类似进程的wati()

printf("end %d\n", retval);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [18:32:28]
$ gcc -Wall creat1.c -o creat1 -lpthread
creat1.c: In function ‘func’:
creat1.c:18:18: warning: passing argument 1 of ‘pthread_exit’ makes pointer from integer without a cast [-Wint-conversion]
18 | pthread_exit(3);
| ^
| |
| int
In file included from creat1.c:12:
/usr/include/pthread.h:207:33: note: expected ‘void *’ but argument is of type ‘int’
207 | extern void pthread_exit (void *__retval) __attribute__ ((__noreturn__));
| ~~~~~~^~~~~~~~
creat1.c: In function ‘main’:
creat1.c:44:23: warning: passing argument 2 of ‘pthread_join’ from incompatible pointer type [-Wincompatible-pointer-types]
44 | pthread_join(tid, &retval); //类似进程的wati()
| ^~~~~~~
| |
| int *
In file included from creat1.c:12:
/usr/include/pthread.h:215:49: note: expected ‘void **’ but argument is of type ‘int *’
215 | extern int pthread_join (pthread_t __th, void **__thread_return);
| ~~~~~~~^~~~~~~~~~~~~~~

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [18:40:25]
$ ./creat1
begin
main
func 2
end 3

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [18:40:27]
$

栈清理

1
2
3
4
5
6
//挂钩子函数 routine
//arg指定传给routine 的参数
void pthread_cleanup_push(void (*routine)(void *), void *arg);

//根据 execute 值为真/假判断是否调用钩子函数
void pthread_cleanup_pop(int execute);

类似atexit()挂钩子函数。用钩子函数对线程进行清理,钩子函数被调用顺序跟注册时相反。

这两个是带参的宏而不是函数,所以必须成对使用,而且必须先使用 pthread_cleanup_push 再使用 pthread_cleanup_pop,否则会报语法错误,括号不匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* Filename: cleanup.c
* No.84.线程-线程终止和栈清理
* $ ps axm
* $ ps ax -L
* Description:
* $ gcc -Wall cleanup.c -o cleanup -lpthread
* Last modified: humble 20200411 17:32
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

//pthread_cleanup_push 和 pthread_cleanup_pop 需要成对使用,位置无所谓
//demo 不为0或1,则编译报错
#define demo (0) //1

static void func_cleanup(void *p)
{
puts(p);
return;
}

static void *func(void *p)
{
puts("pthread func is working");
pthread_cleanup_push(func_cleanup, "cleanup1");
pthread_cleanup_push(func_cleanup, "cleanup2");
pthread_cleanup_push(func_cleanup, "cleanup3");

puts("push over");

#if (demo == 0)
pthread_cleanup_pop(1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
#endif

pthread_exit(NULL);

#if (demo == 1)
//如果 pthread_cleanup_pop 放在pthread_exit后面,传参execute永远为真
pthread_cleanup_pop(1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
#endif
}

int main(int argc, char **argv)
{
pthread_t tid;

puts("begin");
int err = pthread_create(&tid, NULL, func, NULL);
if(err){
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
pthread_join(tid, NULL);
puts("end");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [19:07:58]
$ gcc -Wall cleanup.c -o cleanup -lpthread #demo0

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [19:08:00]
$ ./cleanup
begin
pthread func is working
push over
cleanup3
end

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [19:08:04]
$ vi cleanup.c

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [19:08:13]
$ gcc -Wall cleanup.c -o cleanup -lpthread #demo1

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [19:08:24]
$ ./cleanup
begin
pthread func is working
push over
cleanup3
cleanup2
cleanup1
end

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [19:08:26]
$

pthread_cancel

1
2
3
//取消同一进程中的线程thread
//成功返回0,失败返回errno
int pthread_cancel(pthread_t thread);

当一个线程没有必要继续执行下去时,又没法直接为它收尸,就需要先pthread_cancel取消这个线程,再pthread_join收尸。

如:多线程遍历一个很大的二叉树查找一个数据时,某一个线程找到了数据,那其它线程就没有必要继续执行了,可以取消它们了。

注意pthread_cancel()并不等待线程终止,它仅仅提出请求。 而目标线程收到这个请求也不会立即终止,要执行到取消点才能被取消。

取消有2种状态:

  • 允许
    • 异步cancel
    • 推迟cancel(默认) 推迟至cancel点再取消
  • 不允许。

POSIX 定义的cancel点,都是可能引发阻塞的系统调用(比如open(),read(),write()..)

1
2
3
4
5
6
7
8
9
10
11
12
13
fd1 = open();
// if err

//fd1刚打开,就收到一个pthread_cancel(但这里不是cancel点,所以不会响应)
//'可能引发阻塞的系统调用'才是POSIX定义的cancel点,要执行到下一个cancel点才会响应
//所以,在下一个cancel点之前可以挂钩子函数去关闭/释放资源
pthread_cleanup_push(close(fd1));

fd2 = open();
// if err

pthread_cleanup_push(close(fd2));
...

pthread_setcanceltype

1
2
3
4
//设置是否允许被取消
int pthread_setcancelstate(int state, int *oldstate);
//设置取消方式(异步cancel还是推迟cancel)
int pthread_setcanceltype(int type, int *oldtype);

pthread_testcancel

1
2
//什么都不做,充当一个cancel点
void pthread_testcancel(void);

pthread_detach

1
2
//分离一个线程
int pthread_detach(pthread_t thread);

习惯上谁创建线程谁pthread_join收尸。当分离了thread线程,那就不能为其收尸。

例0:多线程筛质数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* Filename: primer0.c
* No.86.线程-线程竞争实例
* Description:
* 多线程去计算某一范围内的质数
* $ gcc -Wall primer0.c -o primer0 -lpthread
* Last modified: humble 2020-09-01 21:04:58
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#define LEFT (30000000)
#define RIGHT (30000200)
#define THRNUM (RIGHT - LEFT +1)

static void *thr_primer(void *p);

int main(int argc, char **argv)
{
int i,err;
pthread_t tid[THRNUM];

for(i = LEFT; i <= RIGHT; i++){
err = pthread_create(tid + (i - LEFT), NULL, thr_primer, (void *)i); //如果把i的地址则会有竞争
if(err){
//发生错误就收尸已经创建成功的线程
//while(--i < 0){ pthread_join(tid[i - LEFT], NULL); } //未测
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}

for(i = LEFT; i <= RIGHT; i++){ //收尸
pthread_join(tid[i - LEFT], NULL);
}

exit(0);
}

static void *thr_primer(void *p)
{
int i, j, mark;
i = (int)p;
mark = 1;
for(j = 2; j < i/2; j++) {
if(i % j == 0){
mark = 0;
break;
}
}

if(mark == 1){
printf("%d is a primer\n", i);
}

pthread_exit(NULL);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [21:08:38]
$ gcc -Wall primer0.c -o primer0 -lpthread
primer0.c: In function ‘main’:
primer0.c:25:66: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
25 | err = pthread_create(tid + (i - LEFT), NULL, thr_primer, (void *)i); //如果把i的地址则会有竞争
| ^
primer0.c: In function ‘thr_primer’:
primer0.c:45:9: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
45 | i = (int)p;
| ^

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [21:08:40]
$ ./primer0
30000059 is a primer
30000071 is a primer
30000083 is a primer
30000023 is a primer
30000137 is a primer
30000001 is a primer
30000041 is a primer
30000199 is a primer
30000193 is a primer
30000037 is a primer
30000149 is a primer
30000049 is a primer
30000109 is a primer
30000169 is a primer
30000163 is a primer
30000167 is a primer
30000133 is a primer
30000079 is a primer

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [21:09:12]
$

例e:多线程筛质数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* Filename: primer0_e.c
* No.87.线程-线程竞争实例2
* Description:
* 多线程去计算某一范围内的质数
* $ gcc -Wall primer0_e.c -o primer0_e -lpthread
* Last modified: humble 2020-09-01 21:14:22
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#define LEFT (30000000)
#define RIGHT (30000200)
#define THRNUM (RIGHT - LEFT +1)

typedef struct{
int n;
}thrarg_t;

static void *thr_primer(void *p);

int main(int argc, char **argv)
{
int i,err;
pthread_t tid[THRNUM];
thrarg_t *p = NULL;
void *ptr;

for(i = LEFT; i <= RIGHT; i++)
{
p = malloc(sizeof(thrarg_t));
if(!p){
perror("malloc()");
exit(1);
}
p->n = i;

err = pthread_create(tid + (i - LEFT), NULL, thr_primer, p);
if(err){
//while(--i < 0){ pthread_join(tid[i - LEFT], NULL);free(.ptr.); } //未测
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}

for(i = LEFT; i <= RIGHT; i++){
pthread_join(tid[i - LEFT], &ptr);
free(ptr);
}
exit(0);
}

static void *thr_primer(void *p)
{
int i, j, mark;
i = ((thrarg_t *)p)->n;
//free(p);
mark = 1;
for(j = 2; j < i/2; j++){
if(i % j == 0){
mark = 0;
break;
}
}

if(mark == 1){
printf("%d is a primer\n", i);
}
pthread_exit(p); //把p返回给pthread_join
}

运行结果同上。例子创建了200个线程,但能进程空间允许创建的线程数量有限(可能PID耗尽,也可能栈空间耗尽)。每个线程消耗一个PID,并且消耗一份栈空间ulimit -s(32位进程栈最多创建约300个线程),使用线程属性修改线程栈大小可提高最大线程创建量。使用ulimit -a命令可以查看所有资源上限。

3.线程同步

互斥量(pthead_mutex_t)

可以使各个线程实现互斥的效果。由它来保护临界区每次只能由一个线程进入。当一个线程想要进入临界区之前需要先抢锁(加锁),如果能抢到锁就进入临界区工作,并且要在离开的时候解锁以便让其它线程可以抢到锁进入临界区;如果没有抢到锁则进入阻塞状态等待锁被释放然后再抢锁。

要在进入临界区之前加锁,在退出临界区的时候解锁。临界区是每个线程要单独执行的,所以临界区中的代码执行时间越短越好。互斥量限制的是一段代码能否执行,而不是一个变量或一个资源。

防止死锁:在临界区内跳转到临界区外时记得先解锁。

pthread_mutex_xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//静态初始化,定义mutex时用宏初始化,使用默认属性
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//动态初始化,先定义未被初始化的mutex,再用 pthread_mutex_init 函数对mutex初始化,并指定属性mutexattr。返回0(永远不出错)
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

//销毁mutex锁,成功返回0,失败返回errno
int pthread_mutex_destroy(pthread_mutex_t *mutex);

//阻塞死等抢锁,成功返回0,失败返回errno
int pthread_mutex_lock(pthread_mutex_t *mutex);

//非死等抢锁,成功返回0,失败返回errno
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//解锁,成功返回0,失败返回errno
int pthread_mutex_unlock(pthread_mutex_t *mutex);

例:多线程读写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* Filename: add.c
* No.88.线程-竞争故障
* Description: 线程冲突(线程互斥量)
* $ gcc -Wall add.c -o add -lpthread
* $ echo 1 > /tmp/out
* $ ./add && cat /tmp/out
* Last modified: humble 2020-09-02 09:03:58
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

#define THRNUM (20)
#define LINEBUFSIZE (1024)
#define FNAME "/tmp/out"

//demo0 不带互斥量,同时读写资源发生冲突
//demo1 使用互斥量,争抢资源,非同时读写
#define demo (1) //0,1

#if (demo == 1)
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
#endif

static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINEBUFSIZE];

fp = fopen(FNAME, "r+");
if(!fp){
perror("fopen()");
exit(1);
}

#if (demo == 1)
pthread_mutex_lock(&mut);
#endif

fgets(linebuf, LINEBUFSIZE, fp);
fseek(fp, 0, SEEK_SET);
#if (demo == 0)
sleep(1); //扩大竞争时间域,等全部线程都读取完毕,再往下操作
#endif
fprintf(fp, "%d\n", atoi(linebuf) + 1);
fclose(fp); //刷新缓冲区,不能放到unlock后面

#if (demo == 1)
pthread_mutex_unlock(&mut);
#endif

pthread_exit(NULL);
}

int main(int argc, char **argv)
{
int i, err;
pthread_t tid[THRNUM];

for(i = 0; i < THRNUM; i++){
err = pthread_create(tid + i, NULL, thr_add, NULL);
if(err){
//while(--i < 0){ pthread_join(tid[i - LEFT], NULL); } //未测
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}

for(i = 0; i < THRNUM; i++){
pthread_join(tid[i], NULL);
}

#if (demo == 1)
pthread_mutex_destroy(&mut); //对应 pthread_mutex_init()
#endif
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:46:35]
$ gcc -Wall add.c -o add -lpthread #demo0

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:46:40]
$ echo 1 > /tmp/out

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:46:42]
$ ./add

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:46:46]
$ cat /tmp/out
2

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:46:48]
$ vi add.c

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:47:07]
$ gcc -Wall add.c -o add -lpthread #demo1

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:47:10]
$ echo 1 > /tmp/out

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:47:12]
$ ./add

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:47:14]
$ cat /tmp/out
21

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [9:47:16]
$

例:4个线程分别打印abcd,且按顺序,持续5秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* Filename: abcd.c
* No.89.线程-互斥量
* Description:4个线程分别打印abcd,且按顺序,持续5秒
* $ gcc -Wall abcd.c -o abcd -lpthread
* Last modified: humble 2020-09-02 10:06:45
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

#define THRNUM (4)

static pthread_mutex_t mut[THRNUM];

static int next(int n)
{
if(n + 1 == THRNUM){
return 0;
}
return n + 1;
}


static void *thr_abcd(void *p)
{
int n = (int)p;
int c = 'a' + (int)p;

while(1){
pthread_mutex_lock(mut + n); //死等着抢属于自己的锁
write(1, &c, 1);
pthread_mutex_unlock(mut + next(n)); //放开下一个人的锁
}
pthread_exit(NULL);
}

int main(int argc, char **argv)
{
int i, err;
pthread_t tid[THRNUM];

for(i = 0; i < THRNUM; i++)
{
pthread_mutex_init(mut + i, NULL);
pthread_mutex_lock(mut + i); //初始化完,立刻抢锁
err = pthread_create(tid + i, NULL, thr_abcd, (void *)i);
if(err){
//while(--i < 0){ pthread_join(tid[i - LEFT], NULL); } //未测
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
//线程创建完毕时全部锁都抢到了

pthread_mutex_unlock(mut + 0); //放开a的锁
alarm(5); //5秒钟后进程被信号杀死

for(i = 0; i < THRNUM; i++)
{
pthread_join(tid[i], NULL);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [11:20:10] C:142
$ gcc -Wall abcd.c -o abcd -lpthread
abcd.c: In function ‘thr_abcd’:
abcd.c:29:13: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
29 | int n = (int)p;
| ^
abcd.c:30:19: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
30 | int c = 'a' + (int)p;
| ^
abcd.c: In function ‘main’:
abcd.c:49:55: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
49 | err = pthread_create(tid + i, NULL, thr_abcd, (void *)i);
| ^

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [11:21:54]
$ ./abcd
abcdabcdabcdabcdabcdabcdabcda...
[1] 20197 alarm ./abcd

让出cpu

1
2
3
//让出当前线程所占用的调度器给其它线程使用,而不必等待时间片耗尽才切换调度器
//成功返回0,失败返回-1并设置errno
int sched_yield(void);

可以把它理解成一个很短暂的 sleep() 。一般用于在使用一个资源时需要同时获得多把锁但是却没法一次性获得全部的锁的场景下,只要有任何一把锁没有抢到,那么就立即释放已抢到的锁,并让出自己的调度器让其它线程有机会获得被自己释放的锁。当再次调度到自己时再重新抢锁,直到能一次性抢到所有的锁时再进入临界区,这样就避免了出现死锁的情况。

例:N个线程去计算某一范围内的质数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/* Filename: primer0_pool.c
* No.90.线程-线程池实现
* Description: N个线程去计算某一范围内的质数
* 查询法/忙等版,main和多个thr_primer一起抢锁,会出现main长时间抢不到锁的情况
* $ gcc -Wall primer0_pool.c -o primer0_pool -lpthread
* Last modified: humble 2020-09-02 15:18:52
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sched.h>

#define LEFT (30000000)
#define RIGHT (30000200)
#define THRNUM (4)

static int num = 0; //猪圈
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER; //猪圈锁

static void *thr_primer(void *p);

int main(int argc, char **argv)
{
int i,err;
pthread_t tid[THRNUM];

//创建线程(创建4头猪)
for(i = 0; i <= THRNUM; i++){
err = pthread_create(tid + i, NULL, thr_primer, (void *)i);
if(err){
//while(--i < 0){ pthread_join(tid[i], NULL); } //未测
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}

//main把数字投放到num里面(主人把猪食扔到猪圈里)
for(i = LEFT; i <= RIGHT; i++){
pthread_mutex_lock(&mut_num); //抢猪圈的锁
while(num != 0){
//上一份猪食居然还没被猪吃掉
//解锁猪圈,好让猪能抢锁进猪圈吃掉
pthread_mutex_unlock(&mut_num);
//因为有可能某个thr_primer(猪)抢到也可能又被main(主人)抢到
//所以操作调度器,让出cpu(可理解为段时间sleep),避免接下来又是main(主人)抢到锁
sched_yield();
pthread_mutex_lock(&mut_num);
}
num = i; //上一份猪食被猪吃掉了,投放猪食
pthread_mutex_unlock(&mut_num); //解锁猪圈,好让猪能抢锁进猪圈吃掉
}
//到此,猪食已经投完了

//等猪取走猪食后,往猪圈投放一份猪粪(-1),让猪别再抢锁进猪圈
pthread_mutex_lock(&mut_num);
while(num != 0){
//上一份猪食居然还没被猪吃掉
//解锁猪圈,好让猪能抢锁进猪圈吃掉
pthread_mutex_unlock(&mut_num);
//因为有可能别的thr_primer(猪)抢到也可能又被自己抢到
//所以操作调度器,让出cpu(可理解为段时间sleep),避免接下来又是main(主人)抢到锁
sched_yield();
pthread_mutex_lock(&mut_num);
}
num = -1; //猪食被猪吃光了,投放猪粪(-1)
pthread_mutex_unlock(&mut_num); //解锁猪圈,好让猪能抢锁进猪圈来发现猪粪(-1)而结束

for(i = 0; i <= THRNUM; i++)
{
pthread_join(tid[i], NULL);
}

pthread_mutex_destroy(&mut_num);
exit(0);
}

static void *thr_primer(void *p)
{
int i, j, mark;

while(1){
pthread_mutex_lock(&mut_num); //猪抢到锁
while(num == 0){
//发现猪圈是空的
//解锁给main(主人)去投放猪食(有可能被其他thr_primer(猪)抢到)
pthread_mutex_unlock(&mut_num);
sched_yield(); //让出cpu,避免接下来又是自己抢到锁
pthread_mutex_lock(&mut_num);
}

//猪圈终于有东西了
if(num == -1){
//居然是猪粪,释放锁
pthread_mutex_unlock(&mut_num);
break; //准备自杀
}

//是猪食,取走,释放锁给主人或其他猪去抢
i = num;
num = 0;
pthread_mutex_unlock(&mut_num);

//开吃
mark = 1;
for(j = 2; j < i/2; j++)
{
if(i % j == 0){
mark = 0;
break;
}
}

if(mark == 1){
printf("[%d] %d is a primer\n", (int)p, i);
}
}

pthread_exit(NULL);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [15:10:33]
$ gcc -Wall primer0_pool.c -o primer0_pool -lpthread
primer0_pool.c: In function ‘main’:
primer0_pool.c:30:57: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
30 | err = pthread_create(tid + i, NULL, thr_primer, (void *)i);
| ^
primer0_pool.c: In function ‘thr_primer’:
primer0_pool.c:115:45: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
115 | printf("[%d] %d is a primer\n", (int)p, i);
| ^

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [15:17:54]
$ ./primer0_pool
[0] 30000001 is a primer
[1] 30000023 is a primer
[3] 30000041 is a primer
[2] 30000037 is a primer
[4] 30000049 is a primer
[0] 30000059 is a primer
[4] 30000083 is a primer
[3] 30000071 is a primer
[2] 30000109 is a primer
[1] 30000079 is a primer
[4] 30000137 is a primer
[3] 30000149 is a primer
[0] 30000133 is a primer
[4] 30000169 is a primer
[3] 30000193 is a primer
[2] 30000163 is a primer
[1] 30000167 is a primer
[0] 30000199 is a primer

# li @ evpower in ~/humble/tmp/lhq/parallel/thread/posix on git:master x [15:17:56]
$

pthread_once

1
2
3
4
5
6
7
//定义一个 控制执行一次 的变量
pthread_once_t once_control = PTHREAD_ONCE_INIT;

//使用 once_control 的变量判断对init_routine()进行调用
//动态单次初始化,它能保证 init_routine 函数仅被调用一次
//返回0永远不出错
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));

例:查询法令牌桶

把令牌桶写成库的方式提供给它人调用,需要解决并发调用的资源竞争问题。另外实现只在第一次使用时才加载模块module_load(),类似C++的构造函数。

在临界区需要调用一个函数,但是从程序的布局来看该函数内无法通过加锁来避免并发冲突/函数重入。根据 POSIX 标准的约定,这种函数的命名规则是必须以 _unlocked 作为后缀,所以大家在看到这样的函数时在调用之前一定要先加锁。例如下面代码中的 get_free_pos_unlocked()

查看代码

1
2
./parallel/thread/posix/mytbf_mt/
#define demo (mutex_only)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix/mytbf_mt on git:master x [19:17:09] C:2
$ tree
.
├── main.c
├── Makefile
├── mytbf.c
└── mytbf.h

0 directories, 4 files

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix/mytbf_mt on git:master x [19:17:10]
$ make
cc -Wall -c -o main.o main.c
cc -Wall -c -o mytbf.o mytbf.c
gcc -Wall main.o mytbf.o -o mytbf -lpthread

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix/mytbf_mt on git:master x [19:17:16]
$ ./mytbf /etc/services
# Network services, Internet style
#
# Note that i^C

# mi @ evpower in ~/humble/tmp/lhq/parallel/thread/posix/mytbf_mt on git:master x [19:17:29] C:130
$

上面的程序经过测试,发现 CPU 正在满负荷工作,说明程序中出现了忙等。就是 mytbf_fetchtoken() 函数获得锁的时候采用了忙等的方式。异步程序有两种处理方式,一种是通知法,一种是查询法。我们这里用的就是查询法,下面改成通知法实现。

条件变量(pthead_cond_t)

让线程以无竞争的形式等待某个条件的发生,当条件发生时通知等待的线程醒来去做某件事。另一种方式是把所有等待同一个条件的线程都唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//静态初始化,定义cond时用宏初始化,使用默认属性
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

//动态初始化,先定义未被初始化的cond,再用 pthread_cond_init 函数对cond初始化,并指定属性cond_attr
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

//函数用于唤醒当前多个等待的线程中的任何一个
int pthread_cond_signal(pthread_cond_t *cond);

//惊群,将现在正在等待的线程全部唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);

//在临界区外阻塞等待某一个条件发生变化,(死等)直到有一个通知到来打断它的等待,被唤醒后开始抢mutex锁
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

//增加了超时功能的等待,(尝试等)超时之后无论能否拿到锁都返回
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec
*abstime);

//销毁cond锁
int pthread_cond_destroy(pthread_cond_t *cond);

//全部函数成功返回0,失败返回errno

把查询法(忙等)修改为通知法(非忙等)仅仅加一个条件变量(pthread_cond_t) 就行了。

例:通知法实现令牌桶

查看代码

1
2
./parallel/thread/posix/mytbf_mt/
#define demo (with_cond)

通常cond_wait()等待会放在一个循环中,就像上面的令牌桶栗子一样,因为可能有多个线程都在等待条件满足,当前的线程’等待条件’被唤醒时不代表’执行条件’一定满足。可能先被唤醒的线程发现条件满足已经去工作了,等轮到当前线程调度的时候条件可能就又不满足了,所以如果条件不满足又通过while继续进入等待。

用 pthread_cond_signal(3) 还是用 pthread_cond_broadcast(3) 呢?

根据具体场景来选择。一般只有一个线程在等待或者明确知道哪个线程应该被唤醒的时候使用 _signal() 函数,如果有多个线程在等待并且不确定应该由谁起来工作的时候使用惊群。不确定是指业务上不能确定哪个线程应该工作,而不是你作为程序猿稀里糊涂的不知道哪个线程该工作。程序猿应该保证了解你的每一行代码在做什么,而不要写出一坨自己都不知道它在做什么的代码。

4.线程属性&同步属性

5.重入&线程与信号&线程与fork

apue_78信号sigsuspend

apue_78信号sigsuspend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/* Filename: susp.c
* No.78.信号sigsuspend
* Description:
* 1.屏蔽信号
* 2.打印5个*换行后
* 3.取消信号屏蔽并且等信号
* 4.收到信号后回到1
* Last modified: humble 20200404 17:12
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

#define _SAVESET_ (1)

#define demo (2) //0,1,2

static void usage(void);

void int_handler(int i)
{
write(1, "!", 1);
}

int main(int argc, char **argv)
{
int i;
sigset_t set, oset, saveset;

if(argc != 1){
fprintf(stderr, "argc !=1");
usage();
exit(1);
}

signal(SIGINT, int_handler);
sigemptyset(&set);
sigaddset(&set, SIGINT);

#if _SAVESET_
//此句目的在于保存当前mask到saveset,方便以后恢复,用SIG_UNBLOCK或SIG_BLOCK都无所谓
sigprocmask(SIG_UNBLOCK, &set, &saveset);
#endif

#if (demo == 1) || (demo == 2)
sigprocmask(SIG_BLOCK, &set, &oset); //block住
#endif

while(1){
#if (demo == 0)
sigprocmask(SIG_BLOCK, &set, &oset);
#endif

for(i = 0; i < 5; i++)
{
write(1, "*", 1);
sleep(1);
}
write(1, "\n", 1);

#if (demo == 0)
sigprocmask(SIG_SETMASK, &oset, NULL);
//因为sigprocmask和pause两个函数不原子
//如果在sigprocmask和pause之间收到信号,那响应完信号之后进入了pause
//可是我们期望它响应信号后能继续打印*
pause();
#elif (demo == 1)
sigset_t tmpset;
sigprocmask(SIG_SETMASK, &oset, &tmpset); //恢复unblock,并用tmpset记录block集(set和tmpset相同)
//sigprocmask和pause两个函数不原子,结果跟demo0一样
pause();
sigprocmask(SIG_SETMASK, &tmpset, NULL); //再次block
#elif (demo == 2)
//sigsuspend 就相当于上面demo1的原子操作
sigsuspend(&oset);//原子化 unblock后立刻进入pause(wait)
#endif
}

#if _SAVESET_
//把mask设置为saveset(相当于恢复了之前的状态)
sigprocmask(SIG_SETMASK, &saveset, NULL);
#endif

return 0;
}

static void usage(void)
{
#define USAGE "Usage:\n\
Helloworld\n"
printf(USAGE);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [10:39:27]
$ make susp #demo1
cc susp.c -o susp

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [10:39:31]
$ ./susp
*****^C
!^C!*****
^\[1] 35672 quit (core dumped) ./susp

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [10:40:23] C:131
$ vi susp.c

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [10:40:23] C:131
$ make susp #demo2
cc susp.c -o susp

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [10:40:43]
$ ./susp
*****^C
!****^C^C^C^C*^C^C^C^C^C^C
!*^C****
!**^\[1] 35836 quit (core dumped) ./susp

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [10:41:11] C:131
$

apue_76信号集

apue_76信号集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/* Filename: block.c
* No.76.信号集
* Description:
* 每秒打印一个*,打印5个*就换行。中途收到1个或多个SIGINT(CTRL+C)信号暂不处理,等第5个*后只处理1个
* Last modified: humble 20200404 10:41
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

//二选一
//#define demo0 (1)
#define demo1 (1)

#if demo0
#define _SAVESET_ (1)
#else
#define _SAVESET_ (0)
#endif

static void usage(void);

void int_handler(int i)
{
write(1, "!", 1);
}

int main(int argc, char **argv)
{
int i;
sigset_t set, oset, saveset;

if(argc != 1){
fprintf(stderr, "argc !=1");
usage();
exit(1);
}

signal(SIGINT, int_handler);
sigemptyset(&set);
sigaddset(&set, SIGINT);

#if _SAVESET_
//此句目的在于保存当前mask到saveset,方便以后恢复,用SIG_UNBLOCK或SIG_BLOCK都无所谓
sigprocmask(SIG_UNBLOCK, &set, &saveset);
#endif

while(1){
//1.把当前的mask保存到oset内(方便以后恢复),如果为NULL表示不保存
//2.把set指定的信号阻塞住(即把mask的对应bit清零)
//那么收到set指定的信号(padding对应bit被置一),也不会被处理(因为 mask&padding 中信号对应bit不为1)
sigprocmask(SIG_BLOCK, &set, &oset);

for(i = 0; i < 5; i++)
{
write(1, "*", 1);
sleep(1);
}
write(1, "\n", 1);

#if demo0
//把set指定的信号取消阻塞住(即把mask的对应bit置一)
//那么收到set指定的信号(padding对应bit被置一),就会被处理(因为 mask&padding 中信号对应bit为1)
//问题:假如原本set指定的一或多个信号一开始就是BLOCK状态,那执行完下面这句它(们)就变为了UNBLOCK。
//应该改为:原本是什么状态,最后又恢复到什么状态。定义_SAVESET_为1即可
sigprocmask(SIG_UNBLOCK, &set, NULL);
#elif demo1
//1.把当前的mask保存到NULL内,也就是不保存
//2.把mask设置为oset(相当于恢复了之前的状态)
//那么收到oset指定的信号(padding对应bit被置一),就会被处理(因为 mask&padding 中信号对应bit为1)
//另外,用oset好处是:原本是什么状态,最后又恢复到什么状态。则_SAVESET_不需要定义为1
sigprocmask(SIG_SETMASK, &oset, NULL);
#endif
}

#if _SAVESET_
//把mask设置为saveset(相当于恢复了之前的状态)
sigprocmask(SIG_SETMASK, &saveset, NULL);
#endif

return 0;
}

static void usage(void)
{
#define USAGE "Usage:\n\
Helloworld\n"
printf(USAGE);
}
1
2
3
4
5
6
7
8
9
10
11
12
# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master o [9:21:57]
$ make block
cc block.c -o block

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [9:22:02]
$ ./block
***^C**
!*****
**^\[1] 21111 quit (core dumped) ./block

# li @ evpower in ~/humble/tmp/lhq/parallel/signal on git:master x [9:22:47] C:131
$

ubuntu安装后需要做的事情

安装ubuntu后做的事情

删除不必要的软件

1
sudo apt-get remove libreoffice-common unity-webapps-common thunderbird totem rhythmbox empathy brasero simple-scan gnome-mahjongg aisleriot gnome-mines cheese transmission-common gnome-orca webbrowser-app gnome-sudoku landscape-client-ui-install onboard deja-dup

修改软件源为aliyun

1
2
3
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
sudo sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
#sudo sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list

自定义DHCP网络的DNS Server IP地址

sudo vi /etc/dhcp/dhclient.conf 文件,在第21行#prepend domain-name-servers 127.0.0.1;下一行添加如下2行使用aliyun和114的DNS

1
2
prepend domain-name-servers 114.114.114.114;
prepend domain-name-servers 223.5.5.5;

这样可以优先使用aliyun的dns,次要使用114的DNS。

~/up.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

apt-get update
apt-get upgrade
apt-get dist-upgrade

apt-get autoremove

#方法一、如果你知道要删除软件的具体名称,可以使用
#sudo apt-get remove --purge 软件名称
#sudo apt-get autoremove --purge 软件名称

#方法二、如果不知道要删除软件的具体名称,可以使用
#dpkg --get-selections | grep '软件相关名称'
#sudo apt-get purge 一个带core的package,如果没有带core的package,则是情况而定。

#清理残留数据
#dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P

#apt-get的卸载相关的命令有remove/purge/autoremove/clean/autoclean等。具体来说:

#apt-get purge / apt-get --purge remove
#删除已安装包(不保留配置文件)。
#如软件包a,依赖软件包b,则执行该命令会删除a,而且不保留配置文件

#apt-get autoremove
#删除为了满足依赖而安装的,但现在不再需要的软件包(包括已安装包),保留配置文件。

#apt-get remove
#删除已安装的软件包(保留配置文件),不会删除依赖软件包,且保留配置文件。

apt-get autoclean
#APT的底层包是dpkg, 而dpkg 安装Package时, 会将 *.deb 放在 /var/cache/apt/archives/中,apt-get autoclean 只会删除 /var/cache/apt/archives/ 已经过期的deb。

#apt-get clean
#使用 apt-get clean 会将 /var/cache/apt/archives/ 的 所有 deb 删掉,可以理解为 rm /var/cache/apt/archives/*.deb。

######################
#那么如何彻底卸载软件呢?
# 删除软件及其配置文件
#apt-get --purge remove <package>
# 删除没用的依赖包
#apt-get autoremove <package>
# 此时dpkg的列表中有“rc”状态的软件包,可以执行如下命令做最后清理:
#dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P
#当然如果要删除暂存的软件安装包,也可以再使用clean命令。
######################

交换Caps_Lock和Escape键

方法1:

1
dconf write /org/gnome/desktop/input-sources/xkb-options "['caps:escape']"

方法2:

1
gsettings set org.gnome.desktop.input-sources xkb-options "['caps:escape']"

方法3:
ubuntu下交换Caps_Lock和Escape键的方法

1
2
sudo apt-get install dconf-tools
dconf-editor

org >> gnome >> desktop >> input-sources 修改xkb-options为['caps:swapescape']

安装需要的软件

1
sudo apt-get install vpnc git axel openssh-server exfat-fuse unrar unace rar zip unzip p7zip-full silversearcher-ag ctags exuberant-ctags tmux autojump curl jq sshpass net-tools scrot cloc fzf
1
sudo apt-get install g++ libncurses5-dev zlib1g-dev bison flex unzip autoconf gawk make gettext gcc binutils patch bzip2 libz-dev asciidoc subversion libssl-dev

安装typora

安装shutter

1
2
3
sudo add-apt-repository ppa:linuxuprising/shutter
sudo apt-get update
sudo apt-get install shutter

安装albert

1
2
sudo dpkg -i albert_0.16.1_amd64.deb
sudo apt-get install -f

安装vim8

1
2
vim --version #如果已经是vim8那就跳#如果已经是vim8那就跳过此步
sudo apt-get install vim

安装neovim

1
sudo apt-get install neovim

16.04安装neovim

1
2
3
4
5
# sudo add-apt-repository ppa:neovim-ppa/stable
sudo add-apt-repository ppa:neovim-ppa/unstable
sudo apt-get update
sudo apt-get install -y neovim
# pip3 install neovim

设置默认编辑器

1
2
3
4
5
6
sudo update-alternatives --install /usr/bin/vi vi /usr/bin/nvim 60
sudo update-alternatives --config vi
sudo update-alternatives --install /usr/bin/vim vim /usr/bin/nvim 60
sudo update-alternatives --config vim
sudo update-alternatives --install /usr/bin/editor editor /usr/bin/nvim 60
sudo update-alternatives --config editor

选装

安装WPS

先到wps官网上下载wps的deb包。http://www.wps.cn/product/

1
sudo dpkg -i wps-office_11.1.0.8722_amd64.deb

解决字体冲突

下载symbol-fonts.deb点击去下载并安装

1
sudo dpkg -i symbol-fonts_1.1_all.deb

火狐安装插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Tampermonkey(有些脚本在Greasemonkey无法运行)
AC-baidu:重定向优化百度搜狗谷歌必应搜索_favicon_双列
【玩的嗨】VIP工具箱,全网VIP视频免费破解去广告,一站式音乐搜索下载,获取B站封面,下载B站视频,上学吧答案获取等众多功能聚合
百度网盘直链满速下载助手
网盘自动填写访问码【威力加强版】
豆瓣资源下载大师:1秒搞定豆瓣电影|音乐|图书下载
持续更新 CSDN广告完全过滤、人性化脚本优化:不用再登录了!让你体验令人惊喜的崭新CSDN
购物党实时比价工具(浏览器57以上版本安装)
Adblock Plus - 免费的广告拦截器
Vedio Speed Controller
Vimium C 全键盘操作浏览器
划词翻译 有道
京价保 - 京东价保助手
Video Downloader Pro 免费视频下载器
网页截图
Gesturefy 鼠标手势操作
markdown-clipper 网页保存为markdown

vim/neovim配置.vimrc与插件安装

安装经典菜单指示器

1
2
3
sudo add-apt-repository ppa:diesch/testing
sudo apt-get update
sudo apt-get install classicmenu-indicator

安装系统指示器SysPeek

1
2
3
sudo add-apt-repository ppa:nilarimogard/webupd8
sudo apt-get update
sudo apt-get install syspeek

安装网页flashplayer

1
sudo apt-get install flashplugin-installer

系统设置——外观——行为——开启工作区 & 添加“显示桌面”图标到启动器

代理

1
2
3
4
5
6
7
8
9
10
11
12
13
#sudo apt-get install software-properties-common -y
#sudo add-apt-repository ppa:max-c-lv/shadowsocks-libev -y
#sudo apt-get update
sudo apt install shadowsocks-libev
sudo vi /etc/shadowsocks-libev/v2ray.conf

sudo apt-get install proxychains
sudo vi /etc/proxychains.conf #改为 socks5 127.0.0.1 1080

oyvpn(){
pidof ss-local && { echo "oyvpn is aready runing"; return; }
nohup ss-local -c /etc/shadowsocks-libev/v2ray.conf &
}

终端效率

1
2
3
4
5
6
7
8
sudo apt-get install i3
sudo apt-get install pcmanfm
# 注销,选i3,重新登陆
cd $(blogdir)
mv ~/.config/i3/config ~/.config/i3/config.bak
ln -s $(pwd)/source/_posts/ubuntu安装后需要做的事情/config ~/.config/i3/config
sudo mv /etc/i3status.conf /etc/i3status.conf.bak
sudo ln -s $(pwd)/source/_posts/ubuntu安装后需要做的事情/i3status.conf /etc/i3status.conf

on-my-zsh

1
2
3
4
5
6
sudo apt-get install zsh git
# chsh -s /bin/zsh
sh -c "$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)"
cd $(blogdir)
mv ~/.zshrc ~/.zshrc.bak
ln -s $(pwd)/source/_posts/ubuntu安装后需要做的事情/zshrc ~/.zshrc

如果出现 Failed to connect to raw.githubusercontent.com port 443: Connection refused ,就在/etc/hosts文件追加

1
199.232.28.133 raw.githubusercontent.com

zsh-syntax-highlighting

1
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

zsh-autosuggestions

1
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

inkscape: 编辑图片画框画箭头等等

the_silver_searcher: 快速的代码命令行搜索工具 ag

tmux: 终端分屏工作(目前少用)

tmuxp: 终端分屏工作(目前未用)

autojump: 快速跳转目录

cloc: 统计代码行数,可区分文件类型以及空行和注释

1
2
echo '. /usr/share/autojump/autojump.sh' >> ~/.bashrc
source ~/.bashrc

fzf: 终端命令行模糊查询历史

1
2
3
# 编译依赖go,请提前安装
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install

1
2
3
sudo apt-get install fzf
echo '[ -f ~/.fzf.bash ] && source ~/.fzf.bash' >> ~/.bashrc
source ~/.bashrc

tldr: TLDR 页的 GitHub 仓库将其描述为简化的、社区驱动的手册页集合。too long didn’t read

1
2
sudo npm install -g tldr
sudo pip3 install tldr

httpie: HTTPie 是一个 HTTP 的命令行客户端
~/.bashrc 文件末尾追加内容

1
2
3
4
5
6
7
alias jev3='cd ~/humble/ev345/ && ctags -R package/libevpower/ package/chrgrserv/ package/dashboard/'
alias jcs='cd ~/humble/ev345/package/chrgrserv/src/'
alias jlev='cd ~/humble/ev345/package/libevpower/src/'
alias jblog='cd ~/humble/blog/source/_posts/'
alias jtools='cd ~/humble/tools/ && . venv/bin/activate'
. /usr/share/autojump/autojump.sh
[ -f ~/.fzf.bash ] && source ~/.fzf.bash

安装go

16.04安装go 1.6版本 sudo apt install golang-go (太旧了)

安装新版本

1
2
3
4
5
6
sudo apt-get purge golang*  # 卸载旧版1.6
sudo rm -rf /usr/lib/go-1.6/ /usr/lib/go-1.6/src/ /usr/lib/go-1.6/src/runtime/ /usr/lib/go-1.6/src/runtime/race
curl -O https://dl.google.com/go/go1.17.1.linux-386.tar.gz
tar -C /usr/local -xzf go1.17.1.linux-386.tar.gz
# 给 /etc/environment 的PATH添加 “:/usr/local/go/bin" 后注销重新登陆
go env # 检测安装成功

使用国内代理安装go模块速度快(参考https://github.com/goproxy/goproxy.cn/blob/master/README.zh-CN.md)

1
2
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

python

when-changed: 监控文件变动并且执行命令

ipdb:pip3 install ipdb

删除启动项:sudo efibootmgr -b 0001 -B

ubuntu16.04和win时间不同步

先在ubuntu下更新一下时间,确保时间无误:

1
2
sudo apt-get install ntpdate
sudo ntpdate time.windows.com

然后将时间更新到硬件上:

1
sudo hwclock --localtime --systohc

重新进入windows10,发现时间恢复正常了!

ubuntu16.04搭建JAVA开发环境

安装openjdk8

1
2
sudo apt update
sudo apt install openjdk-8-jdk openjdk-8-jre

java -version确认安装成功

1
2
3
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-8u252-b09-1ubuntu1-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)

配置 JAVA_HOME 和 JRE_HOME 环境变量

1
2
3
4
cat >> /etc/environment <<EOL
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
JRE_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre
EOL

注销或重启系统,用javac -version确认javac 1.8.0_252环境配置成功

安装idea

1
2
3
4
5
6
sudo cp ~/下载/ideaIU-2020.1.tar.gz /usr/local/src
cd /usr/local/src
sudo tar -zxvf ideaIU-2020.1.tar.gz
sudo cp ~/下载/jetbrains-agent.jar /usr/local/src/idea-IU-201.6668.121
cd /usr/local/src/idea-IU-201.6668.121/bin
sudo ./idea.sh

Do not import settings - OK

Accept

Evaluate for free - Evaluate

选择UI主题 - Next

Create a destop entry for ... - For all users... - Next

Create a script for opening ... - Next

Default plugins不选 - Next

Featured plugins不选 - Next

如果上面的没出现,可以点Welcome窗口右下角Configure - Restore Default Settings

激活idea

启动idea,Help - Edit Custom VM Options...

追加一行-javaagent:/usr/local/src/idea-IU-201.6668.121/jetbrains-agent.jar

cat ~/下载/ACTIVATION_CODE.txt,拷贝该激活码

重启idea,Help - Register... - activation code - 粘贴激活码 - Activation

如果不行,尝试修改/usr/local/src/idea-IU-201.6668.121/整个目录的权限为755

操作系统

iNode介绍

cron

死锁

死锁必须同时满足4个条件(破坏任意一个即可):

  1. 互斥使用:资源被一个任务使用时,别的任务不能使用(一般不破坏这个条件)
  2. 不可抢占:请求者不能强制从占有者手中夺取资源(任务拿到资源1时却拿不到资源2就主动释放所有已占有的资源)
  3. 占有且等待:请求其他资源的同时保持对原有资源的占有(每个任务一次申请所有的资源,失败就阻塞等待)
  4. 循环等待:任务一等待任务二释放资源,二又等一释放(给所有资源设定序号,每个任务都从需要小的资源开始申请)

线程/协程/异步的编程模型

IO多路复用select/poll/epoll介绍

select:使用文件描述符

  1. 1024 bitmap
  2. FD_SET设置好的bitmap不可重用
  3. bitmap从用户态拷贝到内核态的开销
  4. 发生事件select返回后,需要O(n)遍历全部bitmap才能知道发生了哪些事情

poll:使用struct pollfd{fd,events,revents}

  1. 内核使用链表存储,不止1024(events和revents时short有65535个bit)
  2. 关心事件bitmap(events)和发生事件(revents)分开,使得bitmap可重用
  3. 同select
  4. 同select

epoll:使用struct epoll_event{data.fd,events}

  1. 内核使用红黑树存储,但没有revents
  2. epoll_ctl添加关心事件到内核态(可重用)
  3. 第2步已经拷贝到内核态,一次拷贝,终生受用,降低开销(视频有误:实际没有使用共享内存)
  4. poll_wait直接把发生事件回填到参数内不需要再遍历判断

float是如何在计算机中存储的

信号

用户态与内核态

用户态 申请外部资源(系统调用,中断,异常)后会陷入到内核态

操作系统的内存管理简介

nginx反向代理与负载均衡教程