Java多线程、线程池概述

it2023-09-17  76

1 概念

进程 是指一个内存中运行的应用程序,每个进行都有一块独立的内存空间。

线程 是一个进程中一条执行路径,它们共享进程的内存空间,线程之间可以自由切换,并发执行,一个进程可以有多个线程。

分时调度 :所有线程轮流使用CPU,平均分配每个线程占用CPU的时间。

抢占式调度:让优先级高的线程优先使用CPU资源,CPU使用该方式在多个线程之间进行高速的切换,在某一时刻来说,对于CPU的一个核心来说,只有一个线程在执行,只是执行速度很快,我们无法感知出来,这中体验就像是多个线程在同时运行。多线程不能提高程序的运行速度,但是能提高程序的运行效率,使CPU的使用率更高。

同步:排队执行,效率低(效率低是一般情况下),线程安全 异步:同时执行,效率高,线程不安全

并发:多个事件在同一时间段发生

并行:多个事件在 同一时刻发生(真正的同时发生)

2 实现多线程的三种方法

方法一和方法二

public class MyThread extends Thread { /**重写线程的run()方法,该方法就是一条新的执行路径,启动该路径的方式是调用该类对象的start()方法,而不是直接调用该方法 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("副线程输出:" + i); } } } public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("MyRunnable类输出" + i); } } } public static void main(String[] args) { //实现多线程方法1: 继承Thread类 //首先是创建一个继承于Thread的类,并重写run()方法,然后调用其start()方法即可开启新线程。 MyThread myThread = new MyThread(); myThread.start(); //实现多线程方法2: 实现Runnable接口 MyRunnable myRunnable = new MyRunnable(); //创建一个任务对象 Thread thread = new Thread(myRunnable); //创建一个线程并为其分配一个任务 thread.start(); for (int i = 0; i < 10; i++) { System.out.println("主线程输出" + i); } }

实现Runnable接口和继承 Thread 相比有如下优势:

1.通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况 2.可以避免由于Java只能单继承而带来的局限性 3.任务与线程是分离的,提高了程序的健壮性 4.线程池技术,只接受Runnable类型的任务,不接受Thread类型的线程

方式3 Callable接口

public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable c = new MyCallable(); FutureTask<Integer> task = new FutureTask<>(c); //创建一个任务 new Thread(task).start(); //启动分支线程 //获取线程返回值,该方法在执行时,主线程会等待分支线程执行完毕后才会执行;如果没有该方法,则是交替执行 int j = task.get(); System.out.println("返回值为:" + j); for (int i = 0; i < 10; i++) { Thread.sleep(100); System.out.println(i); } } /*新建一个类继承Callable接口,并传入返回值类型,重写其call()方法*/ static class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { for (int i = 0; i < 10; i++) { Thread.sleep(100); System.out.println(i); } return 100; } }

3 Thread类

常用构造方法作用1Thread()无参构造2Thread(Runnable target)创建一个线程并分配一个target任务3Thread(String name)创建一个名为name的线程(可通过.getName()获取名称) 常用方法作用1getId()返回线程标识符2getName()返回线程名称3setPriority()设置线程优先级4getPriority()获取线程优先级5start()启动线程6sleep(long millis)线程休眠,单位为毫秒7currentThread()获取当前执行的线程对象(静态方法)8setDaemon()设置当前线程为守护线程9interrupt()中断线程

线程阻塞 :也成为耗时操作,一般出现需要相对长时间的操作,比如接收用户输入。

4 线程中断

线程中断:一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定。

在早期版本中,JDK提供了stop()方法用于中断 线程,但是后来发现该方法不合理,例如突然终止线程,可能会出现与此相关联的一些资源得不到及时释放;因此一个线程的中断应该由自身决定才符合逻辑,于是该方法被弃用。

在目前版本中,如果要想线程中断,应该由开发者对线程进行中断标记,然后通过捕获中断异常 InterruptedException 在 catch 块中进行中断线程。

例如下面的例子中,演示了如果主线程 main 执行完之后,则分支线程也中断的方法:

public class Demo2 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new MyRunnable()); //创建一个分支线程并分配一个任务 t.start(); for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + ":" + (i + 1)); Thread.sleep(1000); } /*分支线程中断的标记,如果t线程还在执行中,并且有能触发捕获该异常的操作(例如sleep()) 则会进入捕获中断异常的catch块内*/ t.interrupt(); } static class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + ":" + (i + 1)); try { //每隔1秒输出一次,sleep()方法会触发检查中断异常,如果线程有中断标记则会进入catch块 Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("捕获到中断异常"); return; //捕获到中断异常,则终止运行并返回 } } } } } /* 输出结果: Thread-0:1 main:1 main:2 Thread-0:2 Thread-0:3 main:3 Thread-0:4 捕获到中断异常 从以上的输出结果可以看出,分支线程在主线程执行完成打印任务后就触发了分支线程的中断; 如果在分支线程的catch块中不进行return返回,则分支线程不会中断,直到线程正常结束。 */

5 守护线程

线程可分为 用户线程 和 守护线程 ,直接创建的线程都是用户线程;

对于用户线程来说,当一个进程不包含任何存活的用户线程时才结束;

对于守护线程来说,当最后一个用户线程结束时,所有守护线程自动结束。

public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new MyRunnable()); //创建一个线程并分配一个任务 t.setDaemon(true); //设置为守护线程,要在线程启动之前进行设置 t.start(); for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + ":" + (i + 1)); Thread.sleep(1000); } } static class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + ":" + (i + 1)); try { Thread.sleep(1000); //每隔1秒输出一次 } catch (InterruptedException e) { } } } } /* 输出结果: main:1 Thread-0:1 Thread-0:2 main:2 Thread-0:3 main:3 Thread-0 从结果可以看出,设置为守护线程的t,在主线程结束后也跟着结束了。 */

6 解决线程安全问题

6.1方法一:同步代码块
synchronized(锁对象){ //需要同步的代码 } 任何对象都可以作为锁对象,但是需要各个线程都共用这个对象

使用示例:

public static void main(String[] args) { SaleTicket s = new SaleTicket(); Thread t1 = new Thread(s, "窗口1"); Thread t2 = new Thread(s, "窗口2"); Thread t3 = new Thread(s, "窗口3"); t1.start(); t2.start(); t3.start(); } static class SaleTicket implements Runnable { private int ticket = 10; //票数 Object o = new Object(); //用作锁的对象 @Override public void run() { //如果锁对象写在方法体中,那么每个线程所拥有的锁就不是同一把锁,无法保证线程安全 // Object o = new Object(); while (true) { /*同步代码块,把需要上锁的部分括上就可以达到线程安全,o是锁对象*/ synchronized (o) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "准备出票中..." ); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "出票完成,余票" + ticket); } else { break; } } } } }
6.2方法二:同步方法

同步代码块可以精确控制一行或多行代码,我们也可以把需要上锁的部分单独写成一个同步方法供run() 调用,然后在方法声明中加入 synchronized 关键字即可。

对同步代码块进行修改可得:

public synchronized boolean sale() { /*在非静态的同步方法中,上锁对象为 this ; 如果该方法是静态方法,则上锁对象是类名.class,在本方法中就是SaleTicket.class*/ if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "准备出票中..." ); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "出票完成,余票" + ticket); return true; } return false; }
6.3方法三:显示锁Lock

前两种同步代码块和同步方法都属于隐式锁 。

显示锁使用方法:

//首先创建一个锁对象(默认非公平锁) Lock l = new ReentrantLock(); //其次在需要上锁的代码前一行进行手动上锁 l.lock(); { //需要上锁的部分 } //最后在上锁的代码后一行进行解锁 l.unlock();

7 公平锁和非公平锁

公平锁就相当于先来先到,根据等待时间进行排队;非公平锁就相反,在每次锁解锁后,线程之间又进行争夺,谁抢到谁就执行。

上述介绍的三种上锁方式默认都是非公平锁,如果需要使用公平锁,则需要在创建显示锁的时候传入加上参数 true。

Lock l = new ReentrantLock(true); //创建公平锁

8 显示锁和隐式锁区别

synchronized隐式锁Lock显示锁层面不同synchronized是Java关键字,由JVM维护Lock是JDK中的一个类,是API层面的锁使用方式能够自动获取锁和释放锁需要.lock()上锁和.unlock()释放锁等待是否可中断不可中断,除非抛出异常或者正常运行完成可中断是否可设置公平锁只能为非公平锁默认为非公平锁,可传入true在构造方法中设置为公平锁能否精确唤醒线程不能,要么随机唤醒,要么唤醒所有等待线程可以精确唤醒线程性能早期版本性能不高性能相对较好

9 线程死锁

线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。

死锁产生的条件

互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。。

10 线程通信

wait() :线程休眠

notify() :唤醒等待该线程的单个线程

11 线程状态

new

尚未启动状态

Runnable

正在执行状态

Blocked

阻塞状态,在排队时就是处于阻塞状态,阻塞取消后回到执行状态

Waiting

无限等待状态,直到某个线程唤醒它,唤醒后变成执行状态

TimeWaiting

指定时间等待状态,被唤醒或者计时结束就回到执行状态

Terminated

线程终止

12 线程池

当并发的线程很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会使系统效率大打折扣,因为创建线程和结束线程都需要时间。线程池的出现就是为了解决并发线程效率不高的问题,线程池中的线程可以反复使用,这样就省去了频繁创建线程的操作,节省很多时间。

12.1缓存线程池

线程数量没有限制,在执行时,首先判断线程池是否存在空闲线程,如果存在,则使用空闲线程执行任务;如果不存在,则重新创建线程并放到线程池里供其使用。

public static void main(String[] args) throws InterruptedException { //创建一个缓存线程池 ExecutorService service = Executors.newCachedThreadPool(); //为线程池分配任务 service.execute(new Runnable() { @Override public void run() { System.out.println("任务1:" + Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("任务2:" + Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("任务3:" + Thread.currentThread().getName()); } }); //主线程休眠一秒钟 Thread.sleep(1000); service.execute(new Runnable() { @Override public void run() { System.out.println("任务4:" + Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("任务5:" + Thread.currentThread().getName()); } }); } /*输出: 任务3:pool-1-thread-3 任务1:pool-1-thread-1 任务2:pool-1-thread-2 任务4:pool-1-thread-2 任务5:pool-1-thread-1 从结果可以看出,线程确实得到复用了 */

12.2定长线程池

线程数量是指定的固定值,在执行时,首先判断池中是否存在空闲线程,如果存在则使用;如果不存在空闲线程,且在线程池未满的情况下,会重新创建线程并放到线程池供其使用;如果线程不存在空闲且线程池已满,则需要等待线程池存在空闲线程才进行执行新的任务。

//创建一个长度为2的线程池 ExecutorService e = Executors.newFixedThreadPool(2);

12.3单线程线程池

有点类似于定长线程池中设置线程数为1.

//创建一个单线程线程池 ExecutorService e = Executors.newSingleThreadExecutor();

12.4周期性任务定长线程池

//创建周期性任务定长线程池 ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /**定时执行一次 * 参数1:执行的任务 * 参数2:时长数值 * 参数3:时长单位,由TimeUnit常量指定 * @Param [args] * @return void */ service.schedule(new Runnable() { @Override public void run() { System.out.println("任务执行了"); } }, 5, TimeUnit.SECONDS); /**周期性执行 *参数1.执行的任务 * 参数2.延迟时长数字(第一次执行在多少时间后) * 参数3.周期时长数字(每隔多久执行一次) * 参数4.时长数字的单位 * @Param [args] * @return void */ service.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("任务执行了"); } }, 5, 1, TimeUnit.SECONDS); }
最新回复(0)