首页 Java 学习 线程
文章
取消

Java 学习 线程

多线程的创建方式

多线程概念:从软硬件上实现多条执行流程的技术

继承Tread类

java.lang.Thread

继承该类,重写run()方法

调用该类 xx.start() 来启动线程

缺点

继承了Thread类,无法继承其他类不利于扩展

// 为啥调用start方法 实际执行的是 run 方法

调用的是start来启动的原因

如果调用重写的run,run方法会被当成普通方法来执行,此时还是相当于是单线程执行,调用start方法才会启动一个新的线程

创建的线程会被加到线程列表中 group的类型是 ThreadGroup

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("thread:"+i);
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("main:"+i);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
main:0
thread:0
main:1
thread:1
main:2
thread:2
thread:3
thread:4
main:3
main:4

优点

可以继承其他类,实现其他接口,扩展性更强

缺点

多一层对象包装,如果线程有执行结果是不可以直接返回的

Runnable 匿名对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyRunnable2{

    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("thread:"+i);
                }
            }
        });
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("main:"+i);
      }
    }
}

Runnbale 是函数式接口

函数式接口:有且仅有一个抽象方法

可以用lambda表达式简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyRunnable2{

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("thread:"+i);
            }
        });

        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("main:"+i);
        }
    }
}

实现Callable接口

主要解决前两种方法无法取得返回结果的问题

流程

  • 得到任务类对象

    • 定义类实现Callable 接口,重写call方法

    • FutureTaskCallable对象封装成线程任务对象

  • 线程任务对象交由Thread处理

  • start()启动

  • 通过FutureTask的get方法去获取任务执行的结果

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyCallable implements Callable<String> {
    private String name;

    public MyCallable(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        Thread.sleep(2000);
        return "name is "+name;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable("haha");
        FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
        Thread t = new Thread(stringFutureTask);
        t.start();
        System.out.println(stringFutureTask.get());
        // 上述的get如果没有返回结果 会被阻塞
        MyCallable myCallable2 = new MyCallable("xixi");
        FutureTask<String> stringFutureTask2 = new FutureTask<>(myCallable2);
        Thread t2 = new Thread(stringFutureTask2);
        t2.start();
        System.out.println(stringFutureTask2.get());
    }
}

Callable无法直接交给Thread类 FutureTask 实现了Runnable的接口,所以可以直接交给Thread类

常用API

  • 获取线程名称 getName()

  • 设置名称 setName()

  • 获取当前线程对象 currentThread()

主线程默认名称为 main 线程执行过程 通过 Thread.currentThread().getName()来获取

  • 静态方法 Thread.sleep() 进行休眠

线程安全问题

多个线程操作共享资源

解决思路:加锁

同步代码块

1
2
3
synchronized("锁对象"){
    ...
}

这里的锁对象 是多个线程处理的一个同一个对象即可 即任意唯一的对象,字符串常量 就是一个唯一对象。

但可能会影响其他无关线程的执行,规范要求是 使用 共享资源作为锁对象,比如银行账户取钱,应该用账户作为锁对象

  • 实例方法”锁对象”改为 this

  • 静态方法 使用 类名.class

同步方法

方法的修饰符后面,返回类型前 加上 synchronized 即可

底层原理:

本质上也是有 隐式锁对象的,同样 实例方法 用 this作为锁对象 静态方法使用 类名.class

// 同步方法 可以跨方法 ??? 多个方法使用的是同一个锁对象

理论上来说 代码块 效率更高,但是 同步方法更简单 更直观

Lock 锁

Lock是一个接口 可以使用 ReentrantLock

主要使用 lock()unlock()

private final Lock myLock = new ReentrantLock()

final修饰 唯一和不可替换的

1
2
3
4
5
6
myLock.lock();
try{

}finally{
    myLock.unlock();
}

解锁 写finally里面 防止死锁之类的问题

// 抢占 之类的 暂略

线程通信 生产者和消费者

  • wait()

  • notify()

  • notifyAll()

上述方法应该使用锁对象进行调用

线程池(重点)

jdk线程池接口 ExecutorService

线程池对象

  • ExecutorService 的实现类 ThreadPoolExecutor 创建线程池对象

  • 使用Executors 线程池的工具类调用方法返回不同特点的线程池对象

方式1:

构造器如下:

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}
  1. 核心线程数量

  2. 最大线程数量 (核心线程数量不够用时,可以多创建些,但是后续会被销毁)

  3. 临时线程存活时间

  4. 存活时间单位 秒 分 时 天

  5. 任务队列

  6. 线程工厂

  7. 当最大线程数量的线程都在忙时,任务队列已满的情况下,对于新任务的处理,比如忽略 比如 抛异常

ktv 例子 为例 1参数表示正式员工 一直在的 假设为3个,参数2是正式员工和临时工的数量,假设为10,参数3表示临时工的招工时间,参数5表示门口的排队座位数量,参数6表示专门招人的,是专门招临时工的(正式员工一直存活),参数7表示座位满了,对于新客人的处理

示例:

1
2
3
ExecutorService executorService = new ThreadPoolExecutor(3,5,8,
                TimeUnit.SECONDS,new ArrayBlockingQueue<>(6),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

后俩个参数可以不填,使用默认值

ExecutorService常用方法:

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future
submit(Callable task)
执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
void
shutdown()
等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

拒绝策略:

策略 含义
AbortPolicy 丢弃并抛出异常 默认
DiscardPolicy 丢弃 不抛出
DiscardOldestPolicy 抛弃等待最久的任务,加入新任务
CallerRunsPolicy 主线程负责调用任务的run方法来绕过线程池直接执行

常见面试题

  1. 临时线程创建的时机:新任务提交时发现核心线程都在忙,任务队列都满了,且可以创建临时线程。此时才会创建临时线程

  2. 什么时候开始拒绝任务:最大线程数量都在忙,任务队列已满

简单验证线程创建和拒绝任务时机

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThreadPool {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = new ThreadPoolExecutor(3,5,8,
                TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        Runnable my_task = new MyThreadPoolRunnable();
        for (int i = 0; i < 8; i++) {
            executorService.submit(my_task);
            Thread.sleep(1000);
        }
    }
}

创建8个任务 每创建一个 睡眠一秒

1
2
3
4
5
6
7
8
9
10
11
public class MyThreadPoolRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" now: "+new SimpleDateFormat("HH:mm:ss:SSS").format(new Date()));
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
pool-1-thread-1 now: 14:14:25:531
pool-1-thread-2 now: 14:14:26:503
pool-1-thread-3 now: 14:14:27:504
pool-1-thread-4 now: 14:14:30:525
pool-1-thread-5 now: 14:14:31:538
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@6442b0a6[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@67b64c45[Wrapped task = learning.thread.MyThreadPoolRunnable@4411d970]] rejected from java.util.concurrent.ThreadPoolExecutor@60f82f98[Running, pool size = 5, active threads = 5, queued tasks = 2, completed tasks = 0]
    at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
    at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
    at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
    at learning.thread.MyThreadPool.main(MyThreadPool.java:12)
pool-1-thread-1 now: 14:14:35:553
pool-1-thread-2 now: 14:14:36:514

简单分析:

前3个任务核心线程进行处理,每个间隔1秒,线程3任务结束后,是第28秒,放入第4个任务,休眠1秒,现在是第29秒,放入第5个任务,休眠1秒,现在是第30秒,此时,队列长度为2,最大线程数量为5,即3个线程都在忙,任务队列已满,且可以创建临时线程(线程数量未达上限),会创建第四个线程(临时线程),将该任务取出,此时会输出当前时间,即第30秒,接着第6个任务放入,休眠1秒,此时是第31秒,因为线程数量未达上线,队列又再次满了,会创建第5个线程(临时线程),处理第5个任务,输出当前时间第31秒,第5个任务会被拿出,此时队列长度是1,接着是第7个任务放入,此时队列长度为2,休眠1秒,时间为第32秒,线程不能再被创建,第8个任务到来,抛出异常,该任务被拒绝,因为队列满了,线程1的任务结束(休眠了10秒),现在是第35秒,开始处理第6个任务,过了1秒,线程2的任务结束,现在是第36秒,开始执行第7个任务。

任务队列

这里用的是ArrayBlockingQueue

常见的还有

  • LinkedBlockingQueue 长度可以到Integer最大数,基本可以认为是无限长了,但一般都会人为指定长度(不指定,默认就是Integer.MAX_VALUE),与ArrayBlockingQueue类似,线程都在忙时,任务队列满了,没有达到线程数量上限时,才会创建新的线程处理任务,如果设的很大,会全阻塞在队列中

  • SynchronousQueue 本身没有容量,是无缓冲的等待队列,来一个塞一个给消费者执行,maximumPoolSize 通常会设定为无界(Integer.MAX_VALUE),防止被拒绝执行

// ScheduleExecutorService定时器

内部为线程池 ,早先的Timer定时器是单线程 多个任务顺序执行,存在时延,也可能因为某个任务异常使Timer线程死亡

Executors工具类得到线程池对象

Executors 底层也是基于ThreadPoolExectuor创建线程池对象的

方法名称 说明
newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
newFixedThreadPool​(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
newSingleThreadExecutor () 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public
static ScheduledExecutorService
newScheduledThreadPool​(int corePoolSize)
创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

前三者 返回类型都是 ExecutorService

可能存在的系统风险

  • newFixedThreadPool newSingleThreadExecutor 的请求队列长度为Integer.MAX_VALUE

  • newCachedThreadPool newScheduledThreadPool 线程数量上限为Integer.MAX_VALUE

大型并发系统中都可能导致OOM

未完待续…

主要参考 黑马程序员

线程状态

  • NEW 新建状态—— 创建线程对象

  • RUNNABLE 就绪状态 —— start 方法 等待CPU调度

  • BLOCKED 阻塞状态——无法获得锁对象

  • WAITING 等待状态——wait方法,(其他线程notify或notifyAll才能够唤醒)

  • TIMED_WAITING 计时等待——sleep方法

  • TERMINEATED 结束状态 run方法正常退出而死亡 或 没有捕获的异常终止了run方法而死亡

本文由作者按照 CC BY 4.0 进行授权

Java 学习 Stream流

Java 学习 反射