概念关系
线程 —> 进程 —> 程序
简单来说,一个进程由多个线程组成,进程是一个程序的一次执行过程
一般Java程序默认会有main主线程以及gc线程(垃圾回收),线程的执行顺序是由CPU来决定的
创建线程的四种方式
-
继承Thread类
线程类继承Thread类之后重写run方法,主类创建线程对象,再调用线程对象的start方法,启动线程,如果调用的是线程的run方法是直接执行该线程,如果调用start方法,是与主线程交替执行。
-
实现Runnable接口
线程类实现Runnable接口,重写run方法,主类创建Thread线程对象,实现Runnable接口的对象作为参数传递,再调用start方法,方便同一个对象被多个线程使用
多个线程同时使用会出现并发问题,线程不安全。
-
实现Callable接口
需要返回值类型,不是重写run方法,而是call方法,需要抛出异常
创建线程对象后需要创建执行服务ExecutorService对象,再执行提交,用服务对象的commit方法,需要传入线程对象作为参数,再通过get方法获取结果,最后执行shotdownNow方法关闭服务。
-
使用线程池
Lambda表达式
使用Lambda表达式可以避免内部类定义过多,代码看起来简洁,去掉一堆没有意义的代码,只留下核心的逻辑代码。
函数式接口:如果在一个接口中,只包含唯一一个抽象方法,就称之为函数式接口,可以通过lambda表达式来创建该接口的对象。
写法:在创建函数式接口时,不用再使用new关键字了,而是用 ()->{} 类似于箭头函数
可以简写,在只能有一行代码的情况下才能对其简化成一行,多行就用代码块包裹,可以去掉参数类型,在只有一个参数的情况下也可以去掉括号。
线程的状态
线程有五种状态:创建、就绪、运行、阻塞和死亡
流程图如下:
线程中有几个比较常用的方法
线程休眠:sleep() 使用sleep方法可以做网络延迟,放大问题的发生性
线程礼让:yield() 让当前正在执行的线程暂停,不阻塞,会让CPU重新调度,将线程从运行状态转为就绪状态。
线程插队:join() 让当前正在执行的线程停止,待到本线程执行完之后,再执行其他线程,其他线程会进到阻塞状态,相当于插队
停止线程可以用变量、循环来控制,线程只能启动一次,死亡后就不能再次启动了
线程的优先级
涉及到权重问题,默认的优先级是5,可以使用getPriority()获取优先级,setPriority(int xxx) 来改变优先级,优先级的设定在start()调度前,优先级低只是意味着获得调度的概率变低了,还是会被调用的。
守护线程(daemon)
线程分为用户现场和守护线程,虚拟机必须保证用户现场执行完毕,但又不用保证守护线程,也就是说,当用户线程执行完毕,主线程会结束,只要把Thread类的setDaemon设置为ture,就可以把该线程设置为守护线程。所有线程的getDaemon值默认是false,都是用户线程。
线程同步安全
并发:同一个对象被多个线程同时操作。
要保证线程同步安全,可以使用队列+锁(synchronized)
当一个线程获得对象的锁,独占资源,其他线程必须等待,使用完后再释放锁
但是会存在以下问题:
- 一个线程持有锁会导致其他需要此锁的线程挂起
- 性能问题,如果优先级高的线程等待优先级低的线程释放锁,会导致优先级倒置
同步方法和同步块
同步方法需要加synchronized 修饰方法,锁的是this
同步块需要用synchronized代码块包起来,需要传入锁的对象,这个对象一般是需要进行增删改操作的对象
死锁
两个线程各自等待对方释放资源,需要对方的锁,两个线程都会停止执行,当某一个同步块同时拥有两个以上对象的锁,可能会发生死锁问题。
达成死锁的条件:
-
互斥条件:一个资源每次只能被一个线程使用
-
请求与保持条件:一个线程因请求资源而阻塞,对获得的资源保持不放,即不会释放
-
不剥夺条件:线程以获得的资源,在未使用完之前不能强行剥夺
-
循环等待条件:若干线程之间形成一种头尾相连 的循环等待资源关系。
只要上诉条件中打破任意一个,就可以避免死锁的发生。
Lock锁
Lock是显式锁,需要手动关闭,使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。
使用顺序:Lock > 同步代码块 > 同步方法
线程通信
生产消费问题就是一个线程同步问题,生产者与消费者共享一个资源,两者相互依赖,互为条件,synchronized可以阻止并发更新同一个资源,实现了同步,不能同来实现不同线程之间的消息传递。
wait() 表示线程一直在等待,直到其他线程通知,会释放锁
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上所以调用wait()的线程
三个方法都只能在同步代码块或同步方法中使用,否则会抛异常
解决方法——管理法
利用缓冲区,生产者将生产好的数据放入缓冲区,消费者直接从缓冲区拿数据
线程池
可以提高性能,提前创建多个线程放入线程池,使用时可直接获取,用完再放回线程池中,不用再手动销毁。
好处:
- 提高响应速度,减少创建新线程的时间
- 降低资源消耗,可重复利用
- 便于线程管理