Java多线程
名词解释
程序(program)
- 是为完成特定任务、用某种语言编写的一组指令集合。简单而言:就是自己写的代码
进程(Process)
-
进程是指运行中的程序,比如启动迅雷时,就启动了一个进程,操作系统就会为该进程分配内存空间。
-
进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
线程(Thread)
- 线程由进程创建的,是进程的一个实体
- 一个进程可以拥有多个线程
单线程:同一个时刻,只允许执行一个线程
多线程:同一个时刻,可以执行多个线程
并发:同一个时刻,多个任务交替执行,造成一种 “貌似同时” 的错觉,简单的说,单核CPU实现的多任务就是并发
并行:同一个时刻,多个任务同时执行。多核CPU可以实现并行
一、创建线程的两种方式
方法一、继承 Thread 类,重写 run() 方法,调用 start() 开启线程
- 子类继承 Thread 类具备多线程能力
- 启动线程:子类对象.start();
- 不建议使用:避免OOP单继承局限性
注意:线程开启不一定立即执行,由CPU调度
public static void main(String[] args) {
//main线程为主线程
//创建一个线程对象
A a = new A(a);
//调用 start() 方法开启线程
a.start();
//说明:当main线程启动一个子线程 A,主线程不会阻塞,会继续执行
//这时主线程和子线程是并行交替执行
}
......
class A extends Thread {
/*
1.当一个类继承了 Thread 类,该类就可以当做线程使用
2.会重写 run 方法,写上自己的业务代码
3.run Thread 类 实现了 Runnable 接口的run方法
*/
//线程入口点
@Override
public void run() {
//线程体
super.run();
}
}
方法二、实现 Runnable 接口,重写 run() 方法,执行线程需要丢入 Runnable 接口的实现类。调用 start() 方法
- 实现接口 Runnable 具有多线程能力
- 启动线程:传入目标对象+Thread对象.start();
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
public static void main(String[] args) {
//main线程为主线程
//创建一个 Runnable 接口实现类对象
A a = new A();
//创建一个线程对象,通过线程对象来开启线程,也就是代理
Thread thread = new Thread(a);
//调用 start() 方法开启线程
thread.start(a);
//说明:当main线程启动一个子线程 A,主线程不会阻塞,会继续执行
//这时主线程和子线程是并行交替执行
}
......
class A implements Runnable {
/*
1.当一个类继承了 Thread 类,该类就可以当做线程使用
2.会重写 run 方法,写上自己的业务代码
3.run Thread 类 实现了 Runnable 接口的run方法
*/
//线程入口点
@Override
public void run() {
//线程体
super.run();
}
}
代理实例:
//一份资源
StartThread station = new StartThread();
//多个代理
new Thread(station,"小明").start();
new Thread(station,"小红").start();
new Thread(station,"老师").start();
静态代理(Proxy):为其他对象提供一个代理以控制对这个对象的访问。
- 真实对象和代理对象都要实现同一个接口
- 代理对象要代理真实角色
- 优点:
- 代理对象可以做很多真实对象做不了的事情
- 真实对象专注做自己的事情
//创建一个接口
public interface BuyCar {
public void buyCar();
}
//创建一个实现类
public class BuyCarImpl implements BuyCar {
@Override
public void buyCar() {
System.out.println("我要买车~~~啦啦啦");
}
}
//创建一个代理类
public class BuyCarProxy implements BuyCar{
private BuyCar buyCar;
//注意事final修饰的关键字 不可修改
//构造函数注入,需要被代理的对象
public BuyCarProxy(final BuyCar buyCar) {
this.buyCar = buyCar;
}
//静态代理- 的实现方式
@Override
public void buyCar() {
System.out.println("不贷款,全款!买车前的准备~~~");
buyCar.buyCar();
System.out.println("买完车了,出去浪~~~");
}
}
//客户端调用
public abstract class ProxyTest implements BuyCar {
public static void main(String[] args) {
System.out.println("-+-+-+正常调用-+-+-+");
BuyCar car=new BuyCarImpl();
car.buyCar();
System.out.println("-+-+-+使用静态代理-+-+-+");
BuyCar proxy=new BuyCarProxy(car);
proxy.buyCar();
}
}
Lambda表达式,也可称为闭包。类似于JavaScript中的闭包。
Lambda表达式在Java语言中引入了一个操作符“->”,该操作符被称为Lambda操作符或箭头操作符。它将Lambda分为两个部分:
- 左侧:指定了Lambda表达式需要的所有参数
- 右侧:制定了Lambda体,即Lambda表达式要执行的功能。
(parameters) -> expression
或
(parameters) ->{ statements; }
函数式接口
只包含一个抽象方法的接口,就称为函数式接口。我们可以通过Lambda表达式来创建该接口的实现对象。
我们可以在任意函数式接口上使用@FunctionalInterface注解,这样做可以用于检测它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。
public class TestLambda{
//3、静态内部类
static class Like2 implements ILike{
@Override
public void lambda()}{
System.out.println("I like lambda2");
}
}
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
like = new Like2();
like.lambda();
//4、局部内部类
class Like2 implements ILike{
@Override
public void lambda()}{
System.out.println("I like lambda3");
}
}
like = new Like3();
like.lambda();
//5、匿名内部类,没有类的名称,必须借助接口或者父类
like = new Like(){
@Override
public void lambda(){
System.out.println("I like lambda4");
}
};
like.lambda();
//6、用Lambda简化
like = ()->{
System.out.println("I like lambda5");
};
like.lambda();
}
}
//1、定义一个函数式接口
public interface ILike {
void lambda();
}
//2、实现外部类
class Like implements ILike{
@Override
public void lambda()}{
System.out.println("I like lambda");
}
}
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但无参数或多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
下面对每个语法格式的特征进行举例说明:
(1)语法格式一:无参,无返回值,Lambda体只需一条语句。如下:
public void test01(){
Runnable runnable=()-> System.out.println("Runnable 运行");
runnable.run();//结果:Runnable 运行
}
(2)语法格式二:Lambda需要一个参数,无返回值。如下:
public void test02(){
Consumer<String> consumer=(x)-> System.out.println(x);
consumer.accept("Hello Consumer");//结果:Hello Consumer
}
(3)语法格式三:Lambda只需要一个参数时,参数的小括号可以省略,如下:
public void test02(){
Consumer<String> consumer=x-> System.out.println(x);
consumer.accept("Hello Consumer");//结果:Hello Consumer
}
(4)语法格式四:Lambda需要两个参数,并且Lambda体中有多条语句。
public void test04(){
Comparator<Integer> com=(x, y)->{
System.out.println("函数式接口");
return Integer.compare(x,y);
};
System.out.println(com.compare(2,4));//结果:-1
}
(5)语法格式五:有两个以上参数,有返回值,若Lambda体中只有一条语句,return和大括号都可以省略不写
public void test05(){
Comparator<Integer> com=(x, y)-> Integer.compare(x,y);
System.out.println(com.compare(4,2));//结果:1
}
(6)Lambda表达式的参数列表的数据类型可以省略不写,因为JVM可以通过上下文推断出数据类型,即“类型推断”
public void test06(){
Comparator<Integer> com=(Integer x, Integer y)-> Integer.compare(x,y);
System.out.println(com.compare(4,2));//结果:1
}
总结:
- lambda表达式只能由一行代码的情况才能简化为一行,如果有多行,那么久用代码块包裹
- 前提是接口为函数式接口
- 多个参数也可以去掉参数类型,要去掉都去掉,必须加上括号
二、线程状态
1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
- (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
控制线程的方法
- setPriority(int):设置线程的优先级别,可选范围1-10,默认为5,优先级高代表抢到时间片的概率高
- static sleep(long):让当前的线程休眠指定毫秒数
- static yield():让当前线程直接放弃时间片返回就绪[很可能自投自抢的情况,比较浪费CPU资源,建议少用]
- join():让当前线程邀请调用方法的那个线程优先执行,在被邀请的线程执行结束之前当前线程一直处于阻塞状态,不再继续执行
- static activeCount():得到程序中所有活跃的线程:就绪+运行+阻塞
停止线程
- 不推荐使用JDK提供的 stop() 、destory() 方法[已废弃]
- 推荐线程自己停下来
- 建议使用一个标志位进行终止变量,当 flag == false ,则终止线程运行
public class TestStop implements Runnable{
//1、线程中定义线程体使用的标志
private boolean flag = true;
@Override
public void run(){
//2、线程体使用该标志
while(flag){
System.out.printf("run...Thread");
}
}
//3、对外提供方法改变标志
public void stop(){
this.flag = false;
}
}
线程休眠
sleep可以模拟网络延时,倒计时,放大问题的发生性等。
阻塞状态的线程 如何解除阻塞?
- sleep() : 睡眠的时间超时了,就自动解除
- join() : 被邀请的线程执行结束了,就自动解除
- await() : 门闩都被拔掉的时候,就解除阻塞
- wait() : 被其它线程notify()/notifyAll(),就解除阻塞
- interrupt() 能够在这些条件都没满足的情况下,直接唤醒
优先级取值范围
Java 线程优先级使用 1 ~ 10 的整数表示:
- 最低优先级 1:
Thread.MIN_PRIORITY
- 最高优先级 10:
Thread.MAX_PRIORITY
- 普通优先级 5:
Thread.NORM_PRIORITY
- 注意:
- 优先级的设定建议在 start() 调度之前
- 优先级低只是意味着获得调度的概率低,并不是优先级低就i不会被调度了,这都是看CPU的调度
守护(damon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 守护线程一般用于记录操作日志,监控内存,垃圾回收等待
实现线程同步的方式
通过synchronized关键字和lock锁两种方法来实现线程同步
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是对象本身,或者是class
public void test(){
//同步块,其中Obj为同步监视器,Obj可以时任何对象,但是推荐使用共享资源作为同步监视器
//synchronized 默认锁的是 this。锁的对象应该是变化的量,需要增删改
synchronized(obj){
System.out.println("===");
}
}
死锁:多个线程互相抱着对方需要的资源,然后形成僵持
java 死锁产生的四个必要条件:
- 1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。
synchronized和Lock对比:
- Lock是显式的(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,处理作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock > 同步代码块(已经进入方法体,分配相应的资源)> 同步方法(在方法体之外)
使用ReentrantLock(可重入锁)实现同步
-
lock()方法:上锁
-
unlock()方法:释放锁
-
trylock():synchronized 是不占用到手不罢休的,会一直试图占用下去。与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock方法。
-
public class test{ //定义一个 private final ReentrantLock lock = new ReentrantLock(); public void m(){ //上锁 lock.lock(); try{ //保证线程安全的代码 }catch(Exception e){ e.printStackTrace(); }finally{ //如果同步代码有异常,要将 unlock() 写入finially语句块中 lock.unlock(); } } }
使用Condition实现等待/通知
-
使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法
-
Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法
三、线程间的通信方式
Object类中相关的方法有notify方法和wait方法。因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
1.wait()
- wait()方法:让当前线程进入等待,并释放锁。
- wait(long)方法:让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将自动唤醒。
2.notify()
- notify()方法:让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,并从其他线程中唤醒其中一个继续执行。
- notifyAll()方法:让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,将唤醒所有等待状态的线程。
3.wait()与sleep()比较
- 当线程调用了wait()方法时,它会释放掉对象的锁。
- Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。
线程池
java通过Executors提供线程池
创建线程池,在构造一个新的线程池时,必须满足下面的条件:
- corePoolSize(线程池基本大小)必须大于或等于0
- maximumPoolSize(线程池最大大小)必须大于或等于1
- maximumPoolSize必须大于或等于corePoolSize
- keepAliveTime(线程存活保持时间)必须大于或等于0
- workQueue(任务队列)不能为空
- threadFactory(线程工厂)不能为空,默认为DefaultThreadFactory类
- handler(线程饱和策略)不能为空,默认策略为ThreadPoolExecutor.AbortPolicy