多线程

  • 脱离了任务的线程是没有意义的

    • 但是不一定要去执行任务
  • 线程是通过Thread类来创建的

  • 任务是通过Runnable接口来实现的

  • 继承Thread类

  • 实现Runnable接口

    • 无返回值
  • 实现Callable接口

    • 有返回值

Thread

  • Thread构造器:无参构造就是不需要指定任务,有参构造可以直接指定线程的任务
1
public Thread(Runnable target)	

流程

  1. 创建线程对象,同时指定任务
  2. 启动线程,start后进入就绪状态,等待获取CPU资源
  3. 一旦拿到CPU资源,开始执行任务,调用Thread的run方法
1
2
3
4
5
public void run(){
if(target != null){
target.run();
}
}

示例

1
2
3
4
5
6
7
8
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++){
System.out.println(i);
}
}
}
1
2
3
4
5
6
7
public class TestApplication {

public static void main(String[] args) {
new MyThread().start();
}

}

缺点

  • 继承的缺点在于直接将任务的实现写到了线程当中,耦合度太高,想干其他的,必须要修改源代码
  • 解决办法使用类去实现Runnable接口,将任务和线程进行分开

Runnable

线程休眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("第一个 = " + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("第二个 = " + i);
}
});
thread1.start();
thread1.sleep(1000);
thread2.start();
}
  • sleep方法到底是让哪一个方法休眠?
    • 不在于谁调用sleep,而在与sleep写到哪,如上面的代码,先输出thread1,在输出thread2,休眠的是main方法
  • 下面的代码则是先执行thread2,再执行thread1,休眠的是thread1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 5; i++) {
System.out.println("第一个 = " + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("第二个 = " + i);
}
});
thread1.start();
thread2.start();
}

lambda表达式

1
2
3
4
5
6
7
8
9
(parameters) -> expression

(parameters) ->{ statements; }

parameters 是参数列表,expression 或 { statements; } 是Lambda 表达式的主体。如果只有一个参数,可以省略括号;如果没有参数,也需要空括号。

形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,可以省略形参列表的圆括号。
箭头(->)。英文短线和大于号。
代码块。如果代码块只有一句,可以省略花括号。如果只有一条 return 语句,可以省略 return,lambda表达式会自动返回这条语句的值。
1
2
3
4
5
6
Thread thread = new Thread(new Runnable(){
@Override
public void run(){

}
});
1
2
3
4
5
6
7
//一个类,不过没有名称而已
{
@Override
public void run(){

}
}
  • 进一步使用lambda来进一步简化代码,只把方法的实现进行传值,而不关注其他内容
  • () -> {}括号实现
1
2
3
4
5
6
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("i = " + i);
}
});
thread1.start();
  • 普通方法下如果需要调用MathOperation接口下的operation方法,就需要这样做
    • 创建一个实现MathOperation接口的类,调用这个类的new方法创建对象后调用operation方法
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
public class One {
interface MathOperation {
/**
* 计算和
* @param a 数字A
* @param b 数字B
* @return 相加结果
*/
int operation(int a, int b);
}
public static void main(String[] args) {
MathOperationImpl mathOperation = new MathOperationImpl();
int operation = mathOperation.operation(1, 2);
System.out.println("operation = " + operation);
}
}

class MathOperationImpl implements One.MathOperation {

@Override
public int operation(int a, int b) {
return a + b;
}
}

  • 但是使用lambda表达式就很方便了,代码简化为下方
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
public class One {
interface MathOperation {
/**
* 计算和
* @param a 数字A
* @param b 数字B
* @return 相加结果
*/
int operation(int a, int b);
}
public static void main(String[] args) {
MathOperation mathOperation = new MathOperation() {
@Override
public int operation(int a, int b) {
return a + b;
}
};
int num = mathOperation.operation(1,2);
System.out.println("num = " + num);
}
}
//此时MathOperation接口只有一个抽象方法,还可以简化为下面的写法

public class One {
interface MathOperation {
/**
* 计算和
* @param a 数字A
* @param b 数字B
* @return 相加结果
*/
int operation(int a, int b);

}
public static void main(String[] args) {
MathOperation mathOperation = (int a,int b) -> {
return a + b;
};
int num = mathOperation.operation(1,2);
System.out.println("num = " + num);
}
}

//lambda也可以省略参数类型
public static void main(String[] args) {
MathOperation mathOperation = (a,b) -> {
return a + b;
};
int num = mathOperation.operation(1,2);
System.out.println("num = " + num);
}

//甚至,如果只有一行,且是返回值,{}都可以省略(类似于前端的es6箭头函数写法)
public static void main(String[] args) {
MathOperation mathOperation = (a,b) -> a + b;
int num = mathOperation.operation(1,2);
System.out.println("num = " + num);
}
//注意,二行代码就不能省略{}了

public static void main(String[] args) {
MathOperation mathOperation = (a,b) -> {
System.out.println(112);
return a + b;
};
int num = mathOperation.operation(1,2);
System.out.println("num = " + num);
}

顺带一提

类型推断

  • 有时候你经常看到List<Dog> dogs2 = new ArrayList<>();这种写法,是不是很好奇为啥不完整的写成List<Dog> dogs2 = new ArrayList<Dog>();,原因很简单,因为左边指明了列表类型为Dog,右边可以推断出来,写不写都无所谓的,这就是类型推断的作用
  • 但是类型推断也不是万能的,不是所有的都可以推断出来的,所以有时候,还是要显示的添加形参类型,例如:先不要管这个代码的具体作用
1
2
3
4
5
6
7
BinaryOperator b = (x, y)->x*y;
//上面这句代码无法通过编译,下面是报错信息:无法将 * 运算符作用于 java.lang.Object 类型。
The operator * is undefined for the argument type(s) java.lang.Object, java.lang.Object

//添加参数类型,正确的代码。
BinaryOperator<Integer> b = (x, y)->x*y;

函数式接口

  • 具体可看
  • 满足下面规则就是函数式接口
    • 只能有一个抽象方法。
    • 可以有多个静态方法和默认方法。
    • 默认包含Object类的方法。
  • 可以很方便我们使用lambda表达式

我们常用的Runnable就是函数式接口

System.out::print

练习项目-下载工具

ScheduledExecutorService

  • scheduleAtFixedRate

    • 任务耗费的时间会和设定的period同步开始计时,比如说一个任务耗费6秒,但是设置的是每隔3秒执行,就会导致过了3秒再次执行任务
    • 倘若在执行任务的时候,耗时超过了间隔时间,则任务执行结束之后直接再次执行,而不是再等待间隔时间执
  • scheduleWithFixedDelay

    • 在执行任务的时候,无论耗时多久,任务执行结束之后都会等待间隔时间之后再继续下次任务。

下载文件进度功能

  • 百分比 = 已下载的文件大小 / 要下载的文件大小
    • percent = downloadedSize / totalSize;
  • 下载速度 = 已下载的文件大小 - 上一次下载的文件大小
    • speed = downloadedSize - prevDownloadedSize;
  • 剩余时间 = (要下载的文件大小 - 已下载的文件大小) / 下载速度
    • remainTime = ( totalSize - downloadedSize ) / speed;

线程池

  • execute

    • 任务提交给线程池
  • 线程满了,队列也慢了,就会执行拒绝策略

  • 创建线程池

1
2
3
4
5
6
7
8
9
10
11
12
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Pool {
public static void main(String[] args) {
//设置核心线程为2个,总共线程为3个,非核心线程则为1个,设置等待队列为5
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5));

Runnable r1 = () -> {
System.out.println("当前执行线程" + Thread.currentThread().getName());
};
//提交单个线程到线程池
// threadPoolExecutor.execute(r1);

//如果8改为9,就会执行拒绝策略了
for (int i = 0; i < 8; i++) {
threadPoolExecutor.execute(r1);
}
}
}

  • 关闭
1
2
3
4
shutdown():在完成已提交的任务后关闭服务,不再接受新任;
shutdownNow():停止所有正在执行的任务并关闭服务;
isTerminated():测试是否所有任务都执行完毕了;
isShutdown():测试是否该ExecutorService已被关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
//设置核心线程为2个,总共线程为3个,非核心线程则为1个,设置等待队列为5
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5));

Runnable r1 = () -> {
System.out.println("当前执行线程" + Thread.currentThread().getName());
};
//提交单个线程到线程池
// threadPoolExecutor.execute(r1);

for (int i = 0; i < 8; i++) {
threadPoolExecutor.execute(r1);
}

//线程池关闭
threadPoolExecutor.shutdown();//温和,等待队列任务执行完成后才关闭
threadPoolExecutor.shutdownNow();//暴力关闭,如果等待队列还有任务,都抛弃,不执行

}
  • 你也可以使用来等待一会,如果还没有执行完成则强制关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws InterruptedException {
//设置核心线程为2个,总共线程为3个,非核心线程则为1个,设置等待队列为5
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5));

Runnable r1 = () -> {
System.out.println("当前执行线程" + Thread.currentThread().getName());
};
//提交单个线程到线程池
// threadPoolExecutor.execute(r1);

for (int i = 0; i < 8; i++) {
threadPoolExecutor.execute(r1);
}

//线程池关闭
//threadPoolExecutor.shutdown();//温和,等待队列任务执行完成后才关闭
//threadPoolExecutor.shutdownNow();//暴力关闭,如果等待队列还有任务,都抛弃,不执行
threadPoolExecutor.shutdown();
//等待一分钟,如果线程池没有关闭则执行里面逻辑
if(!threadPoolExecutor.awaitTermination(1,TimeUnit.MINUTES)){
threadPoolExecutor.shutdownNow();
}
}

切片下载

  • 这里用到的切片下载就是请求头的Range
    • 告知服务端,客户端下载该文件想要从指定的位置开始下载,至于 Range 字段属性值的格式有以下几种:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
语法格式
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>

<unit> 类型,一般来说是bytes;
<range-start> 表示范围的起始值,一般是数字,如果不是数字就看服务端逻辑如何处理;
<range-end> 表示范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束,如果非数字,同上。

常见的
Range:bytes=0-500
表示下载从0500字节的文件,即头500个字节 ,[0-500]前闭后闭。0<=range<=500

Range:bytes=501-1000
表示下载从5001000这部分的文件,单位字节

Range:bytes=-500
表示下载最后的500个字节

Range:bytes=500-
表示下载从500开始到文件结束这部分的内容

Range:bytes=500-600,700-1000
表示下载这两个区间的内容

原子类

volatile关键字

Volatile关键字的作用主要有如下两个:

  1. 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

    1. 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

      而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  2. 顺序一致性:禁止指令重排序。

CountDownLatch

  • CountDownLatch 是 Java 中的一个并发工具类,用于协调多个线程之间的同步。其作用是让某一个线程等待多个线程的操作完成之后再执行。它可以使一个或多个线程等待一组事件的发生,而其他的线程则可以触发这组事件。

  • 注意不要讲await写成了wait

问题

  • 改为原子类后scheduleAtFixedRate执行不正常(只执行一次)
    • 在源码的Java doc中的发现了如下一句话:If any execution of the task encounters anexception, subsequent executions are suppressed.Otherwise, the task will onlyterminate via cancellation or termination of the executor.
    • 简单总结就是:如果定时任务执行过程中遇到发生异常,则后面的任务将不再执行。
  • 之前的
    • 可以看到downloadedSize不为空

  • 但是现在这个是null
    • 解决是延迟1秒调用

  • 可能你改为原子类后依旧是null
    • 可能你没有new
    • 应该是public static volatile DoubleAdder downloadedSize = new DoubleAdder();

技巧

  • 快速100次for循环
    • 100.for后tab

  • 输出n次,只保留最新一次的输出结果(java 怎么替换上一次的输出)

    • \r作用 将光标定义到当前行行首

      • \r后有新内容时,会先删除之前以前存在过的文本,即只打印\r后面的内容
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      //将会输出 结果是0结果是1结果是2结果是3结果是4
      for (int i = 0; i < 5; i++) {

      System.out.print("结果是" + i);
      }

      //只会输出 结果是4
      for (int i = 0; i < 5; i++) {
      System.out.print("\r");
      System.out.print("结果是" + i);
      }
  • 常用的流分类

分类字节输入流字节输出流字符输入流字符输出流
抽象父类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutStreamFileReaderFileWriter
访问数值ByteArrayInputStreamByteArrayOutStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream
装饰流FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流PrintStreamPrintWriter
数据过滤流DataInputStreamDataOutputStream

疑问

  • lambda写的这么简略,怎么知道是传入的哪一个类?

thread.run和start方法有什么区别?直接调用run有什么问题吗?

  • idea的建议对 ‘run()’ 的调用可能应当替换为 ‘start()’

  • 直接调用run就不是多线程了,而是执行里面的一个方法
    • run()方法当作普通方法的方式调用,程序还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码: 而如果直接用run方法,这只是调用一个方法而已,程序中依然只有主线程–这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的。
  • 具体可看