多线程是Java语言最为重要的特性之一,利用多线程技术可以提升单位时间内的程序处理性能,这也是现代程序开发中高并发的主要设计形式。
1.线程与进程
线程与进程的区别
以word为例,在使用word拼写文档时,word的自动校对功能就在工作,这就使自动校对线程一直在工作,当把word关掉时,就使关掉了word进程和自动校对线程,还表现出的是线程是在进程的基础上进行的。
虽然多进程可以提高硬件资源的利用率,但是进程的启动和销毁依然需要消耗大量的系统性能,导致程序的执行性能下降。
2.Java多线程实现
2.1 Thread类实现多线程
java.lang.Thread是一个负责线程操作的类,任何类只要继承了Thread类就可以成为一个线程的主类。同时线程类中需要明确覆写父类中的run()方法( public void run() ),当产生若干个线程类对象时,这些对象就会并发执行run()方法中的代码。
面试: 为什么线程启动的时候必须调用start()方法而不是直接调用run()方法?(书上有,忘了,不赘述)
简单来说,就是Java实现多线程的机制并没有表面上那么简单,在调用start()方法后会去进行操作系统函数调用,而使用run()方法无法实现操作系统的函数调用。run()接口按照我现在的理解来说他就是一个用来定义线程业务的方法,并没有对多线程的实现做出贡献。
2.2 Runnable接口实现多线程
使用Thread类的确可以方便地实现多线程,但是这种方法最大的缺点是单继承局限,所以Java提供了Runnable接口来实现多线程。
@FunctionalInterface //JDK1.8引入Lambda表达式后就变成了函数式接口
public interface Runnable{
public void run() ;
}
Runnable接口从JDK1.8开始成为了一个函数式接口,这样就可以在代码中直接利用Lambda表达式来实现线程主体代码,同时在该接口中提供有run()方法进行线程执行功能定义。
但是Runnable方法中没有start()方法,但是线程必须通过Thread类中的start()方法启动,所以为了继续使用Thread类中的start()方法,就得利用Thread类中的构造方法进行线程对象的包裹。
Thread类构造方法:public Thread(Runnable target)
2.3 Thread与Runnable区别
public class Thread extends Object implements Runnable {}
由Thread的定义可以看出,事实上,Thread是一个Runnable的子类。可以知道,继承Thread的时候实际上覆写的还是Runnable接口的run()方法。
由以上结构可以得出,作为Runnable的两个实现子类:Thread 负责资源调度,MytThread(自定义线程类)负责处理真是业务,这样的设计结构类似于代理设计模式。
2.4 Callable接口实现多线程
Callable接口解决了Runnable接口无法中run方法不能返回操作结果的缺陷。
@FunctionalInterface
public interface Callable<V>{
public V call() throws Exception;
}
Callable接口定义的时候可以设置一个泛型,此泛型的类型就是call()方法的返回数据类型,这样的好处是可以避免向下转型所带来的的安全隐患。
**范例:**定义线程主体类
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
for(int x = 0; x < 10; x ++){
System.out.println();
}
return "zhaohongyebest"; //返回结果
}
}
线程类定义完成后如果要进行多线程的启动依然需要通过Thread类实现,所以此时可以通过java.util.concurrent.FutureTask类实现Callable接口与Thread类之间的联系,并且也可以利用FutureTask类获取Callable接口中call()方法的返回值。
Callable接口的子类利用FutureTask类对象进行包装,由于FutureTask是Runnable接口的子类,所以可以利用Thread类的start()方法启动多线程,当线程执行完毕后,可以利用Future接口中的get()方法返回线程的执行结果。
解释Runnable 与 Callable 的区别
- Runnable在JDK1.1提出,而Callable在JDK1.5之后提出
- java.lang.Runnable接口中只提供有一个run方法,并且没有返回值
- java.util.Callable接口提供哟call()方法,可以有返回值(通过Future接口获取)
2.5 多线程运行状态
一共五种状态
创建状态: Thread thread = new Thread()
就绪状态: start()
运行状态: run()
阻塞状态: sleep()、suspend()、wait()
3.多线程常用操作
3.1 线程的命名和取得
方法 | 类型 | 描述 |
public Thread(Runnable target, String name) | 构造 | 实例化线程对象,接收Runnable接口子类对象,同时设置线程名称 |
public final void setName(String name) | 普通 | 设置/修改 线程名字 |
public final String getName() | 普通 | 取得名字 |
由于线程状态的不确定,线程的名字就成为唯一的分辨标记,所以在定义线程名称的时候一定要在线程启动前设置名字,而且尽量不要重名,尽量不要为已经启动线程修改名字。
由于线程的状态不确定,所以每次可以操作的都是正在执行run()方法的线程实例,依靠Thread类的以下方法实现
取得当前线程对象:public static Thread currentThread()
获取当前线程名称: Thread.currentThread().getName()
如果未为线程设置名称,系统会自动分配一个名称,名称的形式以“Thread-xxx”的方式出现
面试: 进程在哪里?
每一个JVM运行都是线程。
当用户使用java命令执行一个类的时候就表示启动了一个JVM的进程,而主方法是这个进程上的一个线程而已,而当一个类执行完毕后,此进程会自动消失。
3.2 线程休眠.
方法 | 类型 | 描述 |
public static void sleep(long millis) throws InterruptedException | 普通 | 设置线程休眠的毫秒数,时间一到自动唤醒 |
public static void sleep(long millis,int nanos) throws InterruptedException | 普通 | 设置线程休眠的毫秒数与纳秒数,时间一到自动唤醒 |
在进行休眠的时候有可能会产生中断异常InterruptedException,中断异常属于Exception的子类,程序中必须强制性进行该异常的捕获与处理
3.3 线程中断
方法 | 类型 | 描述 |
public boolean isInterrupted() | 普通 | 判断线程是否中断 |
public boolean Interrupted() | 普通 | 中断线程执行 |
isInterrupted :线程被中断返回ture,未被中断返回false
3.4 线程强制执行
在多线程并发执行中每一个线程对象都会交替执行,如果某个线程对象需要优先执行完成,则可以设置为强制执行,待其执行完毕后其他线程再继续执行,Thread类定义的线程强制执行方法如下:
public final void join() throws InterruptedException
3.5 线程礼让
现场礼让是指满足某些条件时,可以将当前的调度让给其他线程执行,自己再等待下次调度在执行,方法定义如下
public final void yield()
3.6 线程优先级
在Java线程操作中,所有的线程再运行钱都会保持在就绪状态,那么此时会根据线程的优先级进行资源调度,即那个线程的优先级高,那个线程就有可能会先被执行。
Thread类中支持一下的方法及常量
方法 | 类型 | 描述 |
public static final int MAX_PRIORITY | 常量 | 最高优先级,数值为10 |
public static final int NORM_PRIORITY | 常量 | 中等优先级,数值为5 |
public static final int MIN_PRIORITY | 常量 | 最低优先级,数值为1 |
public final void setPriority(int newPriority) | 方法 | 设置线程优先级 |
public final int getPriority() | 方法 | 取得线程优先级 |
所有线程的默认优先级都是中等优先级,5
4 线程的同步与死锁
4.1 线程同步
同步是指一个线程哟啊等待另一个线程执行完毕才会继续执行的一种操作形式
线程同步是指若干个线程对象并行进行资源访问是实现的资源处理的保护操作。
Java中提供synchronized关键字实现同步处理,同步的关键是要为代码加上锁,而对于锁操作程序有两种:同步代码块、同步方法
4.2 线程死锁
过多的使用同步会出现死锁的情况,通俗的说就是多个线程都在等待对方执行完毕。
5 案例:生产者与消费者
5.1 Object线程等待与唤醒
方法 | 类型 | 描述 |
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() | 普通 | 唤醒全部等待线程 |
利用 wait()和notify()的配合可以实现有序的线程运行,例如有序的输出一些值,如:12a34b…
6.优雅的停止线程
之所以叫优雅的停止线程,是因为JDK中有提供停止线程的方法,例如suspend()、resume()、stop(),但是在1.2版本后就不再推荐使用了,因为会造成死锁。
为了避免出现停止线程时的死锁,可以引入一个参数flag作为参照,用flag的值控制线程是否运行。
说的很模糊,不清楚到时候自己翻书,不再赘述,累了。
7.后台守护线程
方法 | 类型 | 描述 |
public final void setDaemon(boolean on) | 普通 | 设置为守护线程 |
public final boolean isDaemon() | 普通 | 判断是否为守护线程 |
Java线程分两类:用户线程和守护线程。守护线程是一种运行在后台的线程服务线程,当用户线程存在时,守护线程也可以同时存在;当用户线程全部消失(程序执行完毕,JVM进程结束)时守护线程消失。
用户线程就是用户自己开发或者由系统分配的主线程,其处理的是核心功能,守护线程就像是用户线程的保镖一样,如果用户线程消失,守护线程就没有存在的意义了。
8.volatile关键字
在多线程编程中,若干个线程为了可以实现公共资源的操作,往往是复制相应变量的副本,待操作完成后在将此副本变量数据与原始变量进行同步处理。而volatile变量就可以实现不复制,直接对原始变量进行操作。
而在多线程时的不同步主要就是因为在多线程处理数据时会对公共资源进行相应数量的复制,从而导致数据信息的不对称。所以貌似感觉volatile可以实现同步处理。
注意: volatile与synchronized的区别
- volatile无法描述同步的处理,它只是一种直接内存的处理,避免了副本的操作,而synchronized是实现同步操作的关键字。
- volatile主要在属性上使用,而synchronized是在代码块与方法上使用