2020-04-05,首发,持续更新直至该章节学习结束。
一、Java多线程编程
1、继承Thread类实现多线程
Java中提供了一个java.lang.Thread的程序类,那么一个类只要继承了此类,就表示这个类为线程的主体类。(需要覆写run()方法,这个方法属于线程的主方法)
多线程要执行的功能都应该在run()方法中进行定义。需要说明的是,在正常情况下,如果要使用一个类中的方法,一定要产生实例化对象,调用类中提供的方法,但是run()方法是不能被直接调用的,这里牵扯到了操作系统的资源调度问题,所以要想启动多线程,必须使用start()方法完成(public void start())。
调用start()方法,最终执行的是run()方法,且是交替执行的,如果是直接只用run()方法,则是顺序执行的。
每一个线程只允许启动一次,否则会抛出异常。
在Java程序执行的过程中考虑到对于不同层次开发者的需求,所以其支持有本地的操作系统函数调用,而这项技术被称为JNI(Java Native Interface)技术,但是Java开发过程中并不推荐使用,利用这项技术可以使用一些操作系统提供的底层函数进行一些特殊的处理。
Thread的执行分析
任何情况下,只要定义了多线程,多线程的启动永远只有一种方案:Thread类中的start()方法。
2、Runnable接口实现多线程
此时由于不再继承Thread类了,所以不再支持start()这个继承的方法。此时的启动,在所提供的构造方法中了。
构造方法:public Thread(Runnable target);
此时的多线程的实现里可以发现,由于只是实现了Runnable接口对象,所以此时线程主体类上就不再有单继承的局限,是标准型的设计。
3、Thread与Runnable关系
从代码结构本身来讲,使用Runnable是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充。
但是从结构上,也需要观察两者的关系。
Thread类的定义:public class Thread extends Object implements Runnable{}
发现Thread类也是Runnable接口的子类,那么之前在继承Thread类的时候实际上是覆写的还是Runnable接口的run()方法。
多线程的设计之中,使用的代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现交由Thread类来处理。
在进行Thread启动线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,该接口对象将被Thread类中的targer属性所保存,在start()方法执行的时候会调用Thread类中的run()方法,而这个run()方法去调用Runnable接口子类覆写过的run()方法。
多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占。那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。
4、Callable实现多线程
从传统的开发来讲,如果要进行多线程的实现肯定是依靠Runnable,但是Runnable接口有一个缺点:当线程执行完毕之后,无法获取一个返回值。
接口定义:
@FunctionalInterface
public interface Callable<V>{
public V call() throws Exception;
}
Runnable与Callable的区别
- Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后;
- java.lang.Runnable接口之中只提供一个run()方法,并且没有返回值;
- java.until.concurrent.Callable接口中提供有call()方法,可以有返回值。
5、线程运行状态
对于多线程开发而言,编写程序的过程中总是按照:定义线程主体类,而后通过Thread类进行线程的启动,但是并不意味着你调用了start()方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行状态。
- 任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start(),但是启动的时候实际上是若干个线程都将进入就绪状态,并没有执行。
- 进入就绪状态后就需要等待进行资源调度,当某一个线程调度成功之后则进入到运行状态(run()方法)但是所有的线程不可能一直持续执行下去,中间需要产生一些暂停的状态。则进入阻塞状态,随后重新回归就绪状态。
- 当run()方法执行完毕之后,实际上该线程的主要任务也就结束了,此时就直接进入终止状态。
二、线程常用操作方法
1、线程的命名与取得
多线程的运行状态是不确定的,那么在程序开发中为了取得一些需要使用到的线程就只能够依靠线程的名字来进行操作。所以线程的名字是至关重要的概念。在Thread类之中就提供有线程名称的处理。
构造方法:public Thread(Runnable target,String name);
设置名字:public final void setName(String name);
取得名字:public final String getName();
对于线程对象的获取是不可能只依靠一个this来完成的,因为线程的状态不可控,但是有一点是明确的,所有的线程对象一定要执行run()方法,那么这个时候可以考虑获取当前线程,在Thread类里面提供了获取当前线程的方法:
获取当前线程:public static Thread currentThread();
在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将负责逻辑或比较耗时的逻辑交由线程处理。
主线程负责处理整体流程,而子线程负责耗时任务的处理。
2、线程休眠
休眠:public static void sleep(long millis) throws InterrupterException;//毫秒
休眠:public static void sleep(long millis,int nanos) throws InterrupterException;//毫秒,纳秒
在进行休眠的时候有可能产生中断异常“InterrupterException”,中断异常属于Exception的子类,所以该异常必须进行处理。
休眠的主特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在有很多线程对象,那休眠也是有先后顺序的。
3、线程中断
线程的休眠是可以被打断的,而这种打断是由其他线程完成的,在Thread类里提供有这种中断执行的处理方法:
判断线程是否被中断:public boolean isInterrupted();
中断线程执行:public void interrupt();
所有正在执行的线程都可以被中断的,中断线程必须进行异常的处理。
4、线程的强制执行
指当满足某些条件之后,某一个线程对象将可以一直独占资源,一直到该线程的程序执行结束。Thread类中提供了方法:
强制执行:public final void join() throws InterruptedException;
在进行强制执行的时候一定要获取强制执行线程对象之后才可以执行join()的调用。
5、线程礼让
指的是先将资源让出去给别的线程先执行。线程的礼让可以使用Thread类中提供的方法:
礼让:public static void yield();
礼让执行的时候每一次调用yield()方法都只会礼让一次当前的资源。
6、线程优先级
从理论上来讲,线程的优先级越高,越有可能先执行(越有可能先抢占到资源)。
设置优先级:public final void setPriority(int newPriority);
获取优先级:public final int getPriority();
在进行优先级定义的时候都是通过int型的数字来完成,而对于此数字的选择在Thread类里就定义有三个常量:
最高优先级:public static final int MAX_PRIORITY;(10)
中等优先级:public static final int NORM_PRIORITY;(5)
最低优先级:public static final int MIN_PRIORITY;(1)
主线程的优先级:5
默认线程优先级:5
三、线程的同步与死锁
在多线程的处理之中,可以利用Runnable描述多个线程操作的资源,而Thread描述每一个线程对象,当多个线程访问同一资源的时候,如果处理不当就会产生数据的错误操作。即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。(同步指的不是同时,而是协同步调)即多个工人使用一个房间的操作,不是同时进入房间,而是依次进入房间工作。
1、线程同步
解决同步问题的关键是锁,指的当某一个线程执行操作的时候,其他线程外面等待;使用synchronized关键字来实现,利用此关键字可以定义同步方法或者同步代码块,在同步代码块中的操作里面的代码只允许一个线程执行。
synchronized(同步对象){同步代码操作;}
一般要进行同步对象的处理的时候可以采用当前对象this进行同步。
加入同步处理之后,程序的整体性能下降了。同步实际上会造成性能的降低。
2、死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
若干个线程访问同一资源时一定要进行同步处理,而过多的同步会造成死锁。
3、线程的等待与唤醒
等待机制:
死等:public final void wait() throws InterruptedException;
设置等待时间:public final void wait(long timeout) throws InterruptedException;
设置等待时间:public final void wait(long timeout,int nanos) throws InterruptedException;
唤醒机制:
唤醒第一个等待线程:public final void notify();
唤醒全部等待线程:public final void notifyAll();//(哪个线程的优先级高就有可能先执行)
四、多线程深入话题
1、停止多线程
线程的启动使用的是Thread类中的start()方法,而对于线程的停止,Thread类原本提供了stop()方法,但是此方法JDK1.2的版本开始就已经废除了,不建议使用。 此外,除了stop()方法,还有几个方法也被禁用了:
停止多线程:public void stop();
销毁多线程:public void destroy();
挂起线程:public final void suspend();
恢复挂起线程:public final void resume();
之所以废除是因为这些方法有可能导致线程的死锁。
加入判断,调用sleep停止,通过判断进行停止多线程,更加“柔和”。
2、后台守护线程
多线程里面可以进行守护线程的定义,也就是说,如果现在主线程或其他线程还在执行的时候,那么守护线程将一直存在,并且运行在后台状态。
在Thread类中提供如下方法:
设置守护线程:public final void setDaemon(boolean on);
判断是否为守护线程:public final boolean isDaemon();
所有的守护线程都是围绕在用户线程的周围,如果程序执行完毕了,守护线程也就消失了,在JVM中,最大的守护线程就是GC线程。程序执行中GC线程会一直存在,程序执行完毕,GC线程也会消失。
3、volatile关键字
在多线程的定义中,volatile关键字主要是在属性定义上使用,表示此属性为直接数据操作,不进行副本的拷贝。
在正常进行变量处理的时候往往会经历如下几个步骤:
- 获取变量原有的数据内容;
- 利用副本为变量进行数学计算;
- 将计算后的变量,保存到原始空间中;
而如果一个属性追加了volatile关键字,表示的就是不使用副本,而是直接操作原始变量。相当于节约了拷贝副本放回的步骤。
volatile与synchronized的区别? - volatile主要在属性上使用,而synchronized是在代码块与方法上使用的;
- volatile无法描述同步的处理,它只是一种直接内存的处理,避免了副本的操作,而synchronized是实现同步的。
五、Java基础类
1、StringBuffer类
String类是在所有项目开发中一定会使用到的一个功能类,并且这个类拥有如下的特点:
- 每一个字符串的常量都属于一个String类的匿名对象,并且不可更改;
- String有两个常量池:静态常量池,运行时常量池;
- String类对象实例化建议使用直接赋值的形式完成,这样可以直接将对象保存在对象池之中以方便下次重用。
虽然String类很好使用,但是如果也存在很大的弊端:内容不允许修改,虽然大部分情况下都不会涉及到字符串内容的频繁修改,但依然可能存在这种情况,为了解决此问题,专门提供了一个StringBuffer类可以实现字符串内容的修改处理。
StringBuffer并不像String类那样拥有两种对象实例化方法,StringBuffer必须像普通类对象那样实例化后才可以调用方法执行处理,二此时可以考虑使用StringBuffer类中的如下方法:
构造方法:public StringBuffer();
构造方法:public StringBuffer(String str);//(接收初始化字符串内容)
数据追加:public StringBuffer append(数据类型 变量);//(相当于字符串中的“+”操作:这就意味中,String定义的常量中的“+”,在编译之后都变成了StringBuffer中的append()方法,并且在程序中StringBuffer与String类的对象可以直接转换)
实际上大部分情况下,很少出现有字符串内容的改变,这种改变指的并不是针对于静态常量池的改变。
String类对象变为StringBuffer可以依靠StringBuffer类的构造方法或者append()方法;
所有的类对象可以通过toString()方法变为String类型。
在StringBuffer类中处理可以支持有字符串内容的修改之外,实际上也提供了一些String类所不具备的方法:
插入数据:public StringBuffer insert(int offset,数据类型 b);
删除指定范围的数据:public StringBuffer delete(int start,int end);
字符串内容反转:public StringBuffer reverse();//(123变成321)
与StringBuffer类还有一个类似的功能类:StringBuilder类,这个类在JDK1.5提供的,该类中提供的方法与StringBuffer类相同,最大是区别在于StringBuffer类中的方法属性线程安全的,全部使用了synchronized关键字进行标注,StringBuilder属性非线程安全的。
解释String、StringBuffer、StringBuilder的区别:
- String类是字符串的首选类型,其最大的特点是内容不允许修改;
- StringBuffer与StringBuilder类的内容允许修改;
- StringBuffer是在JDK1.0的时候提供的,属性线程安全的操作;而StringBuilder是JDK1.5时提供的,属于非线程安全的操作。
2、CharSequence接口
CharSequence是一个描述字符串结构的接口,在这个接口中一般有三种常用的子类:
String类:public final class String extends Object implements Serializable, Comparable<String>, CharSequence
StringBuffer类:public final class StringBuffer extends Object implements Serializable, CharSequence
StringBuilder类:public final class StringBuilder extends Object implements Serializable, CharSequence
三个子类都能接收CharSequence,所有现在只要有字符串就可以为CharSequence接口实例化
CharSequence本身是一个接口,在该接口中也定义了如下操作:
获取指定索引字符:public char charAt(int index);
获取字符串的长度:public int length();
截取部分字符串:public CharSequence subSequence(int start,int end);
只要看见CharSequence描述的就是一个字符串。
3、AutoCloseable接口
AutoCloseable主要是用于日后进行资源开发的处理上,以实现资源的自动关闭(释放资源),例如文件、网络、数据库开发等资源的关闭。
AutoCloseable在JDK1.7的时候提出,并且该接口只有一个方法:
关闭方法:public void close() throws Exception;
要想实现自动关闭处理,除了要使用AutoCloseable之外,还需要结合异常处理语句才可以正常调用。
4、Runtime类
Runtime描述的是运行时的状态,也就是说在整个JVM中,Runtime是唯一一个于JVM运行状态有关的类,并且默认提供一个该类的实例化对象。
由于在每一个JVM进程里面只允许提供有一个Runtime类对象,所以这个类的构造方法被默认私有化了,证明该类使用的是单例设计模式,并且单例设计模式一定会提供一个static方法获取本类实例。
由于Runtime类属于单例设计模式,如果要想获取实例化对象,就可以依靠类中的getRuntime()方法完成:
获取实例化对象:public static Runtime getRuntime();
通过类中的availableProcessors()方法可以获取本机的CPU内核数。
除了以上方法,Runtime类中还提供了以下四个重要操作方法:
获取最大可用内存空间:public long maxMemory();
获取可用内存空间:public long totalMemory();
获取空闲内存空间:public long freeMemory();
手工进行GC处理:public void gc();
什么是GC?如何处理?
GC(Garbage Collector)垃圾收集器,是可以由系统自动调用的垃圾释放功能,或者使用Runtime中的gc()手工调用。
5、System类
System类是最常用的系统类,在System类中定义有一些其他的方法:
数组拷贝:public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length);
获取当前的日期时间数值:public static long currentTimeMillis();
进行垃圾回收:public static void gc();
在System中也提供了gc()方法,但是这个gc()方法不是重新定义了新方法,而是继续执行了Runtime类中的gc()操作(Runtime.getRuntime().gc();)
6、Cleaner类
Cleaner是在JDK1.9之后提供的一个对象清理操作,其主要功能是进行finalize()方法的替代。在C++语言中有两种特殊的函数:构造函数、析构函数(对象的手工回收),在Java中所有的垃圾空间都是通过GC自动回收的,所以很多情况下不需要使用析构函数,所以Java中没有提供这方法的支持。但是Java本身提供了给用户收尾的操作,每一个实例化对象在回收之前至少给它一个喘息的机会,最初实现对象收尾处理操作的是Object类中的finalize()方法:
@Deprecated(since="9")
protected void finalize() throws Throwable
该替换指的是不建议继续使用这个方法了,而是说子类可以继续使用这个方法名称而不报错。但是这个方法最大的特点是判处了一个Throwable异常类型,而这个异常类型分为两个子类型:Error、Exception,平常所处理的都是Exception。
但是JDK1.9开始,这一操作已经不建议使用了,而对于对象回收释放,从JDK1.9开始建议开发者使用AutoCloseable或者使用java.lang.ref.Cleaner类进行回收处理(Cleaner也支持有AutoCloseable处理)。