1 进程与线程
进程(Process):
每个独立运行的程序都对应一个进程。进程是资源分配的最小单位,占用独立的内存空间和系统资源
线程(Thread):
CPU调度和分派的基本单位,程序执行过程中的最小单元
例如:迅雷是个进程,里面的多个下载任务属于线程
二者区别
进程是资源分配的基本单位,线程是CPU调度的基本单位; 建立一个进程,CPU会分配独立的内存空间,同一进程中线程则共享进程中的数据,而进程之间是独立的内容空间; 一个进程可包含多个线程,线程依附于进程存在
主线程:main()方法即为主线程入口,是产生其他子线程的线程
2 多线程实现与启动
方法 | |
---|---|
public void start() | 启动线程 |
public static Thread currentThread() | 返回当前正在执行的线程对象的引用 |
public final boolean isAlive() | 测试线程是否处于活动状态 |
public final String getName() | 返回该线程名称 |
public final void setName(String name) | 设置线程名称 |
static void sleep(long millis) | 线程睡眠,使线程转到阻塞状态。millis设定睡眠的时间,以毫秒为单位。当睡眠结束,转为就绪(Runnable)状态。 |
static void yield() | 线程礼让,暂停当前正在执行的线程对象,把执行机会让给其他线程 |
void join() | 线程插队(线程加入),在当前线程中调用另一个线程对象的join()方法,当前线程转为阻塞状态,直到另一个线程运行结束,当前线程转为就绪状态。 |
void wait() | 线程等待,导致当前的线程等待,直到其他线程调用此对象的 notify()方法或notifyAll()方法唤醒 |
void notify() | 线程唤醒,唤醒在此对象监视器上等待的单个线程。如果有多个线程在此对象上等待,则会选择唤醒其中一个,选择是任意的。 |
void notifyAll() | 线程唤醒,唤醒在此对象监视器上等待的所有线程 |
wait()、notify()、notifyAll() 这3个方法是Object类的方法,针对的是对象
3个方法只能在同步(synchronized)方法或代码块中调用(持有对象锁),否则报IllegalMonitorStateException异常
2.1 继承Thread类
2.1.1 基本语法
修饰符 class 类名 extends Thread {
@Override
public void run() {
线程中的逻辑
}
}
启动线程:
线程对象名.start()
2.2.2 代码演示
// 自定义线程类
public class MyThread extends Thread {
// 线程执行的核心逻辑,写在run方法里
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++) {
// System.out.println("子线程:" + i);
// Thread.currentThread() 获取当前线程对象 getName() 获取线程名
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class App {
// main()方法程序的入口,也称为主线程
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("主线程");
// 多个线程"同时"执行
// 创建线程对象
MyThread thread = new MyThread();
// 启动线程,并不一定会立即执行,start后先进入就绪状态,不一定能立马获取cpu资源
thread.start();
MyThread thread2 = new MyThread();
thread2.start();
System.out.println("主线程结束");
}
}
2.2 实现Runnable接口
2.2.1 基本语法
public class 类名 implements Runnable {
@Override
public void run() {
线程中的逻辑
}
}
调用
// 创建该类的实例作为参数传递给线程实例,然后启动:
Thread th = new Thread(new 实现Runnable接口的类());
th.start();
2.2.2 代码演示
public class MyRunnable implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class App2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyRunnable myRunnable = new MyRunnable();
// Thread()的参数是Runnable对象
Thread thread = new Thread(myRunnable);
thread.start();
Thread thread2 = new Thread(myRunnable);
thread2.start();
System.out.println("主线程结束");
}
}
Thread和Runnable比较:
继承Thread类编写简单,可直接操作线程适用于单继承
实现Runnable接口避免单继承局限性,便于共享资源
2.2.3 使用匿名内部类写法
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
});
thread3.start();
3 线程状态
3.1 线程的生命周期
Java中线程的生命周期包括6种状态:
新建 New
可运行 Runnable
阻塞 Blocked
等待 Waiting
计时等待 Timed wating
终止 Terminated
3.2 线程状态转换
1)新建状态
使用new创建一个新的线程对象时,该线程处于创建状态。处于创建状态的线程只是一个空的线程对象,系统不为它分配资源。
2)可运行状态
调用线程的start()方法,线程处于可运行状态(JDK5后,就绪和运行统称为可运行状态)。一个可运行的线程可能正在运行,也可能没有运行。
我们可以这样理解,线程调度依赖操作系统提供的服务,抢占式调度系统给每一个可运行的线程一个时间片来执行任务。调用start()后,线程被调度系统选中,此时处于就绪状态(ready);就绪状态的线程在获得CPU使用权后变为运行中状态(running)。
3)阻塞状态
当一个线程试图获取一个内部对象锁,而这个锁被其他线程占用(使用了synchronzied),该线程就会被阻塞。当其他线程释放了这个锁,并且该线程持有了这个锁,该线程进入可运行状态。
4)等待状态
线程进入等待状态(比如wait()),需要等待另一个线程执行唤醒通知,否则会一直处于等待状态。
5)计时等待状态
有些方法有超时参数,调用这些方法,线程进入计时等待状态。
6)终止(Dead)
run()方法执行完成,或因异常退出,该线程结束
3.3 线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。
优先级高的线程获得执行机会的概率更大。
Java线程的优先级用整数表示,取值范围是 1~10,
Thread 类有以下三个静态常量:
static int MAX_PRIORITY:最高优先级,取值为 10。
static int MIN_PRIORITY:最低优先级,取值为 1。
static int NORM_PRIORITY:默认优先级,取值为 5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
4 线程调度
4.1 线程睡眠
时长单位: 毫秒
在哪个线程中调用该方法,哪个线程就会进入阻塞状态,睡眠时间到了,线程进入就绪状态
public class App3 {
public static void main(String[] args) {
System.out.println("启动主线程");
MyThread2 t = new MyThread2();
t.start();
// 线程睡眠 时长单位: 毫秒
// 线程将纳入阻塞状态,时间到了,进入就绪状态
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("主线程结束");
}
}
// 一个类文件中可以些多个类,但是只能有一个public类,而且public类名要和类文件同名
class MyThread2 extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++) {
if(i == 50) {
try {
// 在哪个线程中调用该方法,哪个线程就会休眠
Thread.sleep(6000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
4.2 线程插队 (了解)
当前线程中,另一个线程对象调用join()方法
则,当前线程进入阻塞状态,
另一个线程进入执行状态
public class App4 {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("启动主线程");
SubThread subThread = new SubThread();
subThread.start();
for(int i = 0; i < 1000; i++) {
if(i == 100) {
try {
// 效果上来说,相当于执行插队操作
// i == 500, 有可能main还占着cpu,某个线程调用join方法后,
// 该线程相当于进行了插队,强制获取到了cpu的资源,原来的线程进入到阻塞状态
// 本例中使用无参的join方法,这种情况,子线程逻辑全部执行完,主线程才会继续执行
// subThread.join();
// 带时间的方法,单位毫秒,表示阻塞指定时间后,结束阻塞
subThread.join(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
// for(int i = 0; i < 1000; i++) {
//
// System.out.println(Thread.currentThread().getName() + ":" + i);
// }
}
}
class SubThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 500; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
4.3 线程礼让 (了解)
yield()是Thread中的静态方法
调用yield的线程进入就绪状态,其他线程获得执行机会
但是,执行yield后,只是提供了这样的可能性,并不一定能保证其他线程一定执行。因为执行yield的线程也会进入就绪状态,他们都会参与cpu资源的竞争
public class App5 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Producer producer = new Producer();
Customer customer = new Customer();
producer.start();
customer.start();
}
}
class Producer extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 500; i++) {
if(i == 200) {
// 当前线程,进入就绪状态
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class Customer extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 500; i++) {
if(i == 200) {
// 当前线程,进入就绪状态
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
join()方法和yield()方法的主要区别是什么?
join 使当前线程进入阻塞状态
yield使当前线程进入就绪状态
5 线程同步
5.1 线程安全问题
1)操作同一银行账户
2)抢票大战
比如50个人抢100张票
public class App7 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket ticket = new Ticket();
for(int i = 0; i < 50; i++) {
Thread t = new Thread(ticket);
t.start();
}
}
}
class Ticket implements Runnable {
// 共享的资源
private int num = 100;
@Override
public void run() {
// TODO Auto-generated method stub
num--;
System.out.println("剩余票数:" + num);
}
}
当线程体内操作成员变量(共享数据),多个线程同时执行时,就可能出现多线程数据安全问题。
3)如何解决多线程中的数据安全问题
java中提供了一个同步机制(锁)。
即让操作共享数据的代码在某一时间段,只被一个线程执行(锁住),在执行过程中,其他线程不可以参与进来,这样共享数据就能同步了。
简单来说,就是给代码加把“锁”,而且,多个线程只能使用一把“锁”。
如何加锁呢?借助synchronized关键字
5.2 同步方法
synchronized位于方法声明中
public class App7 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket ticket = new Ticket();
for(int i = 0; i < 50; i++) {
Thread t = new Thread(ticket);
t.start();
}
}
}
class Ticket implements Runnable {
// 共享的资源
private int num = 100;
public void setNum(int num) {
this.num = num;
}
@Override
public void run() {
// TODO Auto-generated method stub
show();
}
// 同步方法, synchronized修饰方法,相当于对方法加锁
// 锁只能有一把,synchronized修饰的方法,用的锁是 this对象
public synchronized void show() {
num--;
System.out.println("剩余票数:" + num);
}
}
5.3 同步代码块
synchronized位于对象前,限定某一代码块
public class App {
public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket t = new Ticket();
for(int i = 0; i < 50; i++) {
Thread th = new Thread(t);
th.start();
}
}
}
class Ticket implements Runnable {
private int num = 100;
@Override
public void run() {
// TODO Auto-generated method stub
// 同步代码块 synchronized(表示锁的对象) {} ,
// 多个Thread使用同一个Runnable对象时,"锁"通常可以使用this
synchronized (this) {
// Ticket.class,返回Class类型的对象,当前先了解一下
// synchronized (Ticket.class) {
num--;
System.out.println("剩余票数:" + num);
}
}
}
5.4 同步机制的优劣
同步的好处:
同步的出现解决了多线程的安全问题。
同步的弊端:
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。(性能和线程安全不可兼得)
6 线程池
6.1 什么是线程池
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程(提高线程复用,减少性能开销)。
线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中然后等待下一次分配任务。
6.2 Executors工厂类创建线程池 了解
newFixedThreadPool() | 定长线程池 | 池中包含固定数目的线程,空闲线程一直保留。只有核心线程,线程数量固定,任务队列为LinkedBlockingQueue |
---|---|---|
newScheduledThreadPool() | 定时线程池 | 用于调度执行的固定线程池。执行定时或周期性任务。核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为DelayedWorkQueue |
newCachedThreadPool() | 可缓存线程池 | 无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为SynchronousQueue。 |
newSingleThreadExecutor() | 单线程化线程池 | 只有一个线程的池,会顺序执行提交的任务。只有 1 个核心线程,无非核心线程,任务队列为LinkedBlockingQueue |
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
线程池执行任务时,可以采用两种方法:
execute(): 没有返回值,无法判断任务是否执行成功
submit():会返回Future对象,通过该对象判断任务是否执行成功
package com.glls.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
*/
public class Executors01 {
public static void main(String[] args) {
// TODO Auto-generated method stub
schedule3();
}
public static void fixed() {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("haha");
}
});
}
public static void cache() {
ExecutorService cachedPool = Executors.newCachedThreadPool();
cachedPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("haha");
}
});
}
public static void single() {
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("houhou");
}
});
}
public static void schedule() {
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
// 延迟指定时间执行
newScheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("houhouhouhou");
}
}, 5, TimeUnit.SECONDS);
}
public static void schedule2() {
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
// 延迟指定时间后,间隔固定时间执行任务
// 比如延迟2秒,间隔5秒执行任务
// 指的是“以固定的频率”执行,period(周期)指的是两次成功执行之间的时间。
// 上一个任务开始的时间计时,一个period后,检测上一个任务是否执行完毕,
// 如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行
// 当前任务执行时间大于等于间隔时间,任务执行后立即执行下一次任务。相当于连续执行了。
newScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("hehehehe");
}
}, 2, 5, TimeUnit.SECONDS);
}
public static void schedule3() {
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
// 延迟指定时间后,间隔固定时间执行任务
// 指的是“以固定的延时”执行,delay(延时)指的是一次执行终止和下一次执行开始之间的延迟
newScheduledThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("hehehehe");
}
}, 2, 5, TimeUnit.SECONDS);
}
}
6.3 ThreadPoolExecutor创建线程池
创建线程池的推荐方式
1)相关构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue){}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
corePoolSize | 核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收 |
---|---|
maximumPoolSize | 线程池所能容纳的最大线程数。 |
keepAliveTime | 线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收 |
unit | 指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分) |
workQueue | 任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。 |
threadFactory | 线程工厂。用于指定为线程池创建新线程的方式 |
handler | 拒绝策略。当达到最大线程数时需要执行的策略 |
2)任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。常见的阻塞队列如下:
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列) |
---|---|
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务 |
DelayQueue | 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来 |
SynchronousQueue | 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回 |
3)拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。Executors 框架已经为我们实现了 4 种拒绝策略:
AbortPolicy(默认) | 丢弃任务并抛出 RejectedExecutionException 异常。 |
---|---|
CallerRunsPolicy | 由调用线程处理该任务 |
DiscardPolicy | 丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式 |
DiscardOldestPolicy | 丢弃队列最早的未处理任务,然后重新尝试执行任务 |
package com.glls.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
*
*/
public class Executors02 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Task task = new Task();
// 创建线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,
10,
1000,
TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for(int i = 0; i < 3; i++) {
pool.execute(task);
}
}
}
class Task implements Runnable{
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
6.4 线程池任务调度流程
附录
操作系统层面线程的状态
5种状态
创建、就绪、运行、阻塞、终止
单例设计模式
单例,即只有一个实例,该模式的作用是保证程序中某个类的对象只有一个。
在程序运行过程中,某些类,在程序中只需要存在一个对象实例就可以满足需求,不必每次请求都创建新的对象。此时,可以使用单例模式。
比如:存储程序的配置信息的类
懒汉式
public class Singleton{
private static Singleton singleton = null;
private Singleton(){}
// 确保线程同步
public synchronized static Singleton getInstance(){
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
饿汉式
public class Singleton{
// 类加载的时候进行初始化
private static final Singleton singleton = new Singleton();
private Singleton (){}
public static Singleton getInstance(){
return singleton;
}
}