第一章 JAVA多线程技能
实现多线程编程的方式主要有两种。
继承Thread类
-
实现Runable接口
工作时的性质相同,主要是Java不能支持多继承。
继承Thread类后,执行
start()
方法的顺序不代表线程启动的顺序。
如何使用实现了MyRunable的类呢?可以看一下Thread.java的构造函数
以下是一个使用实例:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("运行中!");
}
}
public class Run {
public static void main(String[] args){
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("运行结束");
}
}
运行结果:
主要是通过创建Thread对象,将实现了run()
方法的对象传入Thread。
线程安全
非线程安全:主要是指多个线程对同一个对象中的同一个实例变量进行操作时,出现值被更改、值不同步
可通过synchronized关键字给任意对象或者方法加锁以达到线程安全的目的。
Thread.currentThread()和this
区别
Thread.currentThread()
指的是正在执行操作的线程,this
则是指向的线程对象的线程。
run()
和start()
区别
run()
是将run()
方法交给当前线程执行,与主线程是同步执行。
start()
则是另启线程执行方法,与主线程是异步执行。
停止线程
- 使用退出标志使线程正常退出,也就是当
run()
方法完成后线程终止。 - 使用
stop()
方法强行终止线程,但是不推荐使用,是过期作废的方法,且会终止正在运行中的线程。 - 使用
interrupt()
中断线程。
interrupt()
其实是标志了一个中断状态,通过判断这个状态终止线程;
这是三个使用例子:
if(this.interrupted()){
break;
}
if(this.interrupted()){
return;
}
if(this.interrupted()){
throw new InterruptedException();
}
interrupted()
方法具有检验中断状态并清除中断标志的功能。
isInterrupted()
不是Static,且该方法仅检测中断状态不清除中断标志。
在sleep()
方法后,也就是沉睡中被interrupt()
会抛出异常且清除中断标志,与之相反的操作也是一样的结果。
stop()
已经被作废,因为如果强制让线程停止可能使清理性工作不能完成,且会对象进行解锁导致数据不一致。
暂停线程
通过suspend()
暂停线程,resume()
方法恢复线程的执行。
缺点一是独占。如果使用不当,将造成公共的同步对象的独占,使得其他线程无法访问公 共同步对象。当线程获取到锁时,执行了suspend()
就将会造成独占,锁将无法被释放。
有一个特别的坑,printf()
方法内部存在同步锁,这点需要注意。
缺点二是不同步,容易出现因为线程的暂停而导致数据不同步的情况。
yield方法
yield()
方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
线程的优先级
CPU优先执行优先级较高的线程对象中的任务。
设置优先级可使用setPriorith()
,JDK源码如下:
JAVA中线程优先级分为1~10这10个等级,JDK中使用了3个常量来预置定义优先级的值,代码如下:
线程优先级的继承特性
JAVA中线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
优先级具有规则性
高优先级的线程总是大部分先执行完,但不代表高优先级的全部先执行完。当线程优先级差距很大时,谁先执行完和代码的调用顺序无关。
优先级具有随机性
优先级较高的线程不一定每一次都先执行完。
守护线程
JAVA中存在两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程。
第二章 对象及变量的并发访问
synchronized同步方法
方法内的变量为线程安全
方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私的特性造成的。私有变量非共享,不被多线程修改,也就不存在线程安全问题。
实例变量非线程安全
这时候需要添加synchronized关键字。
多个对象多个锁
synchronized锁的是对象的代码和方法,而不是一段代码或者方法。
synchronized方法与锁对象
当两个线程访问同一个对象的两个方法时:
1. A线程先持有Object对象的Lock锁,B线程可以以异步的方式调用Object 对象中的非synchronized类型的方法。
2. A线程先持有Object对象的Lock锁,B线程如果在这是调用Object对象中的synchronized类型的方法则需等待,也就是同步。
脏读
解决同一个对象的脏读问题可在对象的get()
和set()
都加上synchronized关键字。
synchronized锁重入
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
个人理解就是得到锁的线程最优先处理,直到完成该线程的任务。
出现异常,锁自动被释放
这也是为了防止死锁的发生。
synchronized不具有继承性
比如子类调用父类方法,父类方法中的synchronized将会失效。
synchronized同步语句块
顾名思义,可以锁住代码块,使用例子如下:
synchronized(this){
需要锁住的代码
}
synchronized(this)也是锁定当前对象的
this是用来指向对象监视器的。
如果锁定代码块时,对象监视器非同一个对象,如synchronized(方法内的私有对象)
则相当于不是同一个锁,程序将异步执行。以下是一个例子:
public class Service {
private String usernameParam;
private String passwordParam;
// private String anyString = new String(); //如果是对象监视器是这个对象则同步
public void setUsernamePassword(String username,String password){
try {
String anyString = new String(); //方法内的私有对象作为对象监视器,程序将异步调用
synchronized (anyString){
System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入同步块 ");
usernameParam = username;
Thread.sleep(3000);
passwordParam = password;
System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开同步块 ");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
对象监视器
对象监视器:在Java中,每个对象和Class内部都有一个锁,Class广义上也是一个单例对象,每个对象和Class会和一个监视器关联,注意措辞,锁是存在于对象内部的数据结构,监视器是一个独立的结构但是和对象关联,相同点是对象一定有一个锁也一定关联一个监视器。另外,监视器是操控线程的,他会维持一个代码数据区和线程队列等,保证同一时刻只有一个线程访问代码数据区,监视器就是通过判断对象里锁来完成这个安全访问的功能的。监视器是比锁更高层次的抽象。具体的操作流程是:当代码进入同步区域时,找到对象关联的监视器,然后调用监视器获取锁的方法,监视器会读取对象头里面有关锁的信息作为参数,然后进行获取锁的操作,或是让当前线程得到锁,或是让当前线程等待,当代码退出同步区域时,找到对象关联的监视器,然后调用监视器释放锁的操作,整个流程大致是这个样子。另外,需要明白的是,所有代码都隶属于某个对象,非静态方法好说,静态方法是和Class对象关联的,广义上也是隶属于某个对象的。这样就能理解为什么多线程为什么能够实现同步了,因为多个线程执行同一个监视器管理的一份临界资源,自然就能处理同步的细节了。
个人理解:将对象监视器视为分配锁的地方,一次只有一个线程可以进入。进入则获取锁,出门则释放锁。
线程调用同步方法的顺序是随机的
由于线程调用同步方法的顺序是随机的,将可能造成脏读现象。比如一个List,A和B线程同时操作List Service类对其进行add()
如果List为空,添加数据。在synchronized add()
没有设置对象监视器的情况下,将有可能发生脏读。
为了解决这种原因造成的脏读,可以将对象监视器设为实例变量。
比如在上个例子中将synchronized add()
改为
public class ListService{
public add(List list,String data){
try{
synchronized(list){
list.add()
}
}
}
}
不再同步方法而是改为同步代码块且将对象监视器该为list,就可以解决这个脏读问题。
对象监视器的三个结论
x为非this对象。
1. 当多个线程同时执行`synchronized(x)`同步代码块时呈同步效果。
2. 当其他线程执行x对象中的synchronized同步方法时呈同步效果。
3. 当其他线程执行x对象方法里的`synchronized(this)`代码块时也呈现同步效果。
静态同步synchronized方法与synchronized(class)代码块
关键字synchronized还可以应用在static静态方法上,是对当前的*.JAVA文件对应的Class类进行持锁。synchronized关键字加到非static方法上时给对象上锁。
synchronized(class)
的作用与synchronized static
一样都是锁住class类
数据类型String的常量池特性
常量池特性:
String a = "a";
String b = "a";
System.out.println(a == b);
输出结果:
true
当new String对象时,当后面的对象值与前面对象相同时,后面的对象将视为前面的对象,二者都是同一个对象。因此当synchronized(String对象)
时,可能会发生例外,使用了同一个对象监视器。所以在大多数的情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如将synchronized(String对象)
改为synchronized(Object对象)
多线程的死锁
死锁:不同的线程在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。
可以使用JDK自带JCONSOLE工具来检测是否有死锁的现象。
锁对象的改变
锁对象的属性即使改变,以同一个对象为锁的运行结果还是同步的。(String对象比较特别,需要注意)
volatile关键字
voliatile的主要作用是使变量在多个线程间可见
作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
解决异步死循环
先来看一个例子:
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
System.out.println("run");
while (isRunning == true){
}
System.out.println("线程被停止了");
}
}
public class Run {
public static void main(String[] args){
RunThread thread = new RunThread();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.setRunning(false);
System.out.println("already been set false");
}
}
运行结果:
run
already been set false
这是IDEA运行后的结果。但是如果使用同样的代码运行在JVM设置为Server服务器的环境中,运行打印输出相同,但是将会进入死循环。这是因为变量isRunning == true
存在于公共堆栈及线程的私有堆栈中。在JVM设置为-SERVER模式时为了线程运行的效率,线程一直在私有堆栈中取得isRunning的值是true。而代码thread.setRunning(false)
;虽然被执行,更新的却是公共堆栈中的isRunning变量值为false,所以就一直是死循环的状态。
解决这样的问题就要使用volatile关键字了,强制线程访问isRunning这个变量时,从公共堆栈中取值。
修改RunThread代码如下:
public class RunThread extends Thread {
volatile private boolean isRunning = true;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
System.out.println("run");
while (isRunning == true){
}
System.out.println("线程被停止了");
}
}
问题就解决了。
两张图帮助理解:
程序的私有堆栈:
读取公共内存:
volatile的缺点时不支持原子性(整个程序中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节)。个人理解:volatile相当于给变量增加了synchronized。
对比volatile和synchronized
- volatile性能比synchronized好,volatile只能修饰于变量,而synchronized可以修饰方法及代码块。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性也可以间接保证可见效,因为它会将私有内存和公有内存中的数据做同步。
- volatile解决的事变量在多个线程之间的可见性;而synchronized解决的事多个线程之间访问资源的同步性。
synchronized包含两个特征:互斥性和可见性
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
volatile非原子的特性
例子:
public class Mythread extends Thread {
volatile public static int count;
private static void addConut(){
for (int i = 0; i < 1000; i++) {
count++;
}
System.out.println("count= " + count);
}
@Override
public void run() {
addConut();
}
}
public class Run {
public static void main(String[] args){
Mythread[] mythreads = new Mythread[1000];
for (int i = 0; i < 1000; i++) {
mythreads[i] = new Mythread();
}
for (int i = 0; i < 1000; i++) {
mythreads[i].start();
}
}
}
运行结果:
count= 986804
count= 987804
count= 988804
count= 989804
count= 990804
count= 991804
count= 992804
count= 993804
count= 994804
count= 995804
count= 996804
count= 997804
count= 998804
最终结果不是1000000。
更改Mythread类,使用synchronized代替volatile
public class Mythread extends Thread {
public static int count;
synchronized private static void addConut(){
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count= " + count);
}
@Override
public void run() {
addConut();
}
}
运行结果:
count= 989000
count= 990000
count= 991000
count= 992000
count= 993000
count= 994000
count= 995000
count= 996000
count= 997000
count= 998000
count= 999000
count= 1000000
结果正确。
关键字volatile提示线程每次从共享内存中读取变量,而不是私有内存。但如果修改实例变量中的数据,如i++,这样的操作其实并不是一个原子操作,也就是非线程安全的,容易出现脏数据。解决的办法就是使用synchronized关键字。
变量在内存中的工作过程:
- read和load阶段:从主工作内存复制变量到当前线程工作内存
- use和assign阶段:执行代码,改变共享变量值
- store和write阶段:用工作内存数据刷新主内存对应变量的值。
volatile只能保证1阶段是实时的不出问题。2、3阶段不能保证同步,这也是容易造成脏数据的原因。
使用原子类进行i++操作
除了在i++操作时进行synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。
需要注意的是原子类addAndGet方法是原子的,但方法和方法之间的调用却不是原子的。解决这样的问题必须要用同步。
第三章 线程间通信
wait()
作用
wait()
作用是使当前执行代码的线程进行等待,将当前线程置入“预执行队列中”,并且在 wait()
所在的代码行处停止执行,直到接到通知或中断为止。在调用wait()
之前,线程必须获得该对象的对象级别锁。如果在调用wait()
时线程没有持有适当的锁,将抛出IllegalMonitorStateException异常,它是RuntimeException的一个子类,因此不需要TRY-CATCH进行捕捉。
notify()
作用
notify()
也要在同步方法或同步块中调用,调用前线程也必须获得该对象的对象级别锁。如果在调用notify()
时线程没有持有适当的锁,将抛出IllegalMonitorStateException异常。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出notify,并使它获取该对象的对象锁。注意:执行notify()
后,当前线程不会马上释放该对象锁,要等到执行notify()
方法的线程将程序执行完。当第一个获得了该对象锁的wait线程运行完毕也后它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有收到该对象通知,还会继续阻塞在wait状态,直到该对象发出notify或notifyAll。
个人理解:notify使其他线程重新竞争锁,而不是直接获取锁。
notifyAll()
作用
notifyAll()
与notify()
相同,区别是notify()
只唤醒一个线程,notifyAll()
唤醒等待该对象锁的全部线程。
当wait()
遇到interrupt()
当线程呈wait()
状态时,调用interrupt()
会出现InterruptedException异常。
生产者/消费者模式
等待/通知模式最经典的案例就是“生产者/消费者”模式,远离都是基于wait/notify。需要注意的是:wait条件的判断最好使用while而不是if,否则在执行POP时容易抛出异常。唤醒最好使用notifyAll()
而不是notify()
否则在连续唤醒同类线程的情况下将会出现“假死情况”。
通过管道进行线程间通信
可以通过管道流(pipeStream)用于在不同线程间直接传送数据,而无需借助类似临时文件之类的东西。
Java的JDK中提供了4个类:
- PipedInputStream和PipedOutputStream
- PipedReader和PipedWriter
1.用来传递字节流,2.用来传递字符流。
方法join的使用
join方法的作用是使所属的线程对象X正常执行run()
方法中的任务,而使当前线程z进行无限期的阻塞,等待线程X销毁后再继续执行线程z后面的代码,换种说法就是等待线程对象销毁,常用于主线程等待子线程。
join(long)
可以设置等待时间。
join和synchronized的区别是:join在内部使用wait()
方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。
join(long)
和sleep(long)
的区别
方法join(long)
的功能在内部是使用wait(long)
来实现的,所以join(long)
具有释放锁的特点。sleep(long)
不具备释放锁的特点。
join与异常
在join过程中,如果当前线程对象被中断,则当前线程出现异常。
join后面的代码提前运行
类ThreadLocal的使用
主要解决的是每个线程绑定自己的值,可以将ThreadLocal比喻成全局存放数据的盒子,盒子中可以储存每个线程的私有数据。
可以通过继承ThreadLocal类,复写initialValue()
方法为类设置初始值。初始值也可以具有线程变量的隔离性。
类InheritableThreadLocal的使用
使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。
通过复写childValue()
可以继承值并对值进行修改。
需要注意的一点是:如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。
第四章 Lock的使用
ReentrantLock类
使用方法
lock();
doSomething(); //需要同步的代码
unlock();
使用Condition实现等待/通知
Object类中的notify()
方法相当于Condition类中的signal()
方法。
Object类中的notifyAll()
方法相当于Condition类中的signalAll()
方法。
公平锁和非公平锁
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的。
默认情况下,ReentrantLock类使用的是非公平锁。
使用方法:
Lock lock = new ReentrantLock(isFair) //isFair为true则为公平锁
一些Lock类的常用方法
getHoldCount()
、getQueueLength()
、getWaitQueueLength()
的功能
int getHoldCount()
:查询当前线程保持此锁定的个数,也就是调用lock()
方法的次数。
int getQueueLength()
:返回正等待获取此锁定的线程数。
int getWaitQueueLength()
:返回执行了同一个condition.await()
的线程数。
hasQueuedThread()
、hasQueuedThreads()
、hasWaiters()
的功能
boolean hasQueuedThread(Thread thread)
:查询指定线程是否在等待获取此锁定
boolean hasQueuedThreads()
:查询是否有线程在等待获取此锁定
boolean hasWaiters(Condition condition)
:是否有线程正在等待与此锁定有关的condition条件。
isFair()
、isHeldByCurrentThread()
、isLocked()
的功能
boolean isFair()
:判断是不是公平锁
isHeldByCurrentThread()
:当前线程是否保持此锁定
isLocked()
:此锁定是否被线程保持
lockInterruptibly()
、tryLock()
、tryLock(long timeout,TimeUnit unit)
lockInterruptibly()
:如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常
tryLock()
:仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定
tryLock(long timeout,TimeUnit unit)
:如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。
awaitUninterruptibly()
的使用
condition.awaitUninterruptibly()
作用使该线程不可被中断
awaitUnitl()
的使用
condition.awaitUntil(Time time)
相当于wait(Time time)
,可以被提前唤醒。
使用Condition实现顺序执行
使用Condition对象可以对线程执行的业务进行排序规划。
使用ReentrantReadWriteLock类
类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()
方法后面的任务。这样虽然保证了实例变量的线程安全性,但效率低下。所以JDK提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率。
读写锁表示有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
“读写”、“写读”、“写写”都是互斥的;而“读读”是异步的,非互斥的。
简单记忆:写操作与任何操作互斥。
第五章 定时器
书上定时器这章介绍的是Timer类的使用,但Timer类存在许多问题,如果使用JDK的工具类来实现定时任务,阿里巴巴推荐使用ScheduledExecutorService类。
定时器类Timer的使用
JDK中Timer类主要负责计划任务的功能。
Timer类的主要作用是设置计划任务,但封装任务的类是TimerTask类。
执行计划任务的代码要放入TimerTask的子类中,因为TimerTask是一个抽象类。
方法schedule(TimerTask task,Date time)
的使用
schedule()
方法,都是按顺序执行。Task队列中同一个Task只能存在一个,否则将会抛出异常!
该方法的作用是在指定的日期执行一次某一任务。
这是一个使用例子:
public class RunSchedule {
private static Timer timer = new Timer();
static public class MyTask extends TimerTask {
@Override
public void run() {
System.out.println("运行时间为:" + new Date().toLocaleString());
}
}
public static void main(String[] args){
try {
MyTask task = new MyTask();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = "2019-01-21 10:14:10";
System.out.println("字符串时间为:" + dateString + "当前时间为: " + new Date().toLocaleString());
Date dateRef = sdf.parse(dateString);
timer.schedule(task,dateRef);
}catch (ParseException e){
e.printStackTrace();
}
}
}
运行结果:
任务虽然执行完,但进程还未销毁。这是因为创建一个Timer就是启动一个新的线程,这个线程并不是守护线程,它一直在运行。
通过在
Timer timer = new Timer(True) //设置程序运行后迅速结束当前的进程。
方法schedule(TimerTask task,Date FirstTime,long period)
的使用
该方法的作用是在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。
period:填的是间隔时间,以毫秒为单位。
两种情况
计划时间早于当前时间
如果执行任务的时间早于当前时间,则立即执行Task任务。
多个TimerTask任务及延时
TimerTask是以队列的方式一个一个被顺序执行,所以执行的时间有可能和预期的时间不一致,因为前面的任务可能消耗的时间较长,则后面的任务运行的时间也会被延迟。
TimerTask类的cancel()
方法
作用是将自身从任务队列中清除。
Timer类的cancel()
方法
作用是任务队列中全部任务清空。
注意事项:
Timer类中的cancel()
方法有时并不一定会停止执行计划任务,而是正常执行。
下面是一个例子:
public class TimerCancelTest {
static int i = 0;
static public class MyTask extends TimerTask {
@Override
public void run() {
System.out.println("正常执行了:i= " + i + " 运行时间为:" + new Date().toLocaleString());
}
}
public static void main(String[] args){
while (true){
try {
i++;
Timer timer = new Timer();
MyTask task = new MyTask();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = "2019-01-21 10:14:10";
Date dateRef = sdf.parse(dateString);
timer.schedule(task,dateRef);
timer.cancel();
}catch (ParseException e){
e.printStackTrace();
}
}
}
}
运行结果:
并不是每个任务都被清空了,这是因为Timer类中的cancel()
方法并没有争抢到queue锁,所以TimerTask类中的任务继续正常执行。
方法schedule(TimerTask task,long delay,long period)
的使用
作用是以相对时间执行定时任务。
同上,可以是使用schedule(TimerTask task,long delay)方法。
方法scheduleAtFixedRate(TimerTask task,Date firstTime,long period)
的使用
方法schedule()
和scheduleAtFixedRate()
都会按顺序执行,所以不要考虑非线程安全的情况。
方法schedule()
和scheduleAtFixedRate()
主要的区别只在于不延时的情况。
schedule()
:如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“开始”时的时间来计算。
scheduleAtFixedRate()
:如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“结束”时的时间来计算。
schedule方法不具有追赶执行性
错过的Task循环任务,就当无事发生,不执行了,这就是Task任务不追赶的情况。
scheduleAtFixedRate方法具有追赶执行性
错过的Task循环任务将被“补充性”执行也就是直接运行错过任务的次数。
第六章 单例模式与多线程
立即加载/“饿汉模式”
立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法是直接new实例化。而立即加载从中文的语境来看,有“着急”、“急迫”的含义,所以也称为“饿汉模式”。
立即加载/“饿汉模式”是在调用方法前,实例以及被创建了。来看一下实现代码。
public class MyObject {
private static MyObject myObject = new MyObject();
private MyObject(){
}
public static MyObject getInstance(){
//此版本为立即加载
//缺点是不能有其他实例变量
//因为getInstance()方法没有同步
//所以有可能出现非线程安全问题
return myObject;
}
}
创建线程类如下
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
创建运行类Run代码如下
public class Run {
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t3 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
运行结果实现了立即加载型单例设计模式。
延迟加载/“懒汉模式”
延迟加载就是在调用get()
方法时实例才被创建,常见的实现办法就是在get()
方法中进行new实例化。而延迟加载从中文语境来看,是“缓慢”、“不急迫”的含义,所以也被称为“懒汉模式”。
一个简单实现代码如下
public class MyDelayObject {
private static MyDelayObject myObject;
private MyDelayObject(){
}
public static MyDelayObject getInstance(){
if (myObject == null){
myObject = new MyDelayObject();
}
return myObject;
}
}
单线程虽然完成了单例,但如果在多线程的环境中,就会出现取出多个实例的情况。
缺点
多线程情况容易创建多个对象。
public class MyDelayObject {
private static MyDelayObject myObject = new MyDelayObject();
private MyDelayObject(){
}
public static MyDelayObject getInstance(){
if (myObject == null){
//模拟在创建对象之前做一些准备行的工作
Thread.sleep(3000);
myObject = new MyDelayObject();
}
return myObject;
}
}
运行结果
返回了不同的对象。
如何解决呢?
1.声明synchronized
给get()
添加synchronized关键字。
public class MyDelayObject {
private static MyDelayObject myObject;
private MyDelayObject(){
}
synchronized public static MyDelayObject getInstance(){
if (myObject == null){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myObject = new MyDelayObject();
}
return myObject;
}
}
运行结果
[图片上传失败...(image-65776b-1548063380608)]
问题解决了,但此种方法效率非常低下,是同步运行的。
2.尝试同步代码块
public class MyDelayObject {
private static MyDelayObject myObject;
private MyDelayObject(){
}
public static MyDelayObject getInstance(){
synchronized (MyDelayObject.class){
if (myObject == null){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myObject = new MyDelayObject();
}
}
return myObject;
}
}
同步代码块的效果与声明synchronized关键字相同,问题可以解决,但效率低下。
3.使用DCL双检查锁机制
public class MyDelayObject {
private static MyDelayObject myObject;
private MyDelayObject(){
}
public static MyDelayObject getInstance(){
try {
//第一次检查
if (myObject == null){
Thread.sleep(3000);
//同步部分代码块
synchronized (MyDelayObject.class){
//第二次检查
if (myObject == null){
myObject = new MyDelayObject();
}
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
return myObject;
}
}
使用双重检查锁功能,成功的解决了“懒汉模式“遇到的多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。
使用静态内置类实现单例模式
使用了这么一个特性:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
public class MyInnerObject {
private static class MyObjectHandler{
private static MyInnerObject myInnerObject = new MyInnerObject();
}
private MyInnerObject(){}
public static MyInnerObject getInstance(){
return MyObjectHandler.myInnerObject;
}
}
序列化与反序列化的单例模式实现
静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的还是多例。
需要使用一个readResolve()
方法
使用static代码块实现单例模式
静态代码块中的代码在使用类的时候就已经执行了,所以可以应用这个特性来实现单例模式。
public class MyObject {
private static MyObject myObject;
static {
myObject = new MyObject();
}
private MyObject(){
}
public static MyObject getInstance(){
return myObject;
}
}
使用enum枚举数据类型实现单例模式
枚举enum和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用。
第七章 拾遗增补
SimpleDateFormat非线程安全
SimpleDateFormat类主要负责日期的转换与格式化,但在多线程的环境中,使用此类容易造成数据转换及处理的不准确,因为SimpleDateFormat并不是线程安全的。
以下是一个例子
public class MyThread extends Thread {
private SimpleDateFormat sdf;
private String dateString;
public MyThread(SimpleDateFormat sdf, String dateString) {
this.sdf = sdf;
this.dateString = dateString;
}
@Override
public void run() {
try {
Date dateRef = sdf.parse(dateString);
String newDateString = sdf.format(dateRef).toString();
if (!newDateString.equals(dateString)){
System.out.println("报错了 日期字符串: " + dateString + " 转换后的日期为: " + newDateString);
}
}catch (ParseException e){
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateStringArray = new String[]{"2000-01-01","2000-01-02","2000-01-03","2000-01-04","2000-01-05","2000-01-06"};
MyThread[] threads = new MyThread[6];
for (int i = 0; i < 6; i++) {
System.out.println(dateStringArray[i]);
threads[i] = new MyThread(sdf,dateStringArray[i]);
}
for (int i = 0; i <6; i++) {
threads[i].start();
}
}
}
运行结果