第十一周笔记
九、对象序列化控制输入输出
1、对象序列化是什么
对象序列化(Serialize) 指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize) 则指从IO流中恢复该Java对象。
2、对象序列化(Serialize) 指将一个Java对象写入IO流中,与此对应的是,
对象的反序列化(Deserialize) 则指从IO流中恢复该Java对象。
如果想让某个Java对象能够序列化,则必须让它的类实现java.io.Serializable 接口。
3、 ObjectInputStream类继承了InputStream类,同时实现了ObjectInput接口,
提供了将对象序列化并从流中读取出来的功能
输入对象流(高级流)
作用:将Java对象还原出来
要求:还原对象的序列化版本号必须要求相同
4、ObjectOutputStream 类继承了OutputStream 类,同时实现了ObjectOutput接口,
提供将对象序列化并写入流中的功能
对象流(高级流),作用:用于将Java对象写入文件(要求:被写入的对象必须实现序列化接口)
5、反序列化
ObjectInputStream类继承了InputStream类,同时实现了ObjectInput接口,提供了将对象序列化并从流中读取出来的功能。该类方法的构造方法如下:
public ObjectInputStream(InputStream in)
该构造方法需要传入一个InputStream对象,用来创建从指定InputStream读取的ObjectInputStream
(1)创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。
//创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("object.txt"))
(2)调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型。
多线程
一、多线程基础
1、进程
(1)在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
(2)某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
(3)进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
(4)操作系统调度的最小任务单位其实不是进程,而是线程。
(5)同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:多进程模式(每个进程只有一个线程),多线程模式(一个进程有多个线程),多进程+多线程模式(复杂度最高)
2、进程 vs 线程
和多线程相比,多进程的缺点在于:
1、创建进程比创建线程开销大,尤其是在Windows系统上;
2、进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃 会直接导致整个进程崩溃。
3、多线程
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
(1)多线程模型是Java程序最基本的并发模型;
(2)网络、数据库、Web开发等都依赖Java多线程模型。
二、线程的状态
1、JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态: 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU并临时中止自己的执行,进入阻塞状态 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
2、在Java程序中,一个线程对象只能调用一次start()
方法启动新线程,并在新线程中执行run()
方法。一旦run()
方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
(1)New:新创建的线程,尚未执行;
(2)Runnable:运行中的线程,正在执行run()
方法的Java代码;
(3)Blocked:运行中的线程,因为某些操作被阻塞而挂起;
(4)Waiting:运行中的线程,因为某些操作在等待中;
(5)Timed Waiting:运行中的线程,因为执行sleep()
方法正在计时等待;
(6)Terminated:线程已终止,因为run()
方法执行完毕。
3、线程终止的原因有:
(1)线程正常终止:run()
方法执行到return
语句返回;
(2)线程意外终止:run()
方法因为未捕获的异常导致线程终止;
(3)对某个线程的Thread
实例调用stop()
方法强制终止(强烈不推荐使用)。
小结
1、Java线程对象Thread
的状态包括:New
、Runnable
、Blocked
、Waiting
、Timed Waiting
和Terminated
;
2、通过对另一个线程对象调用join()
方法可以等待其执行结束;
3、可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
4、对已经运行结束的线程调用join()
方法会立刻返回。
三、中断线程
volatile
关键字的目的是告诉虚拟机:
1、每次访问变量时,总是获取主内存的最新值;
2、每次修改变量后,立刻回写到主内存。
小结
(1)对目标线程调用interrupt()
方法可以请求中断一个线程,目标线程通过检测isInterrupted()
标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException
;
(2)目标线程检测到isInterrupted()
为true
或者捕获了InterruptedException
都应该立刻结束自身线程;
(3)通过标志位判断需要正确使用volatile
关键字;
(4)volatile`关键字解决了共享变量在线程间的可见性问题。
四、线程同步
如何使用synchronized
:
找出修改共享变量的线程代码块;
选择一个共享实例作为锁;
使用synchronized(lockObject) { ... }
。
在使用synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized
结束处正确释放锁
不需要synchronized的操作
JVM规范定义了几种原子操作:
(1)基本类型(long
和double
除外)赋值,例如:int n = m
;
(2)引用类型赋值,例如:List<String> list = anotherList
。
小结
1、多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized
同步;
2、同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
3、注意加锁对象必须是同一个实例;
4、对JVM定义的单个原子操作不需要同步。
五、同步方法
小结
1、用synchronized
修饰方法可以把整个方法变为同步代码块,synchronized
方法加锁对象是this
;
2、通过合理的设计和数据封装可以让一个类变为“线程安全”;
3、一个类没有特殊说明,默认不是thread-safe;
4、多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。
六、wait和notify
小结
wait
和notify
用于多线程协调运行:
(1)在synchronized
内部可以调用wait()
使线程进入等待状态;
(2)必须在已获得的锁对象上调用wait()
方法;
(3)在synchronized
内部可以调用notify()
或notifyAll()
唤醒其他等待线程;
(4)必须在已获得的锁对象上调用notify()
或notifyAll()
方法;
(5)已唤醒的线程还需要重新获得锁后才能继续执行。
七、ReentrantLock
小结
1、ReentrantLock
可以替代synchronized
进行同步;
2、ReentrantLock
获取锁更安全;
3、必须先获取到锁,再进入try {...}
代码块,最后使用finally
保证释放锁;
4、可以使用tryLock()
尝试获取锁。
八、Condition
Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的:
(1)await()`会释放当前锁,进入等待状态;
(2)signal()`会唤醒某个等待线程;
(3)signalAll()`会唤醒所有等待线程;
(4)唤醒线程从await()
返回后需要重新获得锁。
小结
1、Condition
可以替代wait
和notify
;
2、Condition
对象必须从Lock
对象获取。
九、ReadWriteLock
实际上我们想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待:
读 | 写 | |
读 | 允许 | 不允许 |
写 | 不允许 | 不允许 |
使用ReadWriteLock
可以解决这个问题,它保证:
(1)只允许一个线程写入(其他线程既不能写入也不能读取);
(2)没有写入时,多个线程允许同时读(提高性能)。
小结
使用ReadWriteLock
可以提高读取效率:
(1)ReadWriteLock`只允许一个线程写入;
(2)ReadWriteLock`允许多个线程在没有写入时同时读取;
(3)ReadWriteLock`适合读多写少的场景。
十、StampedLock
小结
1、StampedLock
提供了乐观读锁,可取代ReadWriteLock
以进一步提升并发性能;
2、StampedLock
是不可重入锁。
十一、Semaphore
前面我们讲了各种锁的实现,本质上锁的目的是保护一种受限资源,保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock)。
小结
如果要对某一受限资源进行限流访问,可以使用Semaphore
,保证同一时间最多N个线程访问受限资源。
十二、Concurrent集合
守护线程:
守护线程又称为后台线程,默认创建的线程都是普通线程,或者称为前台线程,
线程提供了一个方法:
void setDaemon(boolean on)
只有调用该方法并且传入参数为true时,该线程才会被设置为守护线程。
守护线程在使用上与普通线程没有差别,但是在结束时会有一个差别,
即:线程结束时,所有正在运行的守护线程都会被强制停止
进程结束:当一个进程中所有的普通线程都结束时,进程才会结束
多线程并发的安全问题:
产生:当多个线程并发操作时,由于线程切换实际的不确定性,会导致操作资源的代码
顺序为按照设计顺序执行,出现操作混乱的情况,严重时可能导致系统瘫痪。
解决:将并发操作同一资源改为同步运行,即:有先后顺序的操作
同步与异步:
同步:程序运行有先后顺序
异步:程序运行没有先后顺序
小结
使用java.util.concurrent
包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码。
十三、Atomic
我们以AtomicInteger
为例,它提供的主要操作有:
(1)增加值并返回新值:int addAndGet(int delta)
(2)加1后返回新值:int incrementAndGet()
(3)获取当前值:int get()
(4)用CAS方式设置:int compareAndSet(int expect, int update)
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
小结
使用java.util.concurrent.atomic
提供的原子操作可以简化多线程编程:
(1)原子操作实现了无锁的线程安全;
(2)适用于计数器,累加器等。
十四、线程池
线程池:
作用:重复利用资源
newCachedThreadPool:
创建一个可缓存的无界线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程。
当线程池中的线程空闲时间超过60s,则会自动回收该线程,当任务超过线程池的线程数则创建新的线程,
线程池的大小上限为Integer.MAX_VALUE,可看作无限大。
newScheduledThreadPool: 创建一个定长的线程池,可以指定线程池核心线程数,支持定时及周期性任务的执行
ewSingleThreadExecutor: 创建一个单线程化的线程池,它只有一个线程,用仅有的一个线程来执行任务,
因为ExecutorService
只是接口,Java标准库提供的几个常用实现类有:
(1)FixedThreadPool:线程数固定的线程池;
(2)CachedThreadPool:线程数根据任务动态调整的线程池;
(3)SingleThreadExecutor:仅单线程执行的线程池。
一个Future<V>
接口表示一个未来可能会返回的结果,它定义的方法有:
(1)get()`:获取结果(可能会等待)
(2)get(long timeout, TimeUnit unit)`:获取结果,但只等待指定的时间;
(3)cancel(boolean mayInterruptIfRunning)`:取消当前任务;
(4)isDone()`:判断任务是否已完成。
十五、CompletableFuture
使用Future
获得异步执行结果时,要么调用阻塞方法get()
,要么轮询看isDone()
是否为true
,这两种方法都不是很好,因为主线程也会被迫等待。
CompletableFuture
的优点是:
1、异步任务结束时,会自动回调某个对象的方法;
2、异步任务出错时,会自动回调某个对象的方法;
3、主线程设置好回调后,不再关心异步任务的执行。
注意CompletableFuture
的命名规则:
(1)xxx()`:表示该方法将继续在已有的线程中执行;
(2)xxxAsync()`:表示将异步在线程池中执行。