本人参考了:
线程同步
https://www.cnblogs.com/XHJT/p/3897440.html
线程间通讯
wait/notify
https://www.cnblogs.com/hapjin/p/5492619.html
线程池
https://blog.csdn.net/liuchuanhong1/article/details/52042182
定时任务调度
http://blog.51cto.com/zhangfengzhe/2064092
一、线程的基本概念和用法
1、 什么是线程和多线程
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。
在单个程序中同时运行多个线程完成不同的工作,称为多线程。
2、 进程,单线程
3、 线程的状态
新建状态new
就绪start()
运行run()
死亡stop(),destory()
阻塞sleep(睡眠)、suspend(挂起)
等待wait()
3.1 getState()获取当前线程的状态
获取的值是枚举
NEW(new)
至今尚未启动的线程处于这种状态
RUNNABLE(runnable)
正在Java虚拟机中执行的线程处于这种状态。
WAITING(waiting)
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
BLOCKED(blocked)
受阻塞并等待某个监视器锁的线程处于这种状态
TIMED_WAITING(timed_waiting)
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED(terminated)
已退出的线程处于这种状态
3.2 jstack-l进程id
在cmd中执行(jstack-l进程id)命令,可显示该进程的线程状态
4、 创建线程的方法
4.1 通过实现Runnable接口;
publicclassMyRunnableimplementsRunnable{
privateThreadt;
@Override
publicvoidrun(){
System.out.println(Thread.currentThread().getName());
}
publicvoidstart(){
//TODOAuto-generatedmethodstub
if(t==null){
t=newThread(this);
t.start();
}
}
}
4.2 通过继承Thread类本身;
4.3 通过Callable和Future创建线程。
4.4 通过匿名内部类创建
newThread(){
publicvoidrun(){
//打印当前线程名
System.out.println(Thread.currentThread().getName());
}
}.start();
newThread(newRunnable(){
publicvoidrun(){
System.out.println(Thread.currentThread().getName());
}
}).start();
4.5 使用Lamda表达式简写实现Runnable接口的线程
newThread(()->{
System.out.println(Thread.currentThread().getName());
}).start();
5、 实现Runnable接口和继承Thread类来创建线程那种方法好,为什么?
6、 启动线程
start()启动线程
注意:不能直接调用线程的run()方法,如果直接调用,会把它看成普类的普通方法调用,还是单线程
7、 线程常用的方法
7.1 wait()
使线程处于等待状态
7.2 notify/notifyAll()
唤醒等待/(所有等待)的线程
在调用wait(),notify()或notifyAll()的时候,必须先获得锁,且状态变量须由该锁保护,而固有锁对象与固有条件队列对象又是同一个对象。也就是说,要在某个对象上执行wait,notify,先必须锁定该对象,而对应的状态变量也是由该对象锁保护的
7.3 join(longmillis)
1.其他线程不执行,等待当前线程执行指定的时间(毫秒millis)
2.如果该线程在指定时间没有执行完,就不在等待,继续执行主线程,但该线程不会停止也会执行
3.默认是0millis代表forever
@Test
publicvoidtest4()throwsInterruptedException{
Threadthread=newThread(){
@Override
publicvoidrun(){
for(inti=0;i<10;i++){
System.out.println("i="+i);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
}
}
}
};
thread.start();//先启动
//start()在前join()在后
thread.join();
for(intj=0;j<10;j++){
System.out.println("j="+j);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
}
}
System.out.println(Thread.currentThread().getName());
}
7.4 isAlive()
isAlive测试线程是否存活
true已启动,尚未死亡
false线程死亡
/**
*Testsifthisthreadisalive.Athreadisaliveifithas
*beenstartedandhasnotyetdied.
*
*@return<code>true</code>ifthisthreadisalive;
*<code>false</code>otherwise.
*/
publicfinalnativebooleanisAlive();
7.5 sleep()
publicstaticnativevoidsleep(longmillis)throwsInterruptedException;
1.在指定的毫秒数内让当前正在执形的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响.
2.Java并不保证线程在阻塞给定的时间后能够马上执行,事实上这几乎是不可能的事情.在阻塞时间到了之后.线程进入就绪状态,继续执行的时机取决于Java虚拟机线程调度机制,唯一能够确定的是.线程中断执行的时间是大于等于给定的阻塞时长的,因此不要将sleep用作精确度要求非常高的定时任务调度
3.不会释放锁资源
7.6 yield()
publicstaticnativevoidyield();
1.暂停当前当前正在执行的线程对象,并执行其他线程
2.yield()方法只是使当前线程重新回到就绪可执行状态,所以执行yield()后线程有可能进入就绪状态后马上又被执行,只能是相同或更高优先级的线程有执行的机会
3.不会释放锁资源
8、 sleep()和yield()的区别
sleep() | yield() |
使当前线程进入被阻塞状态 | 使当前线程转入就绪可执行状态 |
即使没有其他等待运行的线程,当前线程也会等待指定的时间 | 没有其他等待运行的线程,当前线程会马上恢复执行 |
其他等待执行的线程运行机会是相等的 | 其他等待执行的线程,会将优先级相同或更高的运行 |
9、 线程的优先级
setPriority()用于设置线程的优先级
Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行(注意是更高的几率,而不是优先级高的一定有优势,但是优先级在某一些线程调度方法中有特定的作用)
Java线程的优先级是一个整数,其取值范围是1(Thread.MIN_PRIORITY)一10Thread.MAX_PRIORITY
/**
*Theminimumprioritythatathreadcanhave.
*/
publicfinalstaticintMIN_PRIORITY=1;
/**
*Thedefaultprioritythatisassignedtoathread.
*/
publicfinalstaticintNORM_PRIORITY=5;
/**
*Themaximumprioritythatathreadcanhave.
*/
publicfinalstaticintMAX_PRIORITY=10;
@Test
publicvoidtest3(){
Threadt=newThread(){
@Override
publicvoidrun(){
for(inti=0;i<30;i++){
System.out.println("i="+i);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
}
}
}
};
Threadt2=newThread(){
@Override
publicvoidrun(){
for(intm=0;m<30;m++){
System.out.println("m="+m);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
}
}
}
};
Threadt3=newThread(){
@Override
publicvoidrun(){
for(intj=0;j<30;j++){
System.out.println("j="+j);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
}
}
}
};
t.setPriority(1);
t.start();
t2.start();
t3.start();
try{
Thread.sleep(10000);
}catch(InterruptedExceptione){
}
}
10、 终止线程
10.1 stop()已经废弃
publicclassTest3{
publicstaticvoidmain(String[]args){
Threadt=newThread(){
publicvoidrun(){
System.out.println(Thread.currentThread().getName());
for(inti=0;i<100;i++){
try{
Thread.sleep((long)0.1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("i="+i);
}
};
};
t.start();
try{
Thread.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
t.stop();
}
}
10.2 使用标记
publicclassMyThreadextendsThread{
booleanflag=true;
@Override
publicvoidrun(){
while(flag){
System.out.println("xxx");
}
}
publicvoidstopThred(){
flag=false;
}
}
publicclassTest5{
publicstaticvoidmain(String[]args){
MyThreadt=newMyThread();
t.start();
try{
Thread.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
t.stopThred();
}
}
10.3 interrupt()中断
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?”使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使给滩提前结束阻塞状态,退出堵塞代码
@Test
publicvoidtest1(){
//新建一个线程
Threadt=newThread(){
@Override
publicvoidrun(){
for(inti=0;i<30;i++){
System.out.println("i="+1);
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
System.out.println("线程中断");
return;
}
}
}
};
//启动
t.start();
try{
Thread.sleep(3000);
}catch(InterruptedExceptione){
}
//中断正在阻塞的线程
t.interrupt();
}
interrupt()方法并不能阻断I/O阻塞或线程同步引起的线程阻塞
关闭底层!I/O通道,人为引发异常从而进行共享变且盆新赋值而跳出线程的run()方法
nio支持非阻塞式的事件驱动读取操作,在这种模式下,不需要关闭底层资源即可通过interrupt()方法直接中断其等待操作
11、 守护线程和用户线程
11.1 守护线程(DaemonThread)
是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护绍涯结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止.
@Test
publicvoidtest2(){
Threadt=newThread(){
@Override
publicvoidrun(){
for(inti=0;i<30;i++){
System.out.println("i="+i);
try{
Thread.sleep(100);
}catch(InterruptedExceptione){
}
}
}
};
//将线程设为守护线程
t.setDaemon(true);
//启动
t.start();
System.out.println(Thread.currentThread().getName());
}
@Test
publicvoidtest2(){
Threadt=newThread(){
@Override
publicvoidrun(){
for(inti=0;i<30;i++){
System.out.println("i="+i);
try{
Thread.sleep(100);
}catch(InterruptedExceptione){
}
}
}
};
//将线程设为守护线程
t.setDaemon(true);
//启动
t.start();
try{
Thread.sleep(5000);
}catch(InterruptedExceptione){
}
System.out.println(Thread.currentThread().getName());
}
11.2 用户线程(UserThread)
用户线程奋用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的退出:如果用户线程已经全部退出运行了,只剩下守护妇幼呈存在了,虚拟牛了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序了。因为没有了
上图,是用户线程,没有停止,直到线程结束才停止
二、线程同步
1、 为何要使用同步?
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
2、 实现同步的方法有哪些?
1. 同步方法
2. 同步代码块
3. 使用特殊域变量(volatile)实现线程同步
4. 使用重入锁实现线程同步
5. 使用局部变量实现线程同步
6. 使用阻塞队列实现线程同步
7. 使用原子变量实现线程同步
3、 未实现同步的情况
packagecom.hx.test;
publicclassTest{
publicstaticintcount=0;
publicstaticvoidmain(String[]args){
Runnablerunnable=newRunnable(){
publicvoidrun(){
for(inti=0;i<100;i++){
try{
Thread.sleep(1);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"count:"+(++count));
}
}
};
Threadt1=newThread(runnable);
Threadt2=newThread(runnable);
t1.start();
t2.start();
try{
Thread.sleep(2000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println("最终结果:"+count);
}
}
由于操作系统,cpu,等因素影响,每次运行的结果有可能不一样
两个线程读到了了同一数据,自增的值一样,修改时都修改了count,结果造成数据不准确
例如
a线程读到的值为1,自增后为2,结果count=2
b线程读到的值为1,自增后为2,结果count=2
但我们想要的是a和b线程不能同时读到同一数据
4、 同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
publicsynchronizedvoidsave(){}
注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
packagecom.hx.test;
publicclassTest2{
publicstaticintcount=0;
publicstaticvoidmain(String[]args){
Runnablerunnable=newRunnable(){
synchronizedpublicvoidrun(){
for(inti=0;i<100;i++){
try{
Thread.sleep(1);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"count:"+(++count));
}
}
};
Threadt1=newThread(runnable);
Threadt2=newThread(runnable);
t1.start();
t2.start();
try{
Thread.sleep(2000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println("最终结果:"+count);
}
}
5、 同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){
//需要同步的代码
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
packagecom.hx.test;
publicclassTest2{
publicstaticintcount=0;
publicstaticvoidmain(String[]args){
Runnablerunnable=newRunnable(){
publicvoidrun(){
for(inti=0;i<100;i++){
try{
Thread.sleep(1);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
synchronized(this){
System.out.println(Thread.currentThread().getName()+"count:"+(++count));
}
}
}
};
Threadt1=newThread(runnable);
Threadt2=newThread(runnable);
t1.start();
t2.start();
try{
Thread.sleep(2000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println("最终结果:"+count);
}
}
6、 使用特殊域变量(volatile)实现线程同步
1.volatile关键字为域变量的访问提供了一种免锁机制,
2.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
3.因此每次使用该域就要重新计算,而不是使用寄存器中的值
4.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
5.在上面的例子当中,只需在count前面加上volatile修饰,即可实现线程同步。
实际测试后,volatile不能保证实现线程同步
packagecom.hx.test;
publicclassTest2{
publicstaticvolatileintcount=0;
publicstaticvoidmain(String[]args){
Runnablerunnable=newRunnable(){
publicvoidrun(){
for(inti=0;i<100;i++){
try{
Thread.sleep(1);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"count:"+(++count));
}
}
};
Threadt1=newThread(runnable);
Threadt2=newThread(runnable);
t1.start();
t2.start();
try{
Thread.sleep(2000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println("最终结果:"+count);
}
}
7、 使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
ReenreantLock类的常用方法有:
ReentrantLock():创建一个ReentrantLock实例
lock():获得锁
unlock():释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
packagecom.hx.test;
importjava.util.concurrent.locks.Lock;
importjava.util.concurrent.locks.ReentrantLock;
publicclassTest2{
publicstaticintcount=0;
//需要声明这个锁
privatestaticLocklock=newReentrantLock();
publicstaticvoidmain(String[]args){
Runnablerunnable=newRunnable(){
publicvoidrun(){
for(inti=0;i<100;i++){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"count:"+(++count));
}finally{
lock.unlock();
}
}
}
};
Threadt1=newThread(runnable);
Threadt2=newThread(runnable);
t1.start();
t2.start();
try{
Thread.sleep(2000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
System.out.println("最终结果:"+count);
}
}
8、 使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal类的常用方法
ThreadLocal():创建一个线程本地变量
get():返回此线程局部变量的当前线程副本中的值
initialValue():返回此线程局部变量的当前线程的"初始值"
set(Tvalue):将此线程局部变量的当前线程副本中的值设置为value
package com.hx.test;
public class Test2 {
public static ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws Exception {
Runnable runnable = new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
count.set(count.get() + 1);
System.out.println(Thread.currentThread().getName() + "count:" + count.get());
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t1.join();
t2.start();
Thread.sleep(1000);
System.out.println("最终全局count" + count.get());
}
}
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
9、 使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。
使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
本小节主要是使用LinkedBlockingQueue<E>来实现线程的同步
LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),关于队列以后会详细讲解
LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞
注:BlockingQueue<E>定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:
add()方法会抛出异常
offer()方法返回false
put()方法会阻塞
package com.hx.test;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
public class Test3 {
// 阻塞队列
private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
// queue相当于仓库
// 定义生产商品的个数
private static int count = 10;
public static void main(String[] args) throws Exception {
// 生产商品的线程
Runnable runnable1 = new Runnable() {
public void run() {
System.out.println("启动生产者线程");
while (true) {
if (queue.size() < count && queue.size() >= 0) {
// 生产的商品编号
int num = (int) (Math.random() * 100);
try {
queue.put(num);// 添到仓库
System.out.println("生产者生产了" + num + "号商品,仓库还有:" + queue.size());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
// 消费商品的线程
Runnable runnable2 = new Runnable() {
public void run() {
System.out.println("启动消费者线程");
while (true) {
if (queue.size() > 0) {
try {
int num = queue.take();// 从仓库取出
System.err.println("消费者消费了" + num + "号商品,仓库还有:" + queue.size());
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
Thread t1 = new Thread(runnable1);
Thread t2 = new Thread(runnable2);
t1.start();
t2.start();
Thread.sleep(5000);
}
}
10、 使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
补充--原子操作主要有:
对于引用变量和大多数原始变量(long和double除外)的读写操作;
对于所有使用volatile修饰的变量(包括long和double)的读写操作。
package com.hx.test;
import java.util.concurrent.atomic.AtomicInteger;
public class Test4 {
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
Runnable runnable = new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "count:" + count.getAndAdd(1));
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("最终全局count" + count);
}
}
三、线程间通讯
1、 线程间的通信方式
1.同步
2.while轮询的方式
3.wait/notify机制
4.管道通信
2、 同步
这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。
本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行
package com.hx.test;
public class Test5 {
public static void main(String[] args) {
MyObject obj = new MyObject();
ShenChanThread sThread = new ShenChanThread(obj);
XiaoFeiThread xThread = new XiaoFeiThread(obj);
sThread.start();
xThread.start();
}
}
class MyObject {
static int count = 1;
synchronized public void shenChan() {
count++;
System.out.println("生产:库存" + count);
}
synchronized public void xiaoFei() {
count--;
System.out.println("消费:库存" + count);
}
}
class ShenChanThread extends Thread {
private MyObject mobj;
public ShenChanThread(MyObject mobj) {
this.mobj = mobj;
}
@Override
public void run() {
while (true) {
mobj.shenChan();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class XiaoFeiThread extends Thread {
private MyObject mobj;
public XiaoFeiThread(MyObject mobj) {
this.mobj = mobj;
}
@Override
public void run() {
while (true) {
mobj.xiaoFei();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3、 while轮询的方式
在这种方式下,线程a不断地改变条件,线程b不停地通过while语句检测这个条件(count==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程b执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。关于线程的轮询的影响,
可参考
JAVA多线程之当一个线程在执行死循环时会影响另外一个线程吗?
(http://www.cnblogs.com/hapjin/p/5467984.html)
这种方式还存在另外一个问题:
轮询的条件的可见性问题,关于内存可见性问题,可参考:
JAVA多线程之volatile 与 synchronized 的比较
(http://www.cnblogs.com/hapjin/p/5492880.html)
中的第一点“一,volatile关键字的可见性”
线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的 条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样也会造成死循环。
package com.hx.test;
public class Test6 {
public static int count = 0;// 库存
public static void main(String[] args) {
// 生产的线程
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
count++;
System.out.println(this.getName() + "库存" + count);
}
}
};
//不断测试库存是否达到指定值
Thread t2 = new Thread() {
public void run() {
try {
while (true) {
if (count == 5) {
System.out.println("==5, 线程b准备退出了");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.setName("a");
t2.setName("b");
t1.start();
t2.start();
}
}
4、 wait/notify机制
线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。
A,B之间如何通信的呢?也就是说,线程A如何知道 list.size() 已经为5了呢?
这里用到了Object类的 wait() 和 notify() 方法。
当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。---不像②while轮询那样占用CPU
当条件满足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。
这种方式的一个好处就是CPU的利用率提高了。
但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。
package com.hx.test;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test6 {
public static int count = 0;// 库存
private static Object obj = new Object();
public static void main(String[] args) {
// 生产的线程
Thread t1 = new Thread() {
public void run() {
synchronized (obj) {
for (int i = 0; i < 10; i++) {
if (count == 5) {
obj.notify();
System.out.println("我已经发出了通知");
}
count++;
System.out.println(this.getName() + "库存" + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
// 不断测试库存是否达到指定值
Thread t2 = new Thread() {
public void run() {
synchronized (obj) {
if (count != 5) {
try {
System.out.println(
"wait begin " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS").format(new Date()));
obj.wait();
System.out.println(
"wait begin " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
t1.setName("a");
t2.setName("b");
t2.start();
t1.start();
}
}
5、 管道通信
就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信
具体就不介绍了。分布式系统中说的两种通信机制:共享内存机制和消息通信机制。感觉前面的synchronized关键字和while轮询 “属于” 共享内存机制,由于是轮询的条件使用了volatile关键字修饰时,这就表示它们通过判断这个“共享的条件变量“是否改变了,来实现进程间的交流。
而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。
关于wait/notify更多内容,可参考:JAVA多线程之wait/notify
http://www.cnblogs.com/hapjin/p/5492645.html
四、线程池
1、 什么是线程池2、 为什么使用线程
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力,但频繁的创建线程的开销是很大的,那么如何来减少这部分的开销了,那么就要考虑使用线程池了。线程池就是一个线程的容器,每次只执行额定数量的线程,线程池就是用来管理这些额定数量的线程。
3、 创建线程池的方法
3.1 自定义线程池(使用ThreadPoolExecutor类)
1. ThreadPoolExecutor的构造方法
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
/*int corePoolSize,核心线程数
int maximumPoolSize,最大线程数
long keepAliveTime, 线程数大于核心线程数是,空闲线程等待的最长时间
TimeUnit unit,参数的时间单位
BlockingQueue<Runnable> 执行前用于保存任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
threadFactory--执行程序创建新线程时使用的工厂。
Handler--由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
*/
3.1.1 线程池的阻塞队列
阻塞队列
类结构图
直接提交:工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
无界队列:使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列:当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
总的来说有3种
SynchronousQueue
该队列对应的就是上面所说的直接提交,首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。
LinkedBlockingQueue
该队列对应的就是上面的无界队列。
ArrayBlockingQueue
该队列对应的就是上面的有界队列。ArrayBlockingQueue有以下3中构造方法:
public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> c)
fair表示队列访问线程的竞争策略,当为true的时候,任务插入队列遵从FIFO的规则,如果为false,则可以“插队”。举个例子,假如现在有很多任务在排队,这个时候正好一个线程执行完了任务,同时又新来了一个任务,如果为false的话,这个任务不用在队列中排队,可以直接插队,然后执行
3.1.2 线程池的拒绝执行策略
当线程的数量达到最大值时,这个时候,任务还在不断的来,这个时候,就只好拒绝接受任务了。
ThreadPoolExecutor 允许自定义当添加任务失败后的执行策略。你可以调用线程池的 setRejectedExecutionHandler() 方法,用自定义的RejectedExecutionHandler 对象替换现有的策略,ThreadPoolExecutor提供的默认的处理策略是直接丢弃,同时抛异常信息,ThreadPoolExecutor 提供 4 个现有的策略,分别是:
AbortPolicy策略:直接抛出异常,阻止系统正常工作。
CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
DiscardOledestPolicy策略:丢弃最老的一个请求(即将被执行的任务),并尝试再次提交当前任务。
DiscardPolicy策略:丢弃无法处理的任务,不作任何处理。
3.1.3 代码实例及解释
package com.hx.threadpool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ArrayBlockingQueue blockQueue = new ArrayBlockingQueue<>(10);// 等待队列,存储任务,有边界
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, blockQueue, new AbortPolicy());
// 任务
Runnable r1 = new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "执行任务 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 要提交到线程池的任务
for (int i = 0; i < 12; i++) {
pool.submit(r1);
}
}
}
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3, 60, TimeUnit.SECONDS, blockQueue, new AbortPolicy());
核心线程数。2 最大线程数3 队列大小10 处理策略抛异常
1.当要执行的任务小于2(核心线程数)时,开启和任务数一样的线程去执行任务
2.当要执行的任务大于2(核心线程数)小于等于12(核心线程数+阻塞队列大小),开启2(核心线程数)条线程执行任务,未执行的任务放进队列,当线程执行完时从队列里在取任务执行
3.当要执行的任务大于12(核心线程数+阻塞队列大小) 小于等于13(最大线程数+阻塞队列大小)时 开启 3(最大线程数)条线程去执行任务,执行的任务放进队列,当线程执行完时从队列里在取任务执行
4.当要执行的任务大于13(最大线程数+阻塞队列大小) 时 开启 3(最大线程数)条线程去执行任务,执行的任务放进队列,当线程执行完时从队列里在取任务执行,放不进的任务根据处理策略不同进行处理
五、定时任务调度
1、 为什么使用定时任务调度
在实际项目开发中,除了Web应用、SOA服务外,还有一类不可缺少的,那就是定时任务调度。定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券;比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。本篇博客将系统的介绍定时任务调度,会涵盖Timer、ScheduledExecutorService、开源工具包Quartz,以及Spring和Quartz的结合等内容。
2、 定时需要使用到的类Timer介绍
定时任务调度:基于给定的时间点、给定的时间间隔、给定的执行次数自动执行的任务。
Timer位于java.util包下,其内部包含且仅包含一个后台线程(TimeThread)对多个业务任务(TimeTask)进行定时定频率的调度。
2.1 Timer的schedule和scheduleAtFixedRate方法
public void schedule(TimerTask task, long delay, long period)
参数说明:
task:所要执行的任务,需要extends TimeTask override run()
time/firstTime:首次执行任务的时间
period:周期性执行Task的时间间隔,单位是毫秒
delay:执行task任务前的延时时间,单位是毫秒
很显然,通过上述的描述,我们可以实现:
延迟多久后执行一次任务;指定时间执行一次任务;延迟一段时间,并周期性执行任务;指定时间,并周期性执行任务;
schedule方法测试代码
package com.hx.threadpool;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Test2 {
public static void main(String[] args) {
Timer timer = new Timer();// 定时器
MyTask myTask = new MyTask();// 需要执行的任务
// 当前时间执行第一次,每隔3秒执行一次
// timer.schedule(task, firstTime, period);
// timer.schedule(myTask, new Date(), 3000);
// 一秒后执行第一次,每隔2秒执行一次
// timer.schedule(task, delay, period);
// timer.schedule(myTask, 1000, 2000);
// 延迟3秒后执行一次
// timer.schedule(task, delay);
// timer.schedule(myTask, 3000);
// 指定时间执行一次
// timer.schedule(task, time);
Calendar calender = Calendar.getInstance();
// 当前时间
Date date = new Date();
calender.setTime(date);
// 确定时间格式
SimpleDateFormat dFormat = new SimpleDateFormat("YYYY-MM-DD HH:mm:ss.SS");
// 格式化后的时间
String time = dFormat.format(calender.getTime());
System.out.println("当前时间: " + time);
// minute 一分钟后的时间
int minute = calender.get(Calendar.MINUTE);
calender.set(Calendar.MINUTE, minute + 1);
time = dFormat.format(calender.getTime());
System.out.println(time + "执行任务");
// 执行任务
timer.schedule(myTask, calender.getTime());
}
}
class MyTask extends TimerTask {
@Override
public void run() {
System.out.println("生日快乐");
}
}
思考1:如果time/firstTime指定的时间,在当前时间之前,会发生什么呢?
在时间等于或者超过time/firstTime的时候,会执行task!也就是说,如果time/firstTime指定的时间在当前时间之前,就会立即得到执行。
思考2:schedule和scheduleAtFixedRate有什么区别?
scheduleAtFixedRate:每次执行时间为上一次任务开始起向后推一个period间隔,也就是说下次执行时间相对于上一次任务开始的时间点,因此执行时间不会延后,但是存在任务并发执行的问题。
schedule:每次执行时间为上一次任务结束后推一个period间隔,也就是说下次执行时间相对于上一次任务结束的时间点,因此执行时间会不断延后。
思考3:如果执行task发生异常,是否会影响其他task的定时调度?
如果TimeTask抛出RuntimeException,那么Timer会停止所有任务的运行!
思考4:Timer的一些缺陷?
前面已经提及到Timer背后是一个单线程,因此Timer存在管理并发任务的缺陷:所有任务都是由同一个线程来调度,所有任务都是串行执行,意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。
其次,Timer的一些调度方式还算比较简单,无法适应实际项目中任务定时调度的复杂度。
Timer其他需要关注的方法
cancel():终止Timer计时器,丢弃所有当前已安排的任务(TimeTask也存在cancel()方法,不过终止的是TimeTask)
purge():从计时器的任务队列中移除已取消的任务,并返回个数
3、 JDK对定时任务调度的线程池支持:ScheduledExecutorService
由于Timer存在的问题,JDK5之后便提供了基于线程池的定时任务调度:ScheduledExecutorService。
设计理念:每一个被调度的任务都会被线程池中的一个线程去执行,因此任务可以并发执行,而且相互之间不受影响。
4、 定时任务大哥:Quartz
虽然ScheduledExecutorService对Timer进行了线程池的改进,但是依然无法满足复杂的定时任务调度场景。因此OpenSymphony提供了强大的开源任务调度框架:Quartz。Quartz是纯Java实现,而且作为Spring的默认调度框架,由于Quartz的强大的调度功能、灵活的使用方式、还具有分布式集群能力,可以说Quartz出马,可以搞定一切定时任务调度!
说明:
1、从代码上来看,有XxxBuilder、XxxFactory,说明Quartz用到了Builder、Factory模式,还有非常易懂的链式编程风格。
2、Quartz有3个核心概念:调度器(Scheduler)、任务(Job&JobDetail)、触发器(Trigger)。(一个任务可以被多个触发器触发,一个触发器只能触发一个任务)
3、注意当Scheduler调度Job时,实际上会通过反射newInstance一个新的Job实例(待调度完毕后销毁掉),同时会把JobExecutionContext传递给Job的execute方法,Job实例通过JobExecutionContext访问到Quartz运行时的环境以及Job本身的明细数据。
4、JobDataMap可以装载任何可以序列化的数据,存取很方便。需要注意的是JobDetail和Trigger都可以各自关联上JobDataMap。JobDataMap除了可以通过上述代码获取外,还可以在YourJob实现类中,添加相应setter方法获取。
5、Trigger用来告诉Quartz调度程序什么时候执行,常用的触发器有2种:SimpleTrigger(类似于Timer)、CronTrigger(类似于Linux的Crontab)。
6、实际上,Quartz在进行调度器初始化的时候,会加载quartz.properties文件进行一些属性的设置,比如Quartz后台线程池的属性(threadCount)、作业存储设置等。它会先从工程中找,如果找不到那么就是用quartz.jar中的默认的quartz.properties文件。
7、Quartz存在监听器的概念,比如任务执行前后、任务的添加等,可以方便实现任务的监控。
这里给出一些常用的示例:
0 15 10 ? * 每天10点15分触发
0 15 10 ? 2017 2017年每天10点15分触发
0 14 * ? 每天下午的 2点到2点59分每分触发
0 0/5 14 ? 每天下午的 2点到2点59分(整点开始,每隔5分触发)
0 0/5 14,18 ? 每天下午的 2点到2点59分、18点到18点59分(整点开始,每隔5分触发)
0 0-5 14 ? 每天下午的 2点到2点05分每分触发
0 15 10 ? * 6L 每月最后一周的星期五的10点15分触发
0 15 10 ? * 6#3 每月的第三周的星期五开始触发
我们可以通过一些Cron在线工具非常方便的生成,比如http://www.pppet.net/等。
5、 Spring和Quartz的整合
实际上,Quartz和Spring结合是很方便的,无非就是进行一些配置。大概基于2种方式:
第一,普通的类,普通的方法,直接在配置中指定(MethodInvokingJobDetailFactoryBean)。
第二,需要继承QuartzJobBean,复写指定方法(executeInternal)即可。
然后,就是一些触发器、调度器的配置了,这里不再展开介绍了,只要弄懂了原生的Quartz的使用,那么和Spring的结合使用就会很简单。