🦛
type
status
date
slug
summary
tags
category
icon
password
Property
Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。
如果可以复用一组线程
那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
Java标准库提供了
ExecutorService
接口表示线程池,它的典型用法如下:因为
ExecutorService
只是接口,Java标准库提供的几个常用实现类有:- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
🐼
type
status
date
slug
summary
tags
category
icon
password
Property
List
是一种顺序列表,如果有一个存储学生Student
实例的List
,要在List
中根据name
查找某个指定的Student
的分数,应该怎么办?最简单的方法是遍历
List
并判断name
是否相等,然后返回指定元素:这种需求其实非常常见,即通过一个键去查询对应的值。使用
List
来实现存在效率非常低的问题,因为平均需要扫描一半的元素才能确定,而Map
这种键值(key-value)映射表的数据结构,作用就是能高效通过key
快速查找value
(元素)。用
Map
来实现根据name
查询某个Student
的代码如下:通过上述代码可知:
Map<K, V>
是一种键-值映射表,当我们调用put(K key, V value)
方法时,就把key
和value
做了映射并放入Map
。当我们调用V get(K key)
时,就可以通过key
获取到对应的value
。如果key
不存在,则返回null
。和List
类似,Map
也是一个接口,最常用的实现类是HashMap
。如果只是想查询某个
key
是否存在,可以调用boolean containsKey(K key)
方法。如果我们在存储
Map
映射关系的时候,对同一个key调用两次put()
方法,分别放入不同的value
,会有什么问题呢?例如:🦥
type
status
date
slug
summary
tags
category
icon
password
Property
Java的IO标准库提供的
InputStream
根据来源可以包括:FileInputStream
:从文件读取数据,是最终数据源;
ServletInputStream
:从HTTP请求读取数据,是最终数据源;
Socket.getInputStream()
:从TCP连接读取数据,是最终数据源;
- ...
如果我们要给
FileInputStream
添加缓冲功能,则可以从FileInputStream
派生一个类:如果要给
FileInputStream
添加计算签名的功能,类似的,也可以从FileInputStream
派生一个类:如果要给
FileInputStream
添加加密/解密功能,还是可以从FileInputStream
派生一个类:🦛
type
status
date
slug
summary
tags
category
icon
password
Property
使用
Future
获得异步执行结果时,要么调用阻塞方法get()
,要么轮询看isDone()
是否为true
,这两种方法都不是很好,因为主线程也会被迫等待。从Java 8开始引入了
CompletableFuture
,它针对Future
做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。我们以获取股票价格为例,看看如何使用
CompletableFuture
:创建一个
CompletableFuture
是通过CompletableFuture.supplyAsync()
实现的,它需要一个实现了Supplier
接口的对象:这里我们用lambda语法简化了一下,直接传入
Main::fetchPrice
,因为Main.fetchPrice()
静态方法的签名符合Supplier
接口的定义(除了方法名外)。紧接着,
CompletableFuture
已经被提交给默认的线程池执行了,我们需要定义的是CompletableFuture
完成时和异常时需要回调的实例。完成时,CompletableFuture
会调用Consumer
对象:异常时,
CompletableFuture
会调用Function
对象:🦛
type
status
date
slug
summary
tags
category
icon
password
Property
Java的
java.util.concurrent
包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic
包。我们以
AtomicInteger
为例,它提供的主要操作有:- 增加值并返回新值:
int addAndGet(int delta)
- 加1后返回新值:
int incrementAndGet()
- 获取当前值:
int get()
- 用CAS方式设置:
int compareAndSet(int expect, int update)
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
如果我们自己通过CAS编写
incrementAndGet()
,它大概长这样:CAS是指,在这个操作中,如果
AtomicInteger
的当前值是prev
,那么就更新为next
,返回true
。如果AtomicInteger
的当前值不是prev
,就什么也不干,返回false
。通过CAS操作并配合do ... while
循环,即使其他线程修改了AtomicInteger
的值,最终的结果也是正确的。🍃
type
status
date
slug
summary
tags
category
icon
password
Property
Component中有一个方法 setBounds() 可以设置当前容器的位置和大小,但是我们需要明确一件事,如果我们手动的为组件设置位置和大小的话,就会造成程序的不通用性,例如:
创建了一个lable组件,很多情况下,我们需要让lable组件的宽高和“你好,世界”这个字符串自身的宽高一致,这种大小称为最佳大小。由于操作系统存在差异,例如在windows上,我们要达到这样的效果,需要把该Lable组件的宽和高分别设置为100px,20px,但是在Linux操作系统上,可能需要把Lable组件的宽和高分别设置为120px,24px,才能达到同样的效果。
如果要让我么的程序在不同的操作系统下,都有相同的使用体验,那么手动设置组件的位置和大小,无疑是一种灾难,因为有太多的组件,需要分别设置不同操作系统下的大小和位置。为了解决这个问题,Java提供了LayoutManager布局管理器,可以根据运行平台来自动调整组件大小,程序员不用再手动设置组件的大小和位置了,只需要为容器选择合适的布局管理器即可。
FlowLayout
在FlowLayout布局管理器中,组件像水流一样向某方向流动 (排列) ,遇到障碍(边界)就折回,重头开始排列 。在默认情况下, FlowLayout 布局管理器从左向右排列所有组件,遇到边界就会折回下一行重新开始。
FlowLayout 中组件的排列方向(从左向右、从右向左、从中间向两边等) , 该参数应该使用FlowLayout类的静态常量 : FlowLayout. LEFT 、 FlowLayout. CENTER 、 FlowLayout. RIGHT ,默认是左对齐。
🦥
type
status
date
slug
summary
tags
category
icon
password
Property
序列化是指把一个Java对象变成二进制内容,本质上就是一个
byte[]
数组。为什么要把Java对象序列化呢?因为序列化后可以把
byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。有序列化,就有反序列化,即把一个二进制内容(也就是
byte[]
数组)变回Java对象。有了反序列化,保存到文件中的byte[]
数组又可以“变回”Java对象,或者从网络上读取byte[]
并把它“变回”Java对象。一个Java对象要能序列化,必须实现一个特殊的
java.io.Serializable
接口,它的定义如下:Serializable
接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。把一个Java对象变为
byte[]
数组,需要使用ObjectOutputStream
。它负责把一个Java对象写入一个字节流:🦛
type
status
date
slug
summary
tags
category
icon
password
Property
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
这个时候有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。
两个线程同时对一个
int
变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
例如,对于语句:
看上去是一行语句,实际上对应了3条指令:
假设
n
的值是100
,如果两个线程同时执行n = n + 1
,得到的结果很可能不是102
,而是101
,原因在于:🦛
type
status
date
slug
summary
tags
category
icon
password
Property
现代操作系统(Windows,macOS,Linux)都可以执行多任务,多任务就是同时运行多个任务。
CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。
例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业。这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样
类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。
即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。
进程和线程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
🦛
type
status
date
slug
summary
tags
category
icon
password
Property
Java程序入口就是由JVM启动
main
线程,main
线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
答案是使用守护线程(Daemon Thread)。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用
start()
方法前,调用setDaemon(true)
把该线程标记为守护线程: