多线程
1. 线程
概述
线程和进程的关系- 进程:正在进行中的程序(直译)
- 线程:就是进程中一个负责程序执行的控制单元(独立执行的路径)
- 一个进程中至少要有一个线程
- 开启多个线程是为了同时运行多部分代码
- 每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务
多线程的利弊- 多线程好处:解决了多部分同时运行的问题,即同步问题
- 多线程弊端:线程太多导致效率降低
- 其实应用程序的执行都是cpu在做着快速的切换完成的,这个切换是随机的
普通方法的调用和线程开启的区别
线程的生命周期
- 新建状态
- 就绪状态
- 运行状态
- 阻塞状态
- 线程调用 sleep() 方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞 (join)
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
- 线程在等待某个通知(wait)
- 程序调用了线程的 suspend() 方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法
- 死亡状态
2. 线程的创建方式
继承Thread类- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用:避免OOP(面向对象)单继承局限性
public class TestThread01 extends Thread {
@Override
public 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 {
@Override
public 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 {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "创建了一个线程");
}
}