Java多线程高级主题

任务定时调度

通过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是一组原子操作,不会被外部打断。
  • 属于硬件级别的操作(利用CPUCAS指令,同时借助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原子性

猜你喜欢

转载自www.cnblogs.com/yliucnblogs/p/10674465.html