线程(Thread):是一个程序内部的一条执行流程。( 程序中如果只有一条执行流程,那这个程序就是单线程的程序。)
多线程:从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)
Java是通过java.lang.Thread 类的对象来代表线程的。
①定义一个子类MyThread继承线程类java.lang.Thread,,重写run()方法
public cLass MyThread extends Thread{
@override
public void run() {
//运行内容...
}
}
②创建MyThread类的对象
③调用线程对象的start()方法启动线程(启动后还是执行run方法的)
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
优点:编码简单。
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
1.定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
public cLass MyRunnable implements Runnable{
@0verride
public void run() {
//运行内容...
}
}
2.创建MyRunnable任务对象
3.把MyRunnable任务对象交给Thread处理。
Thread类提供的构造器 | 说明 |
public Thread( Runnable target) | 封装Runnable对 象成为线程对象 |
4.调用线程对象的start()方法启动线程
public cLass Main {
public static void main(String[] args) {
Runnable target = new MyRunnabLe();
new Thread(target).start();
}
}
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多一个Runnable对象。
public cLass Main {
public static void main(String[] args) {
Runnable target = new RunnabLe() {
@0verride
public void run() {
//运行内容...
}
};
new Thread(target).start();
//上述代码还可以进一步简化为下列形式
new Thread(new RunnabLe() {
@0verride
public void run() {
//运行内容...
}
}).start();
//进一步简化
new Thread(() -> {
//运行内容...
}).start();
}
}
假如线程执行完毕后有一些数据需要返回,前边两种方式重写的run方法均不能直接返回结果。在JDK 5.0提供了Callable接口和FutureTask类来实现。这种方式最大的优点是可以返回线程执行完毕后的结果。
利用Callable接口、FutureTask类来实现
①、创建任务对象
➢定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
import java.util.concurrent.Callable;
public class Time implements Callable {
private String st;
public Time(String st) {
this.st = st;
}
public String call() throws Exception {
String n = "1" + st;
//内容...
return n;
}
}
➢把Callable类型的对象封装成FutureTask (线程任务对象,是一个任务对象,实现了Runnable对象;可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。)
②、把线程任务对象交给Thread对象。
③、调用Thread对象的start方法启动线程。
④、线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class test {
public static void main(String[] args) throws Exception {
Callable call = new Time("100");
FutureTask f1 = new FutureTask<>(call);
new Thread(f1).start();
String rs = f1.get();
System.out.println(rs);
}
}
FutureTask提供的构造器 | 说明 |
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
FutureTask提供的方法 | 说明 |
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
Thread提供的常见构造器 | 说明 |
public Thread(String name) | 可以为当前线程指定名称 |
public Thread( Runnable target) | 封装Runnable对象成为线程对象 |
public Thread( Runnable target, String name) | 封装Runnable对象成为线程对象,并指定线程名称 |
Thread提供的常用方法 | 说明 |
public void run( ) | 线程的任务方法 |
public void start() | 启动线程 |
public String getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
public void setName(String name ) | 为线程设置名称 |
public static Thread currentThread( ) | 获取当前执行的线程对象 |
public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
public final void join()... | 让调用当前这个方法的线程先执行完! |
Thread类还提供了诸如:yield、 interrupt、 守护线程、线程优先级等线程的控制方法,在开发中很少使用
线程安全问题:多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。可以使用线程同步解决线程安全问题
加锁是线程同步的常见方案
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
同步代码块
●作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入, 执行完毕后自动解锁,其他线程才可以进来执行
同步锁的注意事项
●对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
锁对象的使用规范,
●建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
●对于静态方法建议使用字节码(类名.class)对象作为锁对象。
●作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法底层原理
●同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
●如果方法是实例方法:同步方法默认用this作为的锁对象。
●如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
●Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
●Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
构造器 | 说明 |
public ReentrantLock() | 获得Lock锁的实现类对象 |
常用方法 | 说明 |
void lock() | 获得锁 |
void unlock() | 释放锁 |
●当多个线程 共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
Object类的等待和唤醒方法:
方法名称 | 说明 |
void wait( ) | 让当前线程等待并释放所占锁,直到另一个线程调用notify( )方法或notifyAll()方法 |
void notify() | 唤醒正在等待的单个线程 |
void notifyAll() | 唤醒正在等待的所有线程 |
注意:上述方法应该使用当前同步锁对象进行调用。
线程池:一个可以复用线程的技术。
不使用线程池的问题:用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的, 而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
DK 5.0起提供了代表线程池的接口:ExecutorService。
得到线程池对象:
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
方式二:使用Executors (线程池的工具类)调用方法返回不同特点的线程池对象。
构造器
参数一:corePoolSize:指定线程池的核心线程的数量。
参数二:maximumPoolSize:指定线程池的最大线程数量
参数三:keepAliveTime:指定临时线程的存活时间。
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
参数五:workQueue:指定线程池的任务队列。
参数六:threadFactory:指定线程池的线程工厂(用来创建线程)
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
ExecutorService pooL = new ThreadPooLExecutor(3,5,8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
线程池的注意事项
●新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
●核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
方法名称 | 说明 |
void execute(Runnable command) | 执行Runnable 任务 |
Future |
执行 callable 任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown( ) | 等全部任务执行完毕后,再关闭线程池! |
List |
立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
策略 | 详解 |
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常这是不推荐的做法 |
ThreadPoolExecutor.Discard0ldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor. CallerRunsPolicy | 由主线程负责调用任务的run(方法从而绕过线程池直接执行 |
Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
方法名称 | 说明 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
计算密集型的任务(计算等):核心线程数量= CPU的核数+ 1
I0密集型的任务(读取/通信等):核心线程数量= CPU核数* 2
Executors使用可能存在的陷阱
●大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
进程中的线程 是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
在同一个时刻上,同时有多个线程在被CPU调度执行。
多线程是并发和并行同时进行的。
线程的书名周期:线程从生到死的过程中,经历的各种状态及状态转换。
Java线程的状态:Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中。
线程状态 | 说明 |
NEW(新建) | 线程刚被创建,但是并未启动 |
Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting( 计时等待) | 同waiting状态,有几个方法(sleep,wait) 有超时参数,调用他们将进入Timed Waiting状态 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访向完毕后,再解锁。线程安全, 性能较差!(前边那几种锁就是悲观锁)
乐观锁:一开始不上锁, 认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。(用的CAS算法)
乐观锁用法:在定义变量时用给定的对象(整数用AtomicInteger,小数用AtomicBoolean...),在运算时也要给定的方法(在当前值上加一:incrementAndGet() 除此之外还有其它方法)