多线程
并行和并发的区别
- 并行是指两个或者多个事件在同一时刻发生;并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件(在多台服务器上同时处理多个任务,如Hadoop分布式集群),并发是在同一实体上的多个事件(在一台服务器上“同时”处理多个任务)。
- 并发编程的目标是充分利用处理器的每一个核,以达到最高的处理性能。
线程和进程的区别
- 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
- 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程可以并发执行。
- 进程在执行中拥有独立的内存单元,而多个线程共享内存资源。
创建
三种方法:
- 继承Thread
- 实现Runnable接口
- 实现Callable接口
本质上都是一种方式,后两种还是需要通过创建Thread对象来执行。
//1.继承Thread
public static void main(String[] args){
MyThread thread=new Mythread();
thread.start();
}
class MyThread extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+"running...");
}
}
上面的实例中有三个线程:main主线程、开启的thread线程、垃圾回收线程(后台线程)
//实现Runnable方法
public static void main(String[] args){
Mytask task=new MyTask();
new Thread(Task).start();
}
//本质是一个可执行的任务
public MyTask implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName()+"running...");
}
}
//实现Callable接口
public static void main(String[] args){
FutureTask futureTask=new FutureTask(new MyTask2());
new Thread(FutureTask).start();
System.out.println(FutureTask.get());//get方法得到call方法的返回值
}
//这里的泛型与返回类型对应
public MyTask2 implements Callable<T>{
//该方法是有返回值的
public Object call() throws Exception{
System.out.println(Thread.currentThread().getName()+"running...");
}
}
runnable和callable方法的区别
- run()没有返回值,call()有返回值
- run()不能抛出异常,call()可以
线程的生命周期
线程共有6种状态:new,runnable,blocked,waiting,timed waiting,terminated
要调用一个线程的状态,使用getState方法。
- new:使用new新建立的线程,该线程还没有开始运行其中的代码,此时状态为new。
- runnable:一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
- blocked:当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
- waiting:当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。例如Object.wait方法或者Thread.join方法。当调用notify()或notifyAll()时,或者join的线程执行结束后,会进入runnable状态
- timed waiting:有几个方法有一个超时参数调用它们导致线程进入timed waiting状态。如sleep(time),wait(time)。当休眠时间结束后,或者调用notify()或notifyAll()重新进入runnable状态。
- terminated:程序执行结束后,或者出现没有捕获的异常,线程进入terminated状态
sleep/wait/join/yield
- sleep方法的作用是让当前线程暂停指定的时间(毫秒)。其与wait方法的区别:
1.sleep定义在Thread上,wait定义在Object上
2.sleep不会释放锁,wait会释放锁
3.sleep可以使用在任何代码块,wait必须在同步方法或同步代码块执行 - t.join()方法会使当前运行线程(或者说调用t.join()的线程)进入等待池并等待 t 线程执行完毕后才会被唤醒。
- yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其
他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
为什么wait要定义在Object中,而不定义在Thread中?
因为Java的锁是对象级别的,而不是线程级别的。
为什么wait必须写在同步代码块中?
原因是避免cpu切换到其他线程,而其他线程又提前执行了notify方法,这样就达不到我们的预期(先wait再由其他线程来唤醒),所以需要一个同步锁来保护。
线程属性
线程优先级
在java程序设计语言中,每一个线程有一个优先级,默认情况下,一个线程继承它的父线程的优先级,可以用setPriority方法设置优先级,从MIN_PRIORITY(1)到MAX_PRIORITY(10),NORM_PRIORITY定义为5。
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。注意线程的优先级是高度依赖于系统的,java线程的优先级被映射到宿主机平台的优先级上,优先级的个数也许更多,也许更少。因此,不要将程序构建为功能正确性依赖于优先级。
void yield()
导致当前线程处于让步状态。如果有其他的可运行线程与此线程同样高的优先级,那么这些线程接下来会被调度。
守护线程
可以通过调用t.setDaemon(true)
将线程转换为守护线程。守护线程的唯一用途就是为其他线程提供服务。当只剩下守护线程时,虚拟机就退出了。守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
线程安全
当多个线程访问一个对象时,如果不用进行额外的同步操作,调用这个对象的行为就可以获得正确的结果,我们就说这个对象是线程安全的。
ThreadLocal
作用:实现在线程的上下文传递对象
为每一个线程创建一个副本
public class ThreadLocalTest{
private static final ThreadLocal<Long> threadLocal=new ThreadLocal<>();
public static void main(String[] args){
Task task=new Task();
new Thread(task).start();
Thread.sleep(millis:10);
new Thread(task).start();
}
class Task implements Runnable{
public void run(){
Long result=threadLocal.get();
if(result==null){
threadLocal.set(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"->"+threadLocal.get());
}
}
}
}
为什么可以为每个线程创建一个副本?、
分析get的源码:
Long result = threadLocal.get();
public T get() {
//1.获取当前线程
Thread t = Thread.currentThread();
//2,获取到当前线程对应的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//3.以threadLocal为key,获取到entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.获取对应entry的value,就是我们存放到里面的变量的副本
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
set:
threadLocal.set(System.currentTimeMillis());
public void set(T value) {
//1.获取到当前线程
Thread t = Thread.currentThread();
//2.获取当前线程对应的map
ThreadLocalMap map = getMap(t);
if (map != null)
//3.往map存放一个键值对
//this ThreadLocal
//value 保存的副本
map.set(this, value);
else
createMap(t, value);
}
可以得到结论:每个线程都会有对应的map,map来保存键值对。
ThreadLocal在实际开发中解决了什么问题?
public class UserService {
//省略接口的声明
private UserDao userDao = new UserDao();
private LogDao logDao = new LogDao();
//事务的边界放在业务层
//JDBC的封装,connection
public void add(){
userDao.add();
logDao.add();
}
}
public class UserDao {
public void add(){
System.out.println("UserDao add。。。");
//创建connection对象
//connection.commit();
//connection.rollback();
}
}
public class LogDao {
public void add(){
System.out.println("LogDao add。。。");
//创建connection对象
//connection.commit();
//connection.rollback();
}
}
如果代码按上面的方式来管理connection,我们还可以保证service的事务控制吗?
这是不行的,假设第一个dao操作成功了,那么它就提交事务了,而第二个dao操作失败了,它回滚了事务,但不会影响到第一个dao的事务,因为上面这么写是两个独立的事务。
上面的根源就是两个dao操作的是不同的connection,所以,我们保证是同个connection即可:
//事务的边界放在业务层
//JDBC的封装,connection
public void add(){
Connection connection = new Connection();
userDao.add(connection);
logDao.add(connection);
}
上面的方式代码不够优雅
public class ConnectionUtils {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static Connection getConnection(){
Connection connection = threadLocal.get();
if(connection == null){
connection = new Connection();
threadLocal.set(connection);
}
return connection;
}
}
public class UserDao {
public void add(){
System.out.println("UserDao add。。。");
//创建connection对象
//connection.commit();
//connection.rollback();
Connection connection = ConnectionUtils.getConnection();
System.out.println("UserDao->"+connection);
}
}
线程池
- 在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
- 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
组成
一个线程池包括以下四个基本组成部分:
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括:创建线程池,销毁线程池,添加新任务;
- 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
参数
Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
:核心线程数量(核心线程即使没在使用也不会被回收)
maximumPoolSize
:线程池可容纳最大线程数量
keepAliveTime
:非核心线程最长可以保留的时间
unit
:即这个时间的单位
workQueue
:等待队列。任务可以储存在等待队列中等待被执行,执行先入先出原则。
threadFactory
:创建线程的线程工厂。
handler
:拒绝策略,可以拒绝执行某些任务
执行流程
任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务;如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行;如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
常见线程池
- CachedTreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为无限大。当有需要时就创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
- SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务。有核心线程也有非核心线程,非核心线程的数量也为无限大。适用于执行周期性的任务。
- SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
- FixedThreadPool:定长的线程池,有核心线程,没有非核心线程。