java多线程
在以前参加的项目中,对于从netty客户端接收的报文信息进行处理时,使用了比较多java线程池方面的内容,以及对于日常接触到的进程、线程、线程生命周期和线程创建方式、线程相关方法等相关信息较多,故查阅相关文献资料以及在线技术文章等,对于java线程和多线程的相关知识做了简要汇总总结,为读者提供参考,也便于使自己保持持续学习以及进一步深入研究。文中不免疏漏之处,望读者予以指正,不胜感激!
1. 进程与线程
1.1 进程
进程是处于运行过程中的程序,并且具有一定独立的功能,进程是系统进行资源分配和调度的一个独立单位.
1.2 线程
(1)概念
线程:程序执行流的最小单元,可以理解为:进程中独立运行的子任务。
(2)六种状态
线程五个状态:new、runnable、blocked、waiting、timed_waiting和terminated。
(1)New: 新建状态,线程对象已经创建,但尚未启动
(2)Runnable:就绪状态,可运行状态,调用了线程的start方法,已经在java虚拟机中执行,等待获取操作系统资源如CPU,操作系统调度运行。
(3)Blocked:堵塞状态,线程等待锁的状态,等待获取锁进入同步块/方法或调用wait后重新进入需要竞争锁
(4)Waiting:等待状态。等待另一个线程以执行特定的操作。调用以下方法进入等待状态:Object.wait()、Thread.join()、LockSupport.park
(5)Timed_waiting:线程等待一段时间,
调用带参数的方法:Thread.sleep、objct.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil
(6)Terminated:进程结束状态。
2. 三种线程方式 Thread/Runnable/Callable
2.1 Thread
1)调用方法
(1)定义Thread 类的子类,并重写该类的run() 方法,该run() 方法的方法体就代表类线程需要完成的任务。
因此把run() 方法称为线程执行体。
(2)创建 Thread 子类的实例,即创建线程对象。
(3)调用线程的star()方法来启动该线程。
2)示例
public class Thread1 extends Thread{
private String name;
public Thread1(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("==="+this.name);
}
}
调用:
new Thread1("AAA").start();
new Thread1("BBB").start();
new Thread1("CCC").start();
执行结果:
===BBB
===CCC
===AAA
2.2 Runnable
1)调用方法
(1)定义 Runnable 接口的实现类,并重写该接口的run()方法,该run() 方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable 实例类的实例,此实例作为 Thread 的 target 来创建Thread 对象,该Thread 对象才是真正的对象。
2)示例
public class Thread2 implements Runnable {
private int count = 5;
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"============"+(count--));
}
}
调用:
Runnable thread2 = new Thread2();
new Thread(thread2).start();
new Thread(thread2).start();
new Thread(thread2).start();
new Thread(thread2).start();
new Thread(thread2).start();
new Thread(thread2).start();
执行结果:
Thread-1============5
Thread-4============2
Thread-2============3
Thread-0============4
Thread-5============0
Thread-3============1
2.3 Callable
1)调用方法
(1)创建callable接口的实现类,并实现call() 方法,该call() 方法将作为线程的执行体,且该call() 方法是有返回值的。
(2)创建 callable实现类的实例,使用 FutureTask 类来包装Callable对象,该FutureTask 对象封装 call() 方法的返回值。
(3)使用FutureTask 对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获取子线程执行结束后的返回值。
2)示例
public class Thread3 implements Callable<Long> {
private String name;
private long begin;
private long end;
public Thread3(String name, long begin, long end) {
this.name = name;
this.begin = begin;
this.end = end;
}
@Override
public Long call() throws Exception {
System.out.println(name + " 执行中.......");
long sum = 0;
for (long i = begin; i <= end; i++) {
sum += i;
}
return sum;
}
}
调用:
Thread3 thread3 = new Thread3("Task-01",1,50);
FutureTask<Long> task = new FutureTask(thread3);
new Thread(task).start();
try {
Long result = task.get();
System.out.println("result:"+result);
}catch (Exception e){
e.printStackTrace();
}
执行结果:
Task-01 执行中.......
result:1275
2.3 三种方式对比
采用继承Thread 类的方式创建多线程
劣势: 已经继承Thread类不能再继承其他父类。
优势: 编写简单
采用继承Runnable,Callable 接口的方式创建多线程
劣势: 编程稍微有点复杂,如果需要访问当前线程必须使用Thread.currentThread()
优势:
(1)还可以继承其他类
(2)多个线程可以共享一个target 对象,所以非常适合多个相同的线程来处理同一份资源的情况,
从而将cpu,代码和数据分开,形成清晰的模型,较好的体现类面向对象的思想。
3. 线程常见方法
sleep、yield、wait、notify/notifyAll、join、interrupt、synchronized、volatile
(1)sleep
使用:Thread.sleep(long millis) //参数为毫秒
作用:sleep就是让线程睡眠,交出CPU
注意点:它不会释放锁(即如果该线程持有该对象的锁,那么sleep后其他线程也无法访问该对象)
(2)yield
使用:Thread.yield()
作用: yield 让当前线程交出CPU,但是不能控制时间
注意点: 不会释放锁,但是yield只能让等于或大于自己优先级的线程有机会获得CPU执行机会
(3)wait
使用:object.wait()
object.wait(long timeout)
作用:一般作用于线程共享变量对象,使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,
即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法
(4)notify/notifyall
使用:object.notify()
object.notifyAll()
作用:配合wait使用,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。
所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
(5)join
使用: join()
join(long millis) //参数为毫秒
作用:让当前线程等待子线程结束之后才能运行
注意点:
join实现的顺序如下
1.假设在main线程里运行了子线程A
2.接着设置 A.join()
3.接着main线程会处于waiting状态
4.直到子线程完成后通知main线程结束等待
特别注意:
1.主线程调用的方法就是主线程获得锁,子线程run方法里的调用才是子线程获得锁。
2.所以join通过wait来等待的是主线程,子线程是不会释放锁的
(6)interrupt
使用:interrupt()
作用:中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果是线程死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait()、sleep()、join()时,才马上会抛出 InterruptedException。
(7)synchronized
Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
(8)volatile
volatile是变量修饰符;让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”取;
volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
本段内容汇总线上各种资料介绍了各个方法和修饰符的简要用法以及相关作用,后续会进一步研究给出相关的操作示例进行验证纠正完善,以及后续研究相关的底层实现原理等。
4. java线程池
(1)线程池的优势:
1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
(2)创建线程池
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
1、corePoolSize--线程池基本大小:当向线程池提交一个任务时,
若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,
直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),
也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2、maximumPoolSize--线程池最大大小:线程池所允许的最大线程个数。
当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime--线程存活保持时间:当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,
那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、unit--线程活动保持时间的单位:可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),
毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
5、workQueue--任务队列:用于传输和保存等待执行任务的阻塞队列。
6、threadFactory--线程工厂:用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,
threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
7、handler--线程饱和策略:当线程池和队列都满了,再加入线程会执行此策略。
(3)Executors类提供了4种不同的线程池
newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor
1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。
(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,
适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4、newScheduledThreadPool:适用于执行延时或者周期性任务。
(4)使用示例
long begin = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
Thread3 task01 = new Thread3("Task-01", 1, 50);
Thread3 task02 = new Thread3("Task-02", 51, 100);
//得到的结果集
Future<Long> resultSet01 = threadPool.submit(task01);
Future<Long> resultSet02 = threadPool.submit(task02);
try {
System.out.println("最后的结果是:"+(resultSet01.get()+resultSet02.get()));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally{
threadPool.shutdown();
}
5. 参考资料
[1] java多线程的几种状态.
[2] Thread基本函数详解.
[3] Java多线程学习之wait、notify/notifyAll 详解.
[4] Java并发编程:线程池的使用.