Java笔记
对象序列化
反序列化
多线程
继承thread类
实现runnable接口
线程休眠和线程优先级
线程等待
线程中断
守护线程
线程并发问题
sync锁简介
lock锁
线程池
对象序列化
Java允许我们在内存中创建可复用的Java对象,但一般情况下,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象
Java对象序列化就能够帮助我们实现该功能。使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来再将这些字节组装成对象
必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的静态变量
除了在持久化对象时会用到对象序列化之外,当使用 RMI ,或在网络中传递对象时,都会用到对象序列化
Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用,但性能不是最好的
实例 demo
在Java中,只要一个类实现了 java.io.Serializable 接口,它就可以被序列化(枚举类可以被序列化)。
// Gender类,表示性别
// 每个枚举类型都会默认继承类java.lang.Enum,而Enum类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
public enum Gender {
MALE, FEMALE
}
// Person 类实现了 Serializable 接口,它包含三个字段。另外,它还重写了该类的 toString() 方法,以方便打印 Person 实例中的内容。
public class Person implements Serializable {
private String name = null;
private Integer age = null;
private Gender gender = null;
public Person() {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
// 省略 set get 方法
@Override
public String toString() {
return "[" + name + ", " + age + ", " + gender + "]";
}
}
// SimpleSerial类,是一个简单的序列化程序,它先将Person对象保存到文件person.out中,然后再从该文件中读出被存储的Person对象,并打印该对象。
public class SimpleSerial {
public static void main(String[] args) throws Exception {
File file = new File("person.out");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); // 注意这里使用的是 ObjectOutputStream 对象输出流封装其他的输出流
Person person = new Person("John", 101, Gender.MALE);
oout.writeObject(person);
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); // 使用对象输入流读取序列化的对象
Object newPerson = oin.readObject(); // 没有强制转换到Person类型
oin.close();
System.out.println(newPerson);
}
}
// 上述程序的输出的结果为:
arg constructor
[John, 31, MALE]
• 序列化:把Java对象转换为字节序列的过程。
• 反序列化:把字节序列恢复为Java对象的过程。
对象的序列化主要有两种用途:
• 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)
• 在网络上传送对象的字节序列。(网络传输对象)
2 使用
在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:
• Serializable 接口
• Externalizable 接口
那这两个接口是如何工作的呢?两者又有什么关系呢?我们分别进行介绍。
2.1 Serializable 接口
一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。
由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
2.2 Externalizable 接口
它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
Java 应用程序中的多线程允许多个线程在单个进程中同时运行。线程是独立执行的任务,可以共享数据和其他资源,例如文件和网络连接。在本文中,我们将探讨什么是Java多线程以及它的优点和缺点。
线程是可由操作系统独立调度的轻量级进程。它也被定义为程序中允许同时执行代码的独立执行路径,这意味着多个线程可以同时执行。
每个线程都有自己的堆栈,这意味着它可以有局部变量并跟踪自己的执行。线程可用于实现并行性,即同时执行多个任务。
在Java(和其他编程语言)中,线程共享相同的地址空间,因此可以访问应用程序或程序的所有变量、对象、方法和全局范围。
什么是Java中的多线程?
多线程是操作系统在同一时间点在内存中拥有大量线程的能力,让人误以为所有这些线程都在并发执行。操作系统为每个“线程”分配一个特定的时间片,每当它们的时间片到期时,就在它们之间切换。线程之间的切换过程称为关联转换。
上下文切换包括:
l 存储线程的状态。
l 清空CPU寄存器。
l 将CPU控制权传递给队列中的下一个线程。
应当注意,在任何时间点,处理器可以执行一个且仅一个线程。通过同时启动多个任务,当有许多任务等待完成时(如从磁盘读取),与运行单线程应用程序相比,你可以获得更好的吞吐量。
Java中多线程的优势:
l 提高响应能力
l 更快的执行
l 更好的CPU和内存利用率
l 支持并发
多线程的缺点:
l 测试和调试中的挑战
l 增加了代码库的复杂性
Java中的多线程
Java 编程语言具有对使用多线程的内置支持。当你运行 Java 应用程序时,Java 虚拟机 (JVM) 会创建一个称为主线程的线程。主线程负责运行应用程序的 main() 方法。然后主线程可以创建其他线程,这些线程可以与主线程并发运行。
线程的并发执行可以通过利用多个 CPU 或处理器来帮助提高应用程序的性能。它还可以通过允许在用户与图形用户界面 (GUI) 交互时在后台执行任务来帮助提高响应能力。
Java中的线程状态是什么?
当 Java 程序启动时,只有一个线程——主线程。该线程负责执行程序的 main() 方法。一旦 main() 方法退出,程序就会终止。但是,Java 程序可以有多个线程同时运行。
线程可以处于以下几种状态之一:
l 就绪或可运行——这是线程在就绪或可运行队列中等待分配处理器的状态。当你在线程对象上调用 start 方法时,线程进入此状态。当运行时隐式调用 yield 方法时,线程将控制权交给就绪或可运行队列中的下一个线程。
l Running – 这是处理器正在执行线程时的状态。调度程序负责在适当的时间将线程调度到运行状态——通常是在轮到它并且当前运行的线程完成执行之后。
l Waiting/Suspended/Blocked – 当你调用线程对象的挂起方法时,线程进入挂起状态。在调用 resume 方法后,可以将挂起的线程移回运行状态。线程处于等待状态时等待I/O。
当线程完成执行或终止时,它会停止。
在java中线程来使用有两种方法。
继承Thread类,重写run方法
实现Runnable接口,重写run方法
/创建线程的方式一:继承Thread类,重写run()方法,调用start开启线程
//线程开启不一定立即执行,由cpu决定
public class TestThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我在看代码!!!!!!!!!!!!!!!!!!");
}
}
public static void main(String[] args) {
TestThread1 testThread1=new TestThread1();
testThread1.start();
for (int i = 0; i < 10; i++) {
System.out.println("我在学习多线程---");
}
}
}
实现多线程同步下载图片#
代码演示#
public class TestThread2 extends Thread{
private String url;
private String name;
public TestThread2(String url,String name){
this.url=url;
this.name=name;
}
@Override
public void run() {
WebDownloader webDownloader=new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为:"+name);
}
public static void main(String[] args) {
TestThread2 testThread201=new TestThread2("http://8.136.39.89:9090/asserts/img/portfolio-1.jpg","portfolio-1.jpg");
TestThread2 testThread202=new TestThread2("http://8.136.39.89:9090/asserts/img/portfolio-2.jpg","portfolio-2.jpg");
TestThread2 testThread203=new TestThread2("http://8.136.39.89:9090/asserts/img/portfolio-3.jpg","portfolio-3.jpg");
testThread201.start();
testThread202.start();
testThread203.start();
}
}
class WebDownloader{
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downder出现问题");
}
}
1.定义实现Runnable接口
2.覆盖Runnable接口中的run方法,将线程要运行的代码存放在run方法中。
3.通过Thread类建立线程对象。
4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要讲Runnable接口的子类对象传递给Thread的构造方法。因为自定义的方法的所属的对象是Runnable接口的子类对象。
5.调用Thread类的start方法开启线程并调用Runnable接口子类run方法。
(二)线程安全的共享代码块问题
目的:程序是否存在安全问题,如果有,如何解决?
如何找问题:
1.明确哪些代码是多线程运行代码。
2.明确共享数据
3.明确多线程运行代码中哪些语句是操作共享数据的。
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 class Bank{
private int sum;
public void add(int n){
sum+=n;
System.out.println("sum="+sum);
}
}
class Cus implements Runnable{
private Bank b=new Bank();
public void run(){
synchronized(b){
for(int x=0;x<3;x++)
{
b.add(100);
}
}
}
}
public class BankDemo{
public static void main(String []args){
Cus c=new Cus();
Thread t1=new Thread(c);
Thread t2=new Thread(c);
t1.start();
t2.start();
}
}
线程休眠
相关API
方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
代码示例
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
class MyThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("睡觉前");
System.out.println("悄咪咪要来了");
Thread.sleep(3000);
System.out.println("悄咪咪来了他还在睡觉");
System.out.println("睡醒了");
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
线程等待
进入Thread.State.WAITING
Java 提供了多种将线程置于 WAITING 状态的方法。
• Object.wait()
我们可以将线程置于 WAITING 状态的最标准方法之一是通过 wait() 方法。 当一个线程拥有一个对象的监听器时,我们可以暂停它的执行,直到另一个线程完成工作并使用 notify() 方法将其唤醒。
package com.toutiao.treadwaiting;
public class TreadWaitingApplication {
private static Object lock = new Object();
public static void main(String[] args) {
//Thread1
new Thread(() -> {
System.out.println(Thread.currentThread());
synchronized (lock) {
System.out.println("线程1开始执行");
try {
lock.wait();
System.out.println("线程1执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread1").start();
}
}
执行main方法,thread1线程处于等待状态
线程中断
中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。
使用stop方法虽然可以强行终止正在运行或挂起的线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
1、任务中一般都会有循环结构,只要用一个标记控制住循环,就可以结束任务。
2、如果线程处于了冻结状态,无法读取标记,此时可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格。
(一):使用退出标志
当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的,如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。
在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。
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 public class test1 {
public static volatile boolean exit =false; //退出标志
public static void main(String[] args) {
new Thread() {
public void run() {
System.out.println("线程启动了");
while (!exit) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束了");
}
}.start();
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
exit = true;//5秒后更改退出标志的值,没有这段代码,线程就一直不能停止
}
}
(二):使用 interrupt 方法
Thread.interrupt()方法: 作用是中断线程。将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。
守护线程
Java守护线程
Java中,通过Thread类,我们可以创建2种线程,分为守护线程和用户线程。
守护线程是所有非守护线程的保姆,当所有非守护线程执行完成或退出了,即使还有守护线程在运行,JVM也会直接退出,因此守护线程通常是用来处理一些辅助工作。
反之,对于非守护线程,只要有一个在运行,JVM就不会退出。
典型的守护线程如垃圾回收GC线程,当用户线程都结束后,GC也就没有单独存在的必要,JVM直接退出。
我们可以通过Thread对象的setDaemon(boolean on)方法设置是否为守护线程,要在start之前设置:
Thread thread = new Thread(runnable);
thread.setDaemon(true); // true表示守护线程,false表示用户线程
thread.start();
需要注意的是,如果没有显示调用setDaemon方法进行设置,线程的模式是取决于父线程是否为守护线程,也就是创建此线程所在的线程。
如果父线程是守护线程,创建的线程默认是守护线程;
如果父线程是用户线程,创建的线程默认是用户线程。
这可以从Thread类的init方法源代码中看出:
Thread parent = currentThread();
this.daemon = parent.isDaemon();
对于daemon的设置,保存在了Thread对象的成员变量中,Thread提供了setter/getter:
private boolean daemon = false; // 是否为守护线程
public final void setDaemon(boolean on) {
// SecurityManager安全检查,本文不展开讨论
checkAccess();
// 检查线程是否已启动,已启动无法设置daemon
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
public final boolean isDaemon() {
return daemon;
}
线程并发问题
并发问题(安全性问题)
核心
要编写线程安全的代码,核心在于对状态访问操作进行管理。特别是对共享的和可变的状态的访问。
解决思路
当发生安全性问题时。有三种解决问题的角度:
• 不在线程之间共享变量
• 将状态变量修改为不可变
• 访问状态变量时使用同步机制。
前两种方式从根本上避免了多线程并发问题的原因:对共享和可变状态的访问。
1. 不在线程之间共享变量
即限制变量只能在单个线程中访问。
实现方式:
1. 线程封闭
保证变量只能被一个线程可以访问到。可以通过Executors.newSingleThreadExecutor()实现。
2. 栈封闭
栈封闭即使用局部变量。局部变量只会存在于本地方法栈中,不能被其他线程访问,因此也就不会出现并发问题。所以如果可以使用局部变量就优先使用局部变量。
3. ThreadLocal封闭
ThreadLocal是Java提供的实现线程封闭的一种方式,ThreadLocal内部维护了一个Map,Map的key是各个线程,而Map的值就是要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
2. 将状态变量修改为不可变
即使用不可变对象。
不可变对象:当一个对象构造完成后,其状态就不再变化,我们称这样的对象为不可变对象(Immutable Object),这些对象关联的类为不可变类(Immutable Class)。
比如Java中的String、Integer、Double、Long等所有原生类型的包装器类型,都是不可变的。
大多数时候,线程间是通过使用共享资源实现通信的。如果该共享资源诞生之后就完全不再变更(犹如一个常量),多线程间共同并发读取该共享资源是不会产生线程冲突的,因为所有线程无论何时读取该共享资源,总是能获取到一致的、完整的资源状态,这样也能规避多线程冲突。不可变对象就是这样一种诞生之后就完全不再变更的对象,该类对象天生支持在多线程间共享。
3. 使用同步机制
关注一个并发问题,有3个基本的关注点:
• 原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
• 可见性,当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
• 有序性,有序性指的是数据不相关的变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。
所有的并发问题的都可以从这三个点进行分析并针对性的进行解决。
Sync 锁
synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。synchronized 可以保证线程的可见性,synchronized 属于隐式锁,锁的持有与释放都是隐式的,我们无需干预。synchronized最主要的三种应用方式:
1. 修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
2. 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
3. 修饰代码块:指定加锁对象,进入同步代码库前要获得给定对象的锁
2、synchronized 底层语义原理:
synchronized 锁机制在 Java 虚拟机中的同步是基于进入和退出监视器锁对象 monitor 实现的(无论是显示同步还是隐式同步都是如此),每个对象的对象头都关联着一个 monitor 对象,当一个 monitor 被某个线程持有后,它便处于锁定状态。在 HotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现的,每个等待锁的线程都会被封装成 ObjectWaiter 对象,ObjectMonitor 中有两个集合,WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表 ,owner 区域指向持有 ObjectMonitor 对象的线程。当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合尝试获取 moniter,当线程获取到对象的 monitor 后进入 _Owner 区域并把 _owner 变量设置为当前线程,同时 monitor 中的计数器 count 加1;若线程调用 wait() 方法,将释放当前持有的 monitor,count自减1,owner 变量恢复为 null,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor 并复位变量的值,以便其他线程获取 monitor。
Lock锁
Lock与synchronized
继同步代码块和同步方法之后,Lock作为解决线程安全的第三种方式,JDK5.0新增,与synchronized对比如下:
1.Lock是显示锁(手动开启和关闭锁,别忘了关闭锁),synchronized是隐式锁,出了作用域自动释放。
2.Lock只有代码块锁,synchronized有代码块锁和方法锁。
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多子类)。
优先使用顺序
Lock---->同步代码块(已经进入了方法体,分配了相应资源)---->同步方法(在方法体之外)。
案例
多窗口买票(Lock锁)
Java
1 import java.util.concurrent.locks.ReentrantLock;
2
3 class Windows implements Runnable{
4
5 private int ticket = 100;
6
7 //true:公平锁,先进先出;不写默认为false,不公平
8 //1.实例化ReentrantLock
9 private ReentrantLock lock = new ReentrantLock(true);
10
11 @Override
12 public void run() {
13 while(true){
14 try{
15 //2.调用lock()
16 lock.lock();
17
18 if(ticket > 0){
19 try {
20 Thread.sleep(100);
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 System.out.println(Thread.currentThread().getName() + ": 售票 :票号为: "+ ticket);
25 ticket--;
26 }else {
27 break;
28 }
29 }finally{
30 //3.调用解锁的方法unlock()
31 lock.unlock();
32 }
33 }
34 }
35 }
36 public class LockTest {
37 public static void main(String[] args) {
38 Windows w = new Windows();
39
40 Thread t1 = new Thread(w);
41 Thread t2 = new Thread(w);
42 Thread t3 = new Thread(w);
43
44 t1.setName("窗口1");
45 t2.setName("窗口2");
46 t3.setName("窗口3");
47
48 t1.start();
49 t2.start();
50 t3.start();
51 }
52 }
线程池
线程我们可以使用 new 的方式去创建,但如果并发的线程很多,每个线程执行的时间又不长,这样频繁的创建线程会大大的降低系统处理的效率,因为创建和销毁进程都需要消耗资源,线程池就是用来解决类似问题。
线程池实现了一个线程在执行完一段任务后,不销毁,继续执行下一段任务。用《Java并发编程艺术》提到线程池的优点:
1、降低资源的消耗:使得线程可以重复使用,不需要在创建线程和销毁线程上浪费资源
2、提高响应速度:任务到达时,线程可以不需要创建即可以执行
3、线程的可管理性:线程是稀缺资源,如果无限制的创建会严重影响系统效率,线程池可以对线程进行管理、监控、调优。
Excutor框架
Excutor框架是线程池处理线程的核心,包括创建任务,传递任务,任务的执行三个方面
1、创建任务
执行的任务需要实现 Runnable 或者 Callable 接口,然后重写里面的 run 方法,这两个接口区别下面会写
2、传递任务
以前执行线程都是直接创建线程然后调用 start() 方法去执行线程,现在我们需要把任务传递到线程池里面去,传递任务的核心接口就是 Excutor 接口