多线程
1. 线程
概述
线程和进程的关系- 进程:正在进行中的程序(直译)
- 线程:就是进程中一个负责程序执行的控制单元(独立执行的路径)
- 一个进程中至少要有一个线程
- 开启多个线程是为了同时运行多部分代码
- 每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务
多线程的利弊- 多线程好处:解决了多部分同时运行的问题,即同步问题
- 多线程弊端:线程太多导致效率降低
- 其实应用程序的执行都是cpu在做着快速的切换完成的,这个切换是随机的
普通方法的调用和线程开启的区别

线程的生命周期

- 新建状态
- 就绪状态
- 运行状态
- 阻塞状态
- 线程调用 sleep() 方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞 (join)
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
- 线程在等待某个通知(wait)
- 程序调用了线程的 suspend() 方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法
- 死亡状态
2. 线程的创建方式

继承Thread类- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用:避免OOP(面向对象)单继承局限性
public class TestThread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 200; i++) {System.out.println("正在run!" + i);}}public static void main(String[] args) {TestThread01 testThread01 = new TestThread01();testThread01.start();for (int i = 0; i < 1000; i++) {System.out.println("正在main!" + i);}}}
实现Runnable接口- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
public class TestThread02 implements Runnable {@Overridepublic void run() {for (int i = 0; i < 200; i++) {System.out.println("正在run" + i);}}public static void main(String[] args) {TestThread02 testThread02 = new TestThread02();new Thread(testThread02).start();for (int i = 0; i < 1000; i++) {System.out.println("正在main" + i);}}}
实现Callable接口(了解即可)- 通过 FutureTask 类构建 Callable 对象
- Thread 建立时引用 FutureTask 对象
- 通过 FutureTask.get() 方法调用 call() 线程执行体
- call() 方法可以有返回值,方法可以抛出异常
import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class CallableTest {public static void main(String[] args) throws ExecutionException, InterruptedException {People people = new People();FutureTask futureTask = new FutureTask(people); // 适配器new Thread(futureTask).start();Object o = futureTask.get(); // 这个get()方法可能会阻塞,一般放在最后// 或者使用异步通信System.out.println(o);}}class People implements Callable<String> {public String call() {return "人";}}
3. 线程的安全问题
如何解决?
使用顺序:Lock>同步代码块>同步方法
synchronized 同步代码块
synchronized (锁对象){ //锁的是增删改查的对象//要处理的代码}
注意:
通过代码块中的锁对象,可以使用任意的对象,比如Object对象
但是必须保证多个线程使用的锁对象是同一个
锁对象的作用:把同步代码锁住,只让一个线程在同步代码块中执行
synchronized 同步方法
public synchronized void methodName(){ //同步方法锁的是当前对象//要处理的代码}public static synchronized void methodName(){ //静态同步方法锁的是当前类//要处理的代码}
Lock 锁(常用)
公平锁:使等待的线程有序的被执行,遵循先进先出原则
Lock lock = new ReentrantLock(); //创建一个ReentrantLock对象public void methodName(){lock.lock(); //加锁try{//要处理的代码}catch(Exception e){e.printStackTrace}finally{lock.unlock(); //释放锁}}
synchronized 和 Lock 的区别
- synchronized 是 Java 关键字,在 JVM 层面实现加锁和解锁;Lock 是一个接口,在代码层面实现加锁和解锁
- synchronized 可以用在代码块、方法上;Lock 只能写在代码里
- synchronized 在代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要在 finally 中显示释放锁
- synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 得知加锁是否成功
- synchronized 锁可重入、不可中断、非公平;Lock 锁可重入、可中断、可公平 / 不公平,并且可以细分读写锁提高效率
4. Lambda表达式
Lambda使用的注意事项:
- 必须具有接口,且要求接口中有且仅有一个抽象方法,这种接口也称,函数式接口
- 如果接口的方法中有多行代码,那么就用代码块包裹
- 接口方法中,多个参数也可以去掉参数类型,但必须加上括号
标准的Lambda表达式:
//接口public interface Runnable{public abstract void run();}public class Demo implements Runnable{public static void main(String[] args) {//Lambda表达式Demo demo = new Demo(()->{System.out.println("");});}}
5. 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
6. 线程之间的通信
通信方式
- wait()、notify()、notifyAll()
- 这三个方法是 Object 类中的方法,都是本地方法,且都是 final 修饰,无法重写
- 这三个方法用于同步代码块或同步方法中
- wait():执行该方法,线程进入阻塞状态,并释放同步锁
- notify():唤醒被 wait 的一个线程,使其进入就绪状态,如果有多个线程被 wait,则唤醒优先级高的线程
- notifyAll():唤醒所有被 wait 的线程
- await()、signal()、siganlAll()
- 这三个方法是用于 lock 锁的通信,它们属于 condition 接口的方法。condition 接口依赖于 lock 接口,需要 lock.newCondtion 来创建
- 这三个方法跟 wait()、notify()、notifyAll() 效果一样
- 阻塞队列
面试题
sleep() 和 wait() 的区别
- 方法声明位置不同:
- sleep() 是 Thread 类中的静态方法
- wait() 是 Object 类中的成员方法
- 调用的范围不同:
- sleep() 可以在任何地方调用
- wait() 必须在同步代码块或同步方法下使用
- 是否释放同步锁:
- sleep() 不会释放锁
- wait() 会释放锁,且需要通过 notify() 或 notifyAll() 来重新获得锁
7. 线程池
经常创建和销毁线程,特别是并发情况下的线程,对性能影响很大。线程池的思路就是提前创建好多个线程,放入池中,使用直接取,用完放回池中。
优点
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
线程池的工作流程

线程池创建方式
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class TestThreadPool {public static void main(String[] args) {//使用工厂类Executors里的静态方法newFixedThreadPool生产一个指定线程数量的线程池//返回一个ExecutorService对象ExecutorService es = Executors.newFixedThreadPool(2);es.submit(new Demo(), "A"); //submit方法,可以一直开启线程池,使用完线程,会自动把线程归还es.submit(new Demo(), "B"); // 用于Callable// es.execute(Runnable runnable);es.shutdown(); // 关闭连接池}}class Demo implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "创建了一个线程");}}
