CoreJava多线程
线程的概念:
线程指进程中的一个执行流程,一个进程可以包含多个线程。
每一个进程都独享一块内存空间。每个进程都需要操作系统为其分配独立的内存地址,而同一进程中的多个线程在同一块地址空间工作,他们共享一块内存和资源。
java中有两中方法创建一个多线程类:
1、继承java.lang.Thread类,覆盖Thread类的run()方法
2、实现Runnable接口,实现Runnable接口的run()方法。推荐使用第二种,因为第二种更加的灵活。
每次调用java.exe的时候操作系统都会启动一个JVM进程,当启动JVM的时候,JVM都会创建一个主线程,改线程从程序入口main方法开始执行,这个线程的名字就叫main,可以在main方法中打印线程名:
System.out.println( Thread.currentThread().getName())来测试。
注意:每次程序运行的时候除了自定义的线程外还有一个main线程。
线程的调度
sleep(xxx),指当前运行的线程睡眠xxx秒,睡眠的时候当前线程会交出CPU,但是不会交出对象的锁!
yield(),指当前运行的线程交出CPU,给其他线程运行的机会。
join(),指将CPU交给调用join()方法的线程对象,直到该线程运行结束。
线程的各种状态
新建状态(new) //线程刚被创建出来
Runnable r = new ThreadTest2();
Thread t1 = new Thread(r,"t1");//创建线程t1 线程处在new状态
就绪状态(Runnable)
t1.start();//启动线程t1,这时候他处在Runnable状态,等待CPU的调度。
运行状态(Running)
如果处在Runnable状态的线程,被调度获得cpu,那么他就处在Running状态
阻塞状态(Blocked)
如果Running中的线程调用sleep(),yield()或者程序运行到等待输入的方法时,他就处在阻塞状态。
如果执行到了wait()语句,释放锁标记,并进入等待池中。直到某个线程调用notifyAll()方法将其从对象的等待池转进锁池,等待锁标记!
死亡状态(Dead)
线程执行完毕,就处于死亡状态。
线程安全的控制
关键就是使用:synchronized 、 wait() 和 notifyAll()
当多个线程试图同时修改某个实例的内容时,就由有可能会造成冲突。
为了解决这种冲突,使用synchronized关键字来对该共同访问的实例“加锁”。对同一个实例来说,任意时刻只能有一个synchronized方法在执行。当一个方法正在执行某个synchronized方法时,其他线程如果想要执行这个实例的任意一个synchronized方法,都必须等待当前执行synchronized方法的线程退出此方法后,才能依次执行。
注意!非synchronized方法不受影响,不管当前有没有执行synchronized方法,非synchronized方法都可以被多个线程同时执行。
每一个对象都有一个锁标记,它标记是否有线程在使用该对象。对象有一个等待池,一个锁池。
对同一个对象的不同方法使用synchronized,他们判断的是同一个锁标记,就是对象的本身(this)的锁标记。
一个线程占用了对象的锁标记,其余的线程就得在该对象锁池中等待当前线程释放对象的锁。
Object的方法;wait()和notify()/notifyAll()实现线程之间的通讯。
wait()会使线程对象放弃CPU,并释放对象的锁。JVM会将该线程放到该对象的等待池中,等待其他线程将其唤醒this.notifyAll(),这时所有等待池中的线程都进入对象的锁池,等候锁标记。
wait()产生的效果:
1.让当前正在执行的线程释放掉a的锁标记。
2.让当前正在执行的线程进入a对象的等待池等待。
notify()会唤醒在对象的等待池中等待的一个线程,JVM从对象等待池中随机选择一个线程,把他转换到对象的锁池中,所以一般都用notifyAll()把等待池中所有的线程对象都转入到对象的锁池中。
调用wait()时,该线程必须占用了某个锁标记。也就是必须出现在某一个synchronized的代码块中,并且调用wait()方法的对象必须和synchronized中声明的对象是同一个对象。
例如:
//synchronized修饰的方法就表示使用当前对象的锁标记
public synchronized void f(){
…
this.wait();
…
}
I/O流
四个抽象类:InputStream、OutputStream、Reader、Writer
(所有的操作都是围绕着这四个抽象类展开的)
按方向可分为:输入流和输出流
按流的单位分为:字节流和字符流
字节流和符流是基础,是底层的实现(必须得有)!包装流是对它进行的上层的封装!
InputStream和OutputStream 字节流(最重要)
★FileInputStream/FileOutputStream(需要重点掌握)
调用read()、write()方法来读写文件。
(ByteArrayInputStream/ByteArrayOutputStream和PipedInputStream/PipedOutputStream只用了解)
如果构造FileOutputStream的同时磁盘会建立一个文件。如果创建的文件与磁盘上已有的文件名重名,就会发生覆盖。
如果不想覆盖,则用FileOutputStream(String filename,boolean append),
Reader和Writer 字符流
FileReader、FileWriter可以直接用文件名得到一个对文件的字符输入、输出流对象。但是其实质就是封装了FileInputStream和FileOutputStream并指定默认的字符集!!!
如果想要自己制定字符集,则要用InputStreamReader和OutputStreamWriter来封装字节流~
InputStreamReader(InputStream in, String charsetName)
OutputStreamWriter(OutputStream out, String charsetName)
☆BufferedReader有readLine()使得字符输入更加方便。
在I/O 流中,所有输入方法都是阻塞方法。 都会导致线程阻塞。
在Java中利用控制台输入:
BufferedReader br=new BufferedReader(
new InputStreamReader(System.in)
);
String s=br.readLine();
BufferedWriter 它的作用就是给输出字符流加缓冲
☆PrintWriter 它可以直接使用字节流对象构造,而不用加上OutputStreamWriter()桥转换这一步。而且提供了很多方便的方法。注:他也是带缓冲的!是最常用的输出流。
最常用的字符流就是BufferedReader和PrintWriter!
包装流
包装流的作用就是在原有的底层流的基础上,添加更多的功能,让它更为适用。底层的字节流、字符流才是基础和核心。
缓冲流
BufferedInputStream / BufferedOutputStream 用来包装底层的字节流
BufferedReader / BufferedWriter(PrintWriter) 用来包装底层的字符流
底层的字节流、字符流每次读写都去访问磁盘,内存的读写快,磁盘的读写慢。根据“木桶原则”,程序运行的效率取决于效率最低的部分。频繁的访问磁盘会致程序效率下降。而如果增加了缓冲流,可以减少访问磁盘的次数,不必每次都去访问磁盘。提高了效率!
例如:用BufferedInputStream和BufferedOutputStream,包装底层的字节流(用文件流实现)
FileInputStream in=new FileInputStream(“file1.txt”);
//用缓冲流包装底层的FileInputStream,以提高效率。
BufferedInputStream bin= new BufferedInputStream(in,256);
用 BufferedReader和 BufferedWriter 来包装字符流方法同上。
使用缓冲流时要注意:只有缓冲区满时,才会将数据送到输出流。让磁盘一次读写多个数据。可是如果缓冲区未满,而对方要马上得到数据时,需要人为地将尚未填满的缓冲区中的数据送出,使用flush()方法,或者关闭流close()也会马上清空缓冲区。
专门针对字节流的包装类
DataInputStream / DataOutputStream: 这两个类不仅能读写字节数据流,还可以直接对八种基本类型和String类型进行读写。 比底层的read()、write()方便。
★ObjectInputStream / ObjectOutputStream:对象的持久化。可以直接读出、写入对象类型的数据。(重点掌握)
相关问题:对象序列化(也叫串行化)
把对象作为一个整体在I/O流中传播,这个过程叫做对象的序列化。
对象的寿命通常是随着生成该对象的程序的终止而终止,在有些情况下,需要将对象的状态保存下来writeObject(),然后在必要的时候将对象恢复readObject()。值得注意的是,如果对象有一个成员是另一个对象的引用,则必需让该对象可序列化,或者用transient来修饰该类型的属性,这样序列化对象时会忽略掉transient的属性。
重点注意:
用readObject()读文件时,读到文件末尾并不会返回一个null!而是会抛出一个EOFException!
while(true){
Object obj=in.readObject();//可能会抛异常
System.out.println(obj);
}
}catch(EOFException e){
}
当捕捉到该异常时,就是文件结束。用异常打断无限循环!
关于serialVersionUID:
From JDK5.0:
序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID:
static final long serialVersionUID = 42L;
接口java.io.Serializable是实现对象串行化的工具,实现Serializable接口表示该对象可被串行化。Serializable接口中没有任何的方法,声明“implements Serializable”只是表明该类遵循串行化协议,而不需要实现任何特殊的方法。(标记接口)
对象的序列化与深拷贝
class Student implements Serializable{
…
public Object clone(){
Object obj=null;
try{
//字节流用ByteArrayXXXStream实现,再包装成对象流!效率高!
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(this);
oos.close()
ByteArrayInputStream bais=new ByteArrayInputStream();
ObjectInputStream ois=new ObjectInputSteam(bais);
obj=ois.readObject();
ois.close();
}catch(Exception e){
…
}
return obj;
}
}
RondomAccessFile 类允许随机访问文件同时拥有读和写的功能。
即用一个对象来控制读和写!
GetFilepointer()可以知道文件中的指针位置。
seek()定位文件指针。
Mode(“r”:随机读;”w”:随机写;”rw”:随机读写)
RandomAccessFile raf=new RandomAccessFile(“filename”,“rw”);
raf.seek(raf.getFilePointer()-1);
raf.write()/read();
桥连接
InputStreamReader / OutputStreamWriter
作用是将字节流对象包装成为字符流对象。
尤其常用于网络传输上。因为我们通过Socket得到的都是字节流!!!
s.getInputStream() / s.getOutputStream
如果我们要发送一个字符串,就得设法封装出一个字符流。办法就是:
BufferedReader br=new BufferedReader(
new InputStreamReader(
s.getInputStream())
);
PrintWriter pw=new PrintWriter(
s.getOutputStream()
);
PrintWriter可以直接接受一个OutputStream类型的参数来构造对象!(很少使用BufferedWriter,因为PrintWriter用起来更方便)
问题总结
class ReferenceTest{
public static void main(String[] args){
Object obj = null; //必须得初始化为null么?
//System.out.println(obj);
obj=new String(“hello”);
System.out.println(obj);
}
}
重量级:线程安全、效率低、可以让不同线程共享使用
轻量级:线程不安全、效率高、只能在单线程环境下使用
重量级:StringBuffer Vector Hashtable
轻量级:StringBuilder ArrayList HashMap
Java中多维数组为什么可以每一维长度不等?
成员内部类中不能定义静态成员。
静态内部类中可以定义静态成员。
Why???
类加载
在磁盘上寻找字节码文件,并把该文件通过I/O流读入到JVM的过程。
类加载的效果就是在JVM中生成了 一个 该类类型的 类对象!(先考虑下如何在以后得到该对象)
类加载寻找字节码文件的顺序:
1.当前包下
2.jre/lib/xx.jar
3.CLASSPATH
第一次发生以下情况时,JVM会加载类:
1.生成类的对象(加载子类时,必定会先加载父类)
2.访问类的静态成员(包括属性、方法和静态初始化代码块)
3.Class.forName(“类名”)
类加载执行的原则就是:能少加载就少加载,能不加载就不加载!!!
例如:用子类名调用父类的静态成员,就只会加载父类,而不加载子类!
弄清楚这个问题之后,在考虑下:
外部类的成员方法中可以定义成员内部类的对象么?
反射的使用(关键要理解类对象)
类对象的作用:描述的一个类的结构!(与任何对象无关)
类的对象 & 类的类对象
动态初始化代码块 & 静态初始化代码块
为什么判断两个对象是否是同一种类型可以用.getClass()和==呢?
HashSet<Integer> hs1=new new HashSet<Integer>();
// 得到类对象的方法有三:
Class c=hs1.getClass();
// Class c=HashSet.class;
// Class c=Class.forName(“java.util.HashSet”);
// 对于构造方法私有的类,仍然可以利用类对象得到类的实例
//c.getInstance();
// Constructor con=
c.getDeclaredConstructor(Class…);
// con.newInstance(Object…);
// 具体的突破访问限制,操作数据和方法的代码
Method m=
c.getDeclaredMethod(“add”,Object.class);//???
//m.setAccessible(true);
m.invoke(hs1 ,new Student());
Field f=c.getDeclaredMethod(“data”);
//f.setAccessible(true);
Object obj=f.get(hs1);
f.set(hs1 , XXX);
理解:
以往的程序:对象 . 属性/方法
反射的程序:属性/方法 . 对象
通过类的对象访问该类的属性/方法,在编译的时候就会检查访问权限!!!反过来,通过类的属性/方法来寻找所属的对象,就能打破封装!