java-多线程
java-多线程
start与run
run方法只是Thread的一个普通方法的调用,而且是在主线程中执行
start方法会创建一个新的子线程并启动,通过此线程去调用run方法
栗子
#MyThread.java public class MyThread extends Thread{ @Override public void run() { System.out.println("Thread:" + Thread.currentThread().getName()); } } #Test.java public class Test { public static void main(String[] args) { Thread mt = new MyThread(); mt.run(); //main //mt.start(); //Thread-0 } }
Thread与Runnable
Runnalbe并不具有多线程的特性,它依赖Thread中的start方法去创建一个子线程,在子线程中调用Thread
中实现的run方法去执行相关的业务逻辑,才能让这个类具有多线程的特性。
Runnable中没有start方法,只有Thread中才有,Thread构造函数可以传入Runnable子类实例,我们可以通过Thread类来
启动Runnable来实现多线程。
Thread与Runnable的关系
Thread是实现Runnable接口的类,通过start给Runnable的run方法附上了多线程的特性;
因类的单一继承原则,推荐使用实现Runnable接口
(为了提升系统的可扩展性,往往使业务类实现Runnable接口,将业务逻辑封装在run方法中,便于给普通类附上多线程的特性)
栗子
Thread
//MyThread public class MyThread extends Thread{ private String name; public MyThread(String name){ this.name = name; } @Override public void run() { for (int i = 0; i < 10; i++){ System.out.println("Thread-" + this.name + ", i = " + i); } } } //Test.java public class Test{ public static void main(String[] args) { Thread mt1 = new MyThread("mt1"); Thread mt2 = new MyThread("mt2"); mt1.start(); mt2.start(); } }
Runnable
//MyRunnable.java public class MyRunnable implements Runnable{ private String name; public MyRunnable(String name){ this.name = name; } @Override public void run() { for(int i = 0; i < 10; i++){ System.out.println("Thread-" + this.name + ", i = " + i); } } } //Test.java public class Test { public static void main(String[] args) { Runnable mn1 = new MyRunnable("mn1"); Runnable mn2 = new MyRunnable("mn2"); Thread mt1 = new Thread(mn1); Thread mt2 = new Thread(mn2); mt1.start(); mt2.start(); } }
为什么使用同步
java支持多线程并发操作,当多个线程操作共享的数据时,将会导致数据不准确,
线程之间发生冲突,因此引入了同步锁以避免某个线程没有完成数据操作之前被其他线程
调用,从而保证了数据的唯一性和准确性。
线程的传参和返回值
和线程有关的业务逻辑需要放在run方法中去执行,run方法既无参数,也无返回值
如何给run()函数传参
(1)构造函数传参
(2)成员变量传参
(3)回调函数传参
如何处理线程的返回值
程序的执行依赖子线程的返回值进行的
(1)主线程等待
描述:让主线程循环等待直到目标子线程返回值为止
缺点:需要自己实现循环等待逻辑,若等待的变量太多,代码会臃肿,并且无法做到精准控制
(2)使用Thread中的join()阻塞当前线程以等待子线程处理完成
实现起来比主线程等待更简单,但是精准力度依然不够,例如当线程1任务执行一半时,线程2开始执行,这样的精准力度无法控制
(3)通过Callable接口实现获取线程的返回值:通过FutureTask或者线程池
执行Callable任务获取一个Future对象,调用Future对象的get方法可以获取Callable返回的object。
Future构造函数接收Callable实例对象,Future对象的isDone用来判断传给的call方法是否已执行完成
get方法用来阻塞调用它的当前线程直到Callable的实现类的call方法执行完毕能取到返回值,能精确获取
子线程处理完成之后的返回值。
get有参方法,如果在timeout时间内还没有获取Callable实现类中的call方法的返回值,它就会抛出异常。
Thread可以接收Runnable实例,FutureTask implements RunnableFuture, RunnaFuture extends Runnable。
Runnale接口的优势,我们的类继承或实现Runnable,构造函数都可以接收它们去做相应的处理。
线程池用完一定要关闭,线程池的优点:可以提交多个实现Callable方法的类去让线程池并发的去处理结果,
便于对实现Callable方法的类执行方式做统一管理。
线程状态
(1)新建(NEW):创建后尚未启动的线程的状态
新建一个线程,还没有调用它的start方法
(2)运行(Runnable):包含Running和Ready
包含操作系统中的Running和Ready,处于此状态的线程可能正在执行,也可能在等待CPU为它分配时间
比如创建一个主线程后调用它的start方法,这个线程处于Runnable状态,由于该状态有两个子状态,处于
Running状态的线程位于可运行线程中,等待被CPU调度选中,获取CPU的使用权,处于Ready状态的线程
位于线程池中,等待等待被CPU调度选中,获取CPU的使用权,之后变成Running状态的线程。
(3)无限期等待(Waiting):不会被分配CPU执行时间,需要显示被唤醒
有三种方式会让线程进入无限期等待状态:
没有设置Timeout参数的Object.wait()方法
没有设置Timeout参数的Thread.join()方法
LockSupport.park()方法
(4)限期等待(Timed Waiting):在一定时间后会由系统自动唤醒
有五种方式可以让线程进入限期等待状态
Thread.sleep()方法
设置了Timeout参数的Object.wait()方法
设置了Timeout参数的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUntil()方法
(5)阻塞(Blocked):等待获取排它锁
阻塞状态与等待状态的区别是,阻塞状态在等待获取一个排它锁,这个事件在另一个线程放弃这个锁的时候发生,
等待状态则是在等待一段时间后或是在有唤醒动作时发生,在程序等待进入同步区域时,线程将进入Blocked状态,
比如,当某个线程进入synchronized修饰的代码块或方法中,即获取锁去执行时,其他想进入次代码块或方法的线程
就只能等着,它们的状态便是Blocked。
(6)结束(Terminated):已经终止的线程状态,线程已结束执行
当线程的run方法执行完成或主线程的main方法执行完成时,就认为它终止。
此时这个线程对象也许是活的,但是它已经不是一个单独执行的线程,
一旦终止就不能复生。在终止的线程上调用start方法会抛出异常java.lang.IllegalThreadStateException。
sleep与wait
(1)sleep是Thread类一个静态方法,用于暂停当前线程的活动,wait是Object中的方法
(2)sleep方法可以用在任何地方,wait方法只能用在synchronized修饰的代码块或方法中
(3)本质区别:sleep只会出让CPU,不会导致锁行为的改变,wait不仅出让CPU,还会释放已占用的同步资源锁
比如,当前线程有锁,sleep不会让其他线程释放锁,只会主动让出 CPU,让出后就可以执行其他任务了,
wait释放锁以便其他正在等待该资源的线程得到该资源进而运行,wait必须写在synchroinzed中的原因是,
只有获取锁,才能释放锁。
notify与notifyAll
(to be fixed...)
对于java虚拟机中运行程序的每个对象来说,都有两个池,锁池和等待池,而这两个池又与wait,notify,notifyAll,synchroized
相关
notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
yield
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意出让CPU使用的按时,但是
线程调度器可能会忽略这个暗示,此外yield对锁的行为没有影响。
线程池
遇到的问题
web开发中,服务器需要接收并处理请求,所以会为一个请求来分配一个线程来进行处理,
如果并发请求的数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程。
从而降低系统的效率,可能出现服务器在为每个请求创建新线程和销毁线程的时间和消耗的系统资源
要比实际处理请求的时间和资源更多,那么有没有一种方法去重用线程去执行新的请求呢?答案是肯定的。
怎么创建线程池
利用Executors创建不同的线程池满足不同场景的需求
1)newFixedThreadPool(int nThreads)
指定工作线程数量的线程池
2)newCachedThreadPool()
处理大量短时间工作任务的线程池
a.试图缓存线程并重用,当无缓存可用时,就会创建新的工作线程
b.如果线程闲置的时间超过阈值,则会被终止并移出缓存
c.系统长时间闲置时,不会消耗什么资源
3)newSingleThreadExecutor()
创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个来取代它
4)newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)
定时或周期性的工作调度,两者的区别在于单一工作线程还是多个线程
5)newWorkStealingPool()
内部会构建ForkJoinPool,利用working-stealing算法,并行处理任务,不保证处理顺序
为什么要使用线程池
1)降低资源消耗
通过重复利用已创建的线程来降低线程创建和销毁造成的消耗
2)提高线程的可管理性
线程是稀缺资源,如果无限制创建不仅消耗系统资源还会降低系统的稳定性,
使用线程池可以进行统一的分配,调优和监控
线程池的大小如何选定
1)CPU密集型:线程数=按照核数或核数+1
任务主要是计算,那么CPU的处理能力就是稀缺首要的资源,此时大幅度增加线程来增加处理能力并不可行,
线程太多可能产生频繁的上下文切换导致许多不必要的开销
2)IO密集型:线程数=CPU核数*(1+平均等待时间/平均工作时间)
处理较多需要等待的任务,比如IO操作