第十六章:多线程
1、一般而言,进程包含如下3个特征:独立性,动态性,并发性。并发性和并行性是两个概念,并行指同一时刻,有多条指令在多个处理器上同时执行;并发指同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行。一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。
多线程的优势:①进程之间不能共享内存,但线程之间共享内存非常容易;②系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;③Java语言内置了多线程功能支持。
线程的创建和启动:
方法1:继承Thread类创建线程类,步骤:①定义Thread类的子类,并重写该类的run()方法,该run方法的方法体就代表了线程需要完成的任务,因此把run方法称为线程执行体;②创建Thread子类的实例,即创建了线程对象;③调用线程的start()方法来启动。
public class FirstThread1 extends Thread{
private int i;
@Override
public void run(){
for(;i<100;i++){//当线程继承Thread类,直接使用this即可获得当前线程
System.out.println(this.getName()+" "+i);}}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//调用Thread的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==30){new FirstThread1().start();
//创建并启动第一个线程
new FirstThread1().start();//创建并启动第二个线程
}}}
运行效果:主线程main运行到30(或之后)时,会启动两个线程,然后三个线程一起运行。顺序未知。主线程是必须会有的。Thread.currentThread()是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。getName()返回该方法的线程名字。
程序可以通过setName(String name)方法来为线程设置名字。
方法2:实现Runnable接口创建线程类:步骤:①定义Runnable接口的实现类,并重写该接口的run()方法;②创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。③调用线程对象的start方法启动线程。
public class SecondThread2 implements Runnable{
private int i;
@Override
public void run(){
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);}}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if (i==10) {
SecondThread2 thread2=new SecondThread2();//通过new Thread(target,name)方法创建新线程
new Thread(thread2,"这是线程1的名字").start();
new Thread(thread2,"线程2的名字").start();}}}
方法3:使用Callable和Future创建线程:Callable提供了call方法可以作为线程执行体,可以有返回值,可以抛出异常。Java 5提供了Future接口来代表Callable接口里的call方法的返回值,并为Future接口提供了FutureTask实现类:①V get():返回Callable任务里call方法的返回值,调用该方法导致线程阻塞,必须等到子线程结束后才会得到返回值。②V get(long timeout,TimeUnit unit):返回Callable任务里call返回值,该方法让程序最多阻塞timeout和unit指定的时间,如果超时,则抛出异常。③boolean isCancelled():如果Callable任务正常完成前被取消,返true;④boolean isDone()如果Callable任务已经完成,返回true。
创建并启动有返回值的线程的步骤如下:
①创建Callable接口的实现类,并实现call方法;②使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值;③使用FutureTask对象作为Thread对象的target创建并启动新线程;④调用FutureTask对象的get方法来获得子线程执行结束后的返回值。
public void doThing()throws Exception{
FutureTask<Integer> fTask=new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("子线程在运行:"+Thread.currentThread().getName());
int sum=2+3;return sum;}});
Thread thread=new Thread(fTask,"子线程名字-扁蛋");
thread.start();
System.out.println("主线程在执行任务");
System.out.println("task运行结果:"+fTask.get());}
创建线程的三种方式对比:通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已,因此可以归为一类。继承Thread类的优势是编写简单,无须使用Thread.currentThread()方法,直接使用this即可获得当前线程,劣势是不能再继承其它父类。因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
线程的生命周期:
新建、就绪、运行、阻塞和死亡5种状态。程序使用new创建线程后,处于新建状态,调用start()方法后进入就绪状态,何时运行取决于JVM里线程调度。启动线程使用start()方法而不是run方法,run方法是普通的方法,在run方法没有执行完之前其它线程无法并行执行。因此不要调用线程的run方法。
提示:如果希望调用子线程的start方法后子线程立即开始执行,程序可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠1毫秒。
被阻塞的线程在合适的时候重新进入就绪状态,而不是运行状态。线程会以如下3个方式结束:①run()或者call()方法执行完成,线程正常结束;②线程抛出一个未捕获的Exception或Error;③直接调用线程的stop()方法——该方法容易导致死锁,通常不推荐使用。
线程的suspend()方法将线程挂起,但容易造成死锁,应该少用。
判断某个线程是否已经死亡:调用该线程的isAlive()方法,处于就绪、运行、阻塞三种状态的返回true,新建、死亡的返回false。
如何控制线程?
join线程:Thread提供了一个让线程等待另一个线程完成的方法——join()方法。当某个线程执行过程中调用其它线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完毕。thread.interrupt();//中断线程
join()方法有如下3种重载形式:①join()等待join的线程执行完毕;②join(long millis):等待被join的线程的时间最长为millis毫秒,还没结束的话,将不再等待;③join(long millis,int nanos):等待最长millis毫秒加nanos毫秒。(不推荐第三种)
后台线程:运行在后台,主要为其它线程提供服务,用法:在新建线程后,直接设置为后台线程,再调用start方法,否则异常。
线程睡眠:sleep(long millis),当前线程睡眠这么多毫秒时间里,该线程不会获得执行的机会。
线程让步:yield():它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转为就绪状态,完全可能的情况是:当某个线程调用了yield方法后,线程调度器又将其调度出来重新执行。实际上,当某个线程调用yield方法暂停之后,只有优先级与当前线程相同或者比它高的处于就绪状态的线程才会被执行。
对比sleep和yield两个方法:sleep比yield方法有更好的移植性,通常不建议使用yield方法来控制并发线程的执行。
改变线程优先级:Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。
其中int newPriority参数可以是一个整数,范围是1-10之间,也可以使用Thread类的三个静态常量:
①MAX_PRIORITY:其值是10;②MIN_PRIORITY:其值是1;③NORM_PRIORITY值是5。数值越大,优先级越高。(线程,类似前程,数值越高即钱越高,优先级别越大。)通常推荐使用静态常量,有更好的移植性。
线程同步:
①同步代码块:synchronized关键字来修饰某个方法,该方法称为同步方法。
public synchronized void draw(double drawAmount)
☞不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。
②同步锁:Java 5开始就提供了功能更加强大的线程同步机制——通过显示定义同步锁对象来实现同步,使用Lock。Lock提供了比synchronized方法和代码块更加广泛的锁定操作。
private final ReentrantLock lock=new ReentrantLock();
public void method(){lock.lock();//加锁
try {} catch (Exception e){}
finally{lock.unlock();//解锁
}
线程池:系统启动一个新的线程的成本是比较高的,因此使用线程池可以很好的提高性能。与数据库连接池类似,线程池在系统启动过程即创建大量的空闲线程,当run或者call方法执行结束后,该线程并不会死亡,而是再次返回线程池中称为空闲状态,等待执行下一个Runnable对象的run或者call方法。
使用线程池来执行线程任务的步骤如下:
①调用Executors类的静态工厂方法来创建一个ExecutorService 对象,该对象代表一个线程池;②创建Runnable实现类或者Callable实现类的实例,作为线程执行任务;③调用ExecutorService对象的submit()方法来提交Runnable实例;④当不想提交任何任务时,调用shutdown()方法关闭线程池。
ExecutorService pool=Executors.newFixedThreadPool(5);
Runnable target=new Runnable() {
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" i的值是:"+i);
}}};
pool.submit(target);
pool.submit(target);
pool.shutdown();
线程相关的类ThreadLocal类,是线程安全的工具类。通过使用ThreadLocal类可以简化多线程编程时的并发访问,可以简洁的隔离多线程程序的竞争资源。ThreadLocal类的用法非常简单,只有三个public方法:①T get():返回此线程局部变量中当前线程副本中的值;②void remove():删除此线程局部变量中当前线程的值;③void set(T value):设置此线程局部变量中当前线程副本中的值。
在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。通常建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
用法:
static ThreadLocal<HashMap> map0 = new ThreadLocal<HashMap>(){
@Override
protected HashMap initialValue() {
System.out.println(Thread.currentThread().getName()+"initialValue");
return new HashMap(); }};
包装线程不安全的集合:比如ArrayList、LinkedList、HashSet、TreeSet、HashMap等都是线程不安全的,如果有多个线程访问这些元素,可以使用Collections提供的类方法把这些集合包装成线程安全的集合。这个跟之前的集合那章节的方法一样!
同步控制:
Collections类提供了多个synchronizedXXX()方法,该方法将指定集合包装成线程安全的集合,可以解决多线程并发访问集合元素时的安全性问题。Java中常用到的集合框架中的HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不安全的,如果有多个线程访问它们,而且有超过一个线程试图修改它们,则存在线程安全问题。例子:
Collection c1=Collections.synchronizedCollection(new ArrayList<>());
Collection c2=Collections.synchronizedCollection(new HashSet<>());
List list2=Collections.synchronizedList(new ArrayList<>());
Set set=Collections.synchronizedSet(new HashSet<>());
Map map=Collections.synchronizedMap(new HashMap());
注:不难发现,前面类名都是跟后面synchronizedXXX的XXX是相同的。
第十七章 网络编程
IP地址分为5类ABCDE,
A类:10.0.0.0~10.255.255.255;
B类:172.16.0.0~172.31.255.255;
C类:192.168.0.0~192.168.255.255
公认端口:0~1023;
注册端口:1024~49151,应用程序通常使用这些端口;
动态和/或私有端口:49152~65535,应用程序一般不会主动使用这些端口。
使用InetAddress:InetAddress没有提供构造器,而是提供两个静态方法来获取InetAddress实例:①getByName(String host):根据主机获取对应的InetAddress对象;②getByAddress(byte[] addr):根据原始IP地址获取对应的InetAddress对象。InetAddress还提供了3个方法来获取InetAddress实例对应的IP地址和主机名:①String getCanoicalHostName():获取此IP地址的全限定域名;②String getHostAddress():返回此InetAddress实例对应的IP地址和字符串;③String getHostName():获取此IP地址的主机名。InetAddress还提供了一个getLocalHost()方法来获取本机IP地址和InetAddress实例。InetAddress还提供了isReachable()方法来测试是否可以到达该地址。
InetAddress ip=InetAddress.getByName("www.crazyit.org");
System.out.println("是否可以到达某网站:"+ip.isReachable(2000));
System.out.println(ip.getHostAddress());
InetAddress local=InetAddress.getByAddress(new byte[] {127,0,0,1});
System.out.println("本机是否可达:"+local.isReachable(2000));
System.out.println(local.getCanonicalHostName());
输出:是否可以到达某网站:true 222.73.85.205 本机是否可达:true 127.0.0.1
使用ServerSocket创建TCP服务端:ServerSocket对象用于监听来自客户端的Socket请求,如果没有连接,将一直处于等待状态:①Socket accept():如果接收到一个客户端Socket请求,将返回一个与客户端Socket对应的Socket。ServerSocket提供了如下几个构造器:①Socket accept(int port):用指定端口port来创建一个ServerSocket,有效值0-65535。当ServerSocket使用完毕后,应使用close方法来关闭该ServerSocket。
IP地址是:127.0.0.1一般代表本机的IP地址。Socket提供了2个方法来获取输入流和输出流:InputStream getInputStream和OutputStream getOutputStream。Socket对象提供了一个setSoTimeout(int millis)的超时连接。
Socket通信例子
客户端:Socket conToServer=new Socket("localhost",5500);
//数据输入流:
DataInputStream inFromSer=new DataInputStream(conToServer.getInputStream());
//数据输出流:
DataOutputStream outToSer=new DataOutputStream(conToServer.getOutputStream());
服务端:ServerSocket serSock=new ServerSocket(5500);
//侦听来自客户端的连接请求
Socket conFromCli=serSock.accept();
//接收数据:
DataInputStream inFromCli=new DataInputStream(conFromCli.getInputStream());
使用DatagramSocket发送、接收数据:DatagramSocket构造器有3个:①DatagramSocket()无参数构造器表示绑定到本机默认IP地址、本机的所有可以用的随机端口;②DatagramSocket(int port)③DatagramSocket(int port,InetAddress add),都很好理解。通过如下两个方法来接收和发送数据:receive(DatagramSocket p); send(DatagramSocket p)。
使用代理服务器:
代理服务器的功能就是代理用户去取得网络信息,浏览器不是直接去向Web服务器发送请求,而是向代理服务器发送请求。代理服务器有2个好处:①突破自身IP限制,对外隐藏自身IP地址,包括访问国外受限站点,访问国内特定单位、团体的内部资源;②提高访问速度,代理服务器提供缓存功能可以避免用户直接访问远程主机,从而提高客户端的访问速度。
第十八章 类加载机制与反射
JVM进程终止的原因有以下几种:
①程序运行到最后正常结束;
②程序使用了System.exit()或Runtime.getRuntime().exit()结束程序;
③程序执行过程遇到未知的异常或错误;④程序所在的平台强制结束了JVM进程。
通过反射查看类信息:
instanceof关键字能够进行判断是否是属于某个类。
在Java程序中获得Class对象通常有3种形式:
①使用Class类的forName(String clazzName)静态方法,传入的字符串是某个类的全限定名包括包名。
②调用某个类的class属性来获取该类的Class对象,例如Person.class将返回Person类对应的Class对象;
③调用某个对象的getClass()方法,所有的对象都可以调用该方法,该方法返回该对象所属类对应的Class对象。
对比:第二种方法有两种优势,代码更安全和程序性能更好,因为无须调用方法,所以性能更好。
1、获取类的构造器:Connstructor<T>getConstructor...有4种方法,不一一介绍。
2、获取Class对应类所包含的方法:Method []getMethod
3、访问成员变量:Field[] getFields()
4、获取所有注解:Annotation[] getAnnotations()
5、获取所有内部类:Class<?>[] getDeclaredClasses()
6、访问该Class对象对应类所实现的接口:Class<?>[] getInterfaces
7、访问所继承的父类:Class<? super T>getSuperclass()。
使用反射生成并操作对象:例子
public class Test8Class {
//定义一个对象池,前面是对象名,后面是实际对象
private Map<String, Object>objectPool=new HashMap<>();
//定义一个方法,只需要传入一个字符串的名字,程序就可以根据该类名生成Java对象
private Object createObject(String name) throws Exception{
Class<?>clazz=Class.forName(name);
return clazz.newInstance();}
//初始化对象池
public void initPool(String fileName){
try {
FileInputStream fis=new FileInputStream(fileName);
Properties props=new Properties();
props.load(fis);
for(String name:props.stringPropertyNames()){
//每次取出一对 key-value,就执行一次方法
objectPool.put(name, createObject(props.getProperty(name)));}} catch (Exception e) {e.printStackTrace();}}
//从对象池中取出指定的name对应的对象
public Object getObject(String name){return objectPool.get(name);}
public static void main(String[] args)throws Exception {
Test8Class t8=new Test8Class();
t8.initPool("WebRoot/obj.txt");
System.out.println(t8.getObject("name"));
System.out.println(t8.getObject("password"));}}
注:使用配置文件来配置对象,然后由程序根据配置文件来创建对象的方式非常有用,大名鼎鼎的Spring框架就是采用这种方式大大简化了JavaEE的开发。Spring采用的是XML的配置方式。