简单的任务调度可以使用Timer,TimerTask两个方法,TimerTask是一个线程的子类,用来指定时间干什么事,Timer用来指定任务的时间。
Timer mt = new Timer();
//1秒后执行任务一次
//mt.schedule(new MyTimer(), 1000);
//2秒后,每个1秒执行一次
//mt.schedule(new MyTimer(), 2000,1000);
//指定时间运行
Calendar cc = new GregorianCalendar(2020,1,18,14,29);
mt.schedule(new MyTimer(), cc.getTime(),1000);
class MyTimer extends TimerTask{
public void run() {
for(int i = 0;i < 1;i++) {
System.out.println("helloWorld");
}
}
}
更高级的任务调度可以选用quartz,这是一个框架,需要到官网下载,不过需要科学上网才能访问。
我们写的代码可能没有按照我们所预期的顺序执行,因为编译器和CPU会尝试重排指令使代码更快的执行。这种情况发生在代码之间没有关系的地方,即这几行代码换顺序对结果没有影响的时候
执行一条指令有四个步骤:
执行一条指令会有延时,这时cpu会查看下一条指令与这一条指令所需要的数据有没有关系,如果没有关系,就会提前执行。在多线程中指令重排会出现问题
上面三种情况都会产生数据依赖(语句交换后,结果发生变化),不会发生数据重排。
线程本身是由自己的工作空间的,即缓存。当线程操作内存中的数据,是先把数据复制到工作空间,操作完后再把数据复制回去。如果不用同步,可能会导致数据储存。而用volatile修饰的变量,线程会直接再内存中修改,不再复制到工作空间操作,保证可见性。这样虽然后面的线程会立即看到数据的改动,但是会降低效率(缓存的加入就是为了提高效率),并且不能保证原子性(原子性:复制数据副本,操作数据副本,把数据覆盖回去同时发生)。现在一般不用了。
volatile int a = 0;
ThreadLocal类定义了一大块内存,每个线程在里面都有自己的一块区域,线程只对自己区域内的数据进行修改操作,不会对其他线程的数据造成影响。
ThreadLocal里面有三个常用的方法:get()/set()/initial()。get(),set()方法让线程可以获取,设置自己区域的数据,可以重写initial()方法设置区域内的初始值,用泛型来确定区域内存的数据类型。
API建议定义为private static,
一个类的run()方法和构造方法是两个线程
package ThreadClass;
/**
* 测试ThreadLocal
* ThreadLocal:每个线程自身的存储本地,局部区域
* get/set/initialValue
* @author 王星宇
* @date 2020年2月18日
*/
public class ThreadLocalTest {
//更改初始值只需要重写initialValue方法
//1,匿名类
// private static ThreadLocal tl = new ThreadLocal() {
// protected Integer initialValue() {
// return 200;
// };
// };
//2,λ推导
private static ThreadLocal<Integer> tl = ThreadLocal.withInitial(()->200);
public static void main(String[] args) {
//获取值
System.out.println(Thread.currentThread().getName() + "-->" + tl.get());
//设置值
tl.set(100);
System.out.println(Thread.currentThread().getName() + "-->" + tl.get());
//另一个线程
new Thread(new MyRun()).start();
}
static class MyRun implements Runnable{
public void run() {
tl.set((int)(Math.random() * 99));
System.out.println(Thread.currentThread().getName() + "-->" + tl.get());
}
}
}
用来继承上下文环境的数据,子线程继承(复制)父线程的数据
package ThreadClass;
/**
* InheritableThreadLocal测试
*
* @author 王星宇
* @date 2020年2月18日
*/
public class ThreadLocalTest02 {
private static ThreadLocal<Integer> tl = new InheritableThreadLocal<Integer>();
public static void main(String[] args) {
tl.set(99);
System.out.println(Thread.currentThread().getName() + "-->" + tl.get());
new Thread(()-> {//由main方法开辟的线程继承了main里面的数据
System.out.println(Thread.currentThread().getName() + "-->" + tl.get());
}).start();
}
}
大多数锁时可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立即成功,并且会将这个锁的计数加1,而当线程退出同步代码块时,计数器将会递减,当计数值等于0时,锁释放。
如果没有可重入锁的支持,在第二次获取锁的时候会进入死锁的状态。
可重入锁的简单实现:
package ThreadClass;
/**
* lock的实现
* @author 王星宇
* @date 2020年2月18日
*/
public class LockTest {
Lock lock = new Lock();
public void a() throws InterruptedException {//第一次上锁
lock.lock();
dosomething();
lock.unlock();
}
public void dosomething() throws InterruptedException {//第二次上锁
lock.lock();
//TODO
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
LockTest lt = new LockTest();
lt.a();
}
}
class Lock{
//是否被占用
private boolean isLocked = false;
private Thread useBy = null;//锁被谁用
private int count = 0;//计数锁
//使用锁
public synchronized void lock() throws InterruptedException {
Thread t = Thread.currentThread();
while(isLocked&&useBy != t) {
wait();
}
useBy = t;
isLocked = true;
count++;
System.out.println("锁了第" + count + "次锁");
}
//释放锁
public synchronized void unlock() {
Thread t = Thread.currentThread();
if(useBy == t) {
count--;
if(count == 0) {
isLocked = false;
notify();
useBy = null;
}
System.out.println("解锁第" + count + "个锁");
}
}
}
按照是否可重入:
按照等待进程是否采用队列
按照是否独占锁
比较并交换(compare and swap,CAS):多个线程改变a值,其中一个线程最先把a值由a1改成了a2,后面的线程来把a1改成a3,改之前要先进行比较,后面的线程发现a值变成了a2,说明自己不是第一个,所以不会修改。
优点:1.效率高 2.CAS是一组原子操作,不会被打断。
缺点:如果后面的线程在比较之前a值又被改回了a1,线程会认为a值没有被改变,会继续修改。