任务定时调度
通过Timer和TimerTask,我们可以实现定时启动某个线程。
- java.util.Timer:类似闹钟的功能,本身实现的就是一个线程
- java.util.TimerTask:一个抽象类,该类实现了Runnable接口,所以该类具备了多线程的能力
/**
* 任务调度:借助Timer 和 TimerTask 实现
*/
public class TimerTask {
public static void main(String[] args) {
Timer timer = new Timer();
// 只执行一次
// timer.schedule(new MyTask(), 6000L);
// 多次执行,五秒后执行,每隔两秒打印一次
Calendar c = new GregorianCalendar(2019, 3, 7, 16, 11, 00);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(c.getTime()));
timer.schedule(new MyTask(), c.getTime(), 2000L);
}
}
// 任务类 (多线程)
class MyTask extends java.util.TimerTask {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("放空大脑,休息一下。 :-)");
}
System.out.println("本次结束了。。。。");
}
}
任务调度框架(Quartz)
Quartz介绍
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或 EJBs。
Spring框架已经集成了 Quartz
Quartz分为组成部分:
- Scheduler:调度器,控制所有调度
- Trigger:触发条件,采用DSL模式
- JobDetail:需要处理的Job
- Job:执行逻辑
DSL模式:Domain-specific language领域特定语言,针对一个特定的领域,具有受限表达性的一种计算机程序语言,即领域专用语言,声明式编程。特点就是 简洁、连贯。例如:StringBuilder/StringBuffer的append()方法。
举例(例子用到了Quartz相关jar包,下载地址:http://www.quartz-scheduler.org/downloads
/**
* 任务
*/
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println("--------开始--------");
System.out.println("Hello World! - " + new Date());
System.out.println("--------结束--------");
}
}
---------------------------------------------------------------------------------------
/**
* 任务处理
*/
public class QuartzTest {
public static void main(String[] args) throws Exception {
// 1、创建Schedule工厂
SchedulerFactory sf = new StdSchedulerFactory();
// 2、从工厂获取调度器
Scheduler sched = sf.getScheduler();
// 3、创建JobDetail withIdentity() 方法参数 放入唯一标识 job1、group1
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1").build();
// 获取下一秒时间 下一秒开始执行
Date runTime = DateBuilder.evenSecondDateAfterNow();
// 4、触发器 withIdentity() 方法参数 放入唯一标识 trigger1、group1
// 执行一次
// Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
// 间隔执行 间隔五秒,重复三次
SimpleScheduleBuilder simpleSchedule = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3);
SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1").startAt(runTime).withSchedule(simpleSchedule).build();
//5、 注册任务和触发条件
sched.scheduleJob(job, trigger);
// 启动
sched.start();
// 等待20秒
try {
Thread.sleep(20L * 1000L);
} catch (Exception e) {
e.printStackTrace();
}
// 20秒后停止任务
sched.shutdown(true);
}
}
HappenBefore
代码的执行顺序与预期的不一致。
代码重排建立在代码和代码之间没有任何依赖的情况下的。
数据依赖
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖。
数据依赖分为以下三种类型:
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1; b = a; | 写一个变量之后,再读这个变量 |
写后写 | a = 1; a = 2; | 写一个变量之后,再写这个变量 |
读后写 | a = b; b = 1; | 读一个变量之后,再写这个变量 |
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。所以,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
执行步骤:
1、获取指令
2、从寄存器中存储值
3、操作
4、写回
volatile
volatile保证了线程间变量的可见性,简单地说就是当线程A对变量X进行了修改,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:
- 线程对变量进行修改之后,要立刻回写到主内存
- 线程对变量读取的时候,要从主内存中读,而不是缓存
各线程的工作内存间彼此独立、互补可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为提高执行效率。
volatile是不错的机制,但是volatile不能保证原子性
/**
* volatile 用于保证数据的同步,也就是可见性
*/
public class VolatileTest {
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
// 此处没有在多线程情况下同步 num ,所以即使num不为0时还是会一直运行
// 解决办法,加上 volatile 关键字修饰 num 变量
for (; num == 0; ) {
// 此处不写代码
}
}).start();
Thread.sleep(1000L);
num = 1;
}
}
ThreadLocal
代表每个线程本地存储区域。
- 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程
- ThreadLocal能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。说白了,ThreadLocal就是在多线程环境下保证成员变量的安全,常用方法有:
- get
- set
- initialValue
每个线程自身的存储本地、局部区域
/**
* ThreadLocal:每个线程自身的存储本地、局部区域
*/
public class ThreadLocalTest01 {
// 初始化
// private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
// 更改初始值
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
threadLocal.set(99);
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
new Thread(new MyRun()).start();
new Thread(new MyRun()).start();
}
public static class MyRun implements Runnable {
@Override
public void run() {
threadLocal.set((int) (Math.random() * 99));
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
}
}
}
每个线程自身的数据,更改后不会影响其它线程
/**
* 利用ThreadLocal实现发糖果小案例
* 每个线程自身的数据,更改后不会影响其它线程
*/
public class ThreadLocalTest02 {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new MyRun()).start();
}
}
public static class MyRun implements Runnable {
@Override
public void run() {
Integer left = threadLocal.get();
System.out.println(Thread.currentThread().getName() + "得到了-->" + left);
threadLocal.set(left - 1);
System.out.println(Thread.currentThread().getName() + "还剩下->" + threadLocal.get());
}
}
}
分析ThreadLocal上下文(环境)
/**
* 分析ThreadLocal上下文(环境)
*/
public class ThreadLocalTest03 {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
new Thread(new MyRun()).start();
}
public static class MyRun implements Runnable {
public MyRun() {
// 这里的 Thread.currentThread().getName() 实际上时 main 方法里的
// 在这里修改 threadLLocal的值,只对main线程有影响
// 构造器 哪里调用,就属于哪里
System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "还剩下->" + threadLocal.get());
}
}
}
可重入锁
锁作为并发共享数据保证一致性的工具,大多数内置锁都是可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器将会递减,当计数值等于0时,锁释放。如果没有可重入锁的支持,在第二次企图获取锁时将会进入死锁状态。
举例:
不可重入锁
/**
* 不可重入锁:锁不可以延续使用
*/
public class NotReentrantLock {
private Lock lock = new Lock();
public void a() {
lock.lock();
b();
lock.unLock();
}
// 不可重入锁
public void b() {
lock.lock();
lock.unLock();
}
public static void main(String[] args) {
NotReentrantLock lock = new NotReentrantLock();
lock.a();
lock.b();
}
}
class Lock {
// 是否占用
private boolean isLockd = false;
// 使用锁
public synchronized void lock() {
for (; isLockd; ) {
try {
wait(); // 等待,线程进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLockd = true;
}
// 释放锁
public synchronized void unLock() {
isLockd = false;
notify(); // 唤醒等待线程
}
}
可重入锁(JUC包下JDK提供了ReentrantLock类实现重入锁)
/**
* 可重入锁:锁可以延续使用
*/
public class ReentrantLock {
// private java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
private ReLock lock = new ReLock();
public void a() {
lock.lock();
System.out.println(lock.getHoldCount());
b();
lock.unlock();
System.out.println(lock.getHoldCount());
}
// 不可重入锁
public void b() {
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.a();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(lock.lock.getHoldCount());
}
}
class ReLock {
// 是否占用
private boolean isLockd = false;
// 存储线程
private Thread lockedBy = null;
// 计数器
private int holdCount = 0;
// 使用锁
public synchronized void lock() {
Thread thread = Thread.currentThread();
for (; (this.isLockd && (this.lockedBy != thread)); ) {
try {
wait(); // 等待,线程进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.isLockd = true;
this.lockedBy = thread;
this.holdCount ++;
}
// 释放锁
public synchronized void unlock() {
if (this.lockedBy == Thread.currentThread()) {
this.holdCount--;
// 等于0 标识没有使用了
if (this.holdCount == 0) {
this.isLockd = false;
notify(); // 唤醒等待线程
this.lockedBy = null;
}
}
}
public int getHoldCount() {
return holdCount;
}
}
CAS 原子操作
锁分为两类:
- 悲观锁:synchronized时独占锁即悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
- 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
Compare and Swap 比较并交换:
-
乐观锁的表现
-
有三个值:一个当前内存值V、旧的预期值A、将更新的值B。先获取到内存当中当前的内存值V,再将内存值V和原值A作比较,要是相等就修改为要修改的值B并返回true,否则什么都不做,并返回false。
-
CAS是一组原子操作,不会被外部打断。
-
属于硬件级别的操作(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算法),效率比加锁操作高。
-
ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其它线程修改过了吗?如果在这期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
/**
* CAS:比较并交换
*/
public class CAS {
// 库存
private static AtomicInteger stock = new AtomicInteger(5);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
// 模拟网络延时
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
int result = stock.decrementAndGet();
String name = Thread.currentThread().getName();
if (result < 1) {
System.out.println(name + "--->抢完了。。。");
return;
}
System.out.println(name + "抢了一个商品--->还剩" + result + "商品");
}).start();
}
}
}
Java中JUC包下的atomic包下类具有CAS原子性