多线程
创建方式
Thread类
- 定义一个子类
MyThread
继承线程类
Java.lang.Thread
,重写
run()
方法
- 创建MyThread对象
- 调用线程对象的start()方法启动线程(启动后还是执行run方法)
优缺点
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展
注意
为什么不直接调用了run方法,而是调用start启动线程。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
把主线程任务放在子线程之前
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理
- 调用线程对象的start()方法启动线程
Thread构造器
// 可以为当前线程指定名称public Thread(String name)// 封装Runnable对象称为线程对象public Thread(Runnable target)
优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
- 缺点:编程多一层对象包装,如果线程有执行结果不可以直接返回
// 用Lambda实现public class runnable2 {public static void main(String[] args) {Thread t = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println("子线程:" + i);}});t.start();for (int i = 0; i < 10; i++) {System.out.println("主线程:" + i);}}}
// 可以更为简洁public class test {public static void main(String[] args) {new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("thread1:" + i);}}).start();for(int i = 0; i < 5; i++) {System.out.println("thread0:" + i);}}}
小结
前两种线程创建方式都存在一个问题
- 重写run方法不能直接返回结果,不适合需要返回线程执行结果的业务场景
Callable&FutureTask
- 得到任务对象
- 定义类实现Callable接口,重写call方法,封装要做的事情
- 用FutureTask把Callable对象封装成线程任务对象
- 把线程任务对象交给Thread处理
- 调用Thread的start方法启动线程,执行任务
- 线程执行完毕后、通过FutureTask的get方法来获取任务执行的结果
API
// 把Callable对象封装成FutureTask对象public FutureTask<>(Callable call)// 获取线程执行call方法返回的结果public V get() throws Exception
优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
- 可以在线程执行完毕后去获取线程执行的结果
public class callll {public static void main(String[] args) throws Exception{FutureTask<String> future = new FutureTask<>(new callllll());Thread t = new Thread(future);t.start();String s1 = future.get();System.out.println("s1: " + s1);}}class callllll implements Callable<String> {@Overridepublic String call() {return "getResult";}}
对比
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能返继承其他的类。 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
Thread常用API
Thread常用方法:
- 获取线程名称getName()
- 设置名称setName()
- 获取当前线程对象currentThread()。
构造器
// 可以为当前线程指定名称public Thread(String name)// 封装Runnable对象成为线程对象public Thread(Runnable target)// 封装Runnable对象成为线程对象,并指定线程名称public Thread(Runnable target ,String name )
方法
// 获取当前线程的名称,默认线程名称是Thread-索引String getName()// 设置线程名称void setName(String name)// 返回对当前正在执行的线程对象的引用public static Thread currentThread():// 让线程休眠指定的时间,单位为毫秒public static void sleep(long time)// 线程任务方法public void run( )// 线程启动方法public void start( )
线程安全
多个线程同时访问同一个共享资源且存在修改该资源的时候会出现不安全隐患
同步代码块
- 作用: 把出现线程安全问题的核心代码给上锁。
- 原理: 每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized(同步锁对象) {操作共享资源的代码}
锁对象要求
-
理论上:锁对象只要对于当前同时执行的线程说是同一个对象即可
-
不要使用任意唯一的对象,可能会影响其他无关线程的执行
-
规范:
建议使用共享资源作为锁对象
- 对于实例方法建议使用
this
作为锁对象
- 对于静态方法建议使用字节码(
类名.class
)对象作为锁对象
同步方法
底层原理
- 底层是有隐式锁对象的,只是锁的范围是整个方法代码
- 如果方法是实例方法:同步方法默认用
this
作为锁对象。
- 如果方法是静态方法:同步方法默认用
类名.class
作为锁对象
synchronized T method(...) {}
Lock锁
- 为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
// 获取Lock锁的实现类对象public ReentrantLock()
Lock API
// 获取锁void lock()// 释放锁void unlock()
实例
// 银行取钱,一次只能一个人进去// try finally 确保锁对象被释放public double drawMoney(double money) {lock.lock();try {if (money > this.money) {System.out.println(Thread.currentThread().getName() + "钱不够!");return 0;}System.out.println(Thread.currentThread().getName() + "取现成功");this.money -= money;} finally {lock.unlock();}return money;}
线程通信
什么是线程通信、如何实现?
- 所谓线程通信就是线程间相互发送数据。
线程通信常见形式
- 通过共享一个数据的方式实现。
- 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
线程通信实际应用模型
- 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
- 一般要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。
Object类的等待和唤醒方法
// 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法void wait()// 唤醒正在等待的单个线程void notify()// 唤醒正在等待的所有线程void notifyAll()
需要使用锁对象去唤醒其他的线程
实例
使用Runnable接口创建两个线程
- 监听电话接入(生产者)
- 接听电话(消费者)
public class communication {// true标识目前有电话接入private boolean flag = false;public void run() {// 监听电话的线程new Thread(() -> {try {while (true) {synchronized (communication.this) {if (!flag) {System.out.println("新电话来了!");Thread.sleep(5000);flag = true;communication.this.wait();communication.this.notify();}}}} catch (InterruptedException e) {e.printStackTrace();}}).start();// 接听电话的线程new Thread(() -> {try {while (true) {synchronized (communication.this) {if (flag) {System.out.println("电话接听中,通信五分钟结束~~~");flag = false;}communication.this.wait();communication.this.notify();}}} catch (InterruptedException e) {e.printStackTrace();}}).start();}public static void main(String[] args) {communication xiaomi = new communication();xiaomi.run();}}
线程池
线程池就是一个可以复用线程的技术
为什么要使用线程池?
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
创建线程池对象
- 方式一:使用
ExecutorService
的实现类
ThreadPoolExecutor
自创建一个线程池对象
- 方式二:使用
Executors
(线程池的工具类)调用方法返回不同特点的线程池对象
ThreadPoolExecutor
构造器
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
参数 | 定义 |
---|---|
corePoolSize |
指定线程池的线程数量(核心线程) : 不能小于0 |
maximumPoolSize |
指定线程池可支持的最大线程数,最大数量>=核心线程数量 |
keepAliveTime |
指定临时线程的最大存活时间:,不能小于0 |
unit |
指定存活时间的单位(秒、分.时、天) |
workQueue |
指定任务队列,不能为null |
threadFactory |
指定用哪个线程工厂创建线程,不能为null |
handler |
指定线程忙,任务满的时候,新任务来了怎么办,不能为null |
临时线程什么时候创建?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝
新任务拒绝策略
策略 | 详解 |
---|---|
ThreadPoolExecutor. AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor. DiscardPolicy | 丢弃任务,但是不抛出异常这是不推荐的做法 |
ThreadPoolExecutor. DiscardOldestPolicy | 抛弃队列中等待最久的任务然后把当前任务加入队列中 |
ThreadPoolExecutor. CallerRunsPolicy | 由主线程负责调用任务的run0方法从而绕过线程池直接执行 |
常用API
// 执行任务/命令,没有返回值,-般用来执行Runnable 任务void execute( Runnable command)// 执行Callable任务,返回未来任务对象获取线程结果Future<T) submit(Callable<T> task)// 等任务执行完毕后关闭线程池void shutdown()// 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务List<Runnable> shutdownNow()
示例
创建了一个target Runnable对象,线程池核心线程有两个,最大线程数为4,任务队列大小为3(最大线程数和任务队列大小决定了任务数的上限为7)
public class test {public static void main(String[] args) {ExecutorService pool = new ThreadPoolExecutor(2, 4, 6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());Runnable target = () -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " Hello");};for (int i = 0; i < 7; i++) {pool.execute(target);}pool.shutdown();}}
Executors
线程池的工具类通过调用方法返回不同类型的线程池对象
// 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。public static ExecutorService newCachedThreadPool()//创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。public static ExecutorService newFixedThreadPool(int nThreads)//创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。public static ExecutorService newSingleThreadExecutor ()//创建个线程池, 可以实现在给定的延迟后运行任务,或者定期执行任务。public static ScheduledExecutorService nesceduledTreadPool(int corePoolsize)
注意: Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
陷阱
-
大型并发系统环境中使用Executors如果不注意可能会出现系统风险
-
newFixedThreadPool
和
newSingleThreadExecutor
允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError )
-
newCachedThreadPool
和
newScheduledThreadPool
创建的线程数量最大上限是Integer.MAX_VALUE, 线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError )
定时器
定时器是一种控制任务延时调用,或者周期调用的技术。
作用:闹钟、定时邮件发送。
定时器的实现方式
-
Timer
-
ScheduledExecutorService
Timer
构造器
public Timer()
方法
// 开启一个定时器,按照计划处理TimerTask任务public void schedule(TimerTask task, long delay, 1ong period)
Timer定时器的特点和存在的问题
- Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
- 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
ScheduledExecutorService
ScheduledExecutorService是 jdk5中引入了并发包,目的是为了弥补Timer的缺陷
ScheduledExecutorService内部为线程池。
Executors的方法
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)//得到线程池对象
ScheduledExecutorService的方法
// 周期调度方法public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,TimeUnit unit)
优点
- 基于线程池,某个任务的执行情况不会影响其他定时任务的执行。
并发与并行
并发与并行
- 正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。
并发的理解
- CPU同时处理线程的数量有限。
- CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解
- 在同一个时刻 上,同时有多个线程在被CPU处理并执行。
线程的生命周期
Java线程的状态
- Java总共定义了6种状态
- 6种状态都定义在Thread类的内部枚举类中。
public enum State {/*** Thread state for a thread which has not yet started.*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.** For example, a thread that has called {@code Object.wait()}* on an object is waiting for another thread to call* {@code Object.notify()} or {@code Object.notifyAll()} on* that object. A thread that has called {@code Thread.join()}* is waiting for a specified thread to terminate.*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.*/TERMINATED;}