Java--多线程基础知识(四)

一.定时器

Java官方有专门的类使用定时器:Timer

import java.util.Timer;
import java.util.TimerTask;

public class Demo6 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("Hello 3000");

            }
        },3000);
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("Hello 2000");

            }
        },2000);
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("Hello 1000");

            }
        },1000);
    }
}

我们可以看到此时线程并没有结束,这是因为Timer创建的线程是主线程,在没有收到中止命令的时候是不会自动结束的,此时我们可以用timer.cancel() 来结束定时器。

        //确保上述三个任务结束之后再中止
        Thread.sleep(4000);
        timer.cancel();

此时就可以正常结束Timer线程

简易定时器的创建

import java.util.PriorityQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

public class Demo7 {
    static class MyTimerTesk implements Comparable<MyTimerTesk>{
        public Runnable task;
        public long Time;
        public MyTimerTesk(Runnable task,long Time){
            this.task = task;
            this.Time = System.currentTimeMillis() + Time;
        }
        public long getTime(){
            return Time;
        }
        public void run(){
            task.run();
        }
        //重写CompareTo方法
        @Override
        public int compareTo(MyTimerTesk o) {
            return (int)(this.Time - o.Time);
        }
    }
    static class MyTimer{
        Object locker = new Object();
        PriorityQueue<MyTimerTesk> queue = new PriorityQueue<>();
        public MyTimer(){
            //定时器内部就是创建线程执行任务
            Thread t = new Thread(()->{
                try {
                    while (true){
                        //加锁避免线程安全问题
                        synchronized (locker){
                            //读取队列中最早的元素
                            MyTimerTesk task = queue.peek();
                            //运用while循环是因为要再次确认task != null,防止被唤醒之后task == null
                            while (task == null){
                                    //此处等待是因为防止线程忙等,当task == null的时候,不至于一直循环
                                    locker.wait();
                                    task = queue.peek();
                            }
                            //获取当前系统时间
                            Long curtime = System.currentTimeMillis();
                            if (curtime > task.Time){
                                queue.poll();
                                task.run();

                            }else {
                                //此处等待也是为了避免忙等,
                                locker.wait(task.getTime() - curtime);
                            }
                        }
                    }
                }catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
        public void Schedule(Runnable runnable,long time){
            synchronized (locker){
                MyTimerTesk task = new MyTimerTesk(runnable,time);
                queue.add(task);
                locker.notify();
            }
        }
    }

    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.Schedule(()->{
            System.out.println("Hello 3000");
        },3000);
        timer.Schedule(()->{
            System.out.println("Hello 2000");
        },2000);
        timer.Schedule(()->{
            System.out.println("Hello 1000");
        },1000);
    }
}

二.原子类

回顾之前的知识,我们知道当多个线程对同一个变量进行修改或者读取容易引起线程安全问题,之前我们是通过上锁来解决的,但除了上锁,我们还可以通过原子类来解决这一问题。

public class Demo8 {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

以上代码有很明显的线程安全问题,加下来使用原子类来解决问题:

import java.util.concurrent.atomic.AtomicInteger;

public class Demo8 {
    //一个静态的原子整数变量count,并初始化为0
    static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                //count++
                count.getAndIncrement();
                //++count
                //count.incrementAndGet();
                //count += 10;
                //count.addAndGet(10)
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.join();
        t2.join();
        System.out.println(count);
    }
    
}

使用原子类,我们可以避免锁的使用,相比于上锁,原子类使用了CAS操作,避免了线程拥堵,减少了关于锁的开销.

三:相关名词知识

乐观锁:

假设最好的情况,访问共享资源时不会发生冲突。特点:不会阻塞其他线程,只有在更新时才会检查数据是否被其他线程修改过

悲观锁:

假设最坏的情况,访问共享资源的时候一定会发生冲突。特点:在整个数据处理过程中将数据锁定,其他线程无法访问。

Synchronized:开始使用乐观锁,当竞争程度高的时候就会切换成悲观锁。

重量级锁:

少量的内核态用户态切换,成本低。

轻量级锁:

容易引发线程调度和内核态用户态切换,操作成本高。

Synchronized 开始是一个轻量级锁,如果锁冲突比较严重,就会切换为重量级锁。

自旋锁:

线程在等待锁释放的时候不会陷入阻塞状态,而是不断地检查锁的状态,一旦锁可用了,线程会立即获取锁并且执行

公平锁:

获取锁要遵循先来后到。

非公平锁:

获取锁不遵循先来后到,每个线程都有可能获取到锁

Synchrnoized 是非公平锁

可重入锁:

允许同一个线程重复获取同一个锁,不会发生阻塞

不可重入锁:

不允许同一个线程重复获取同一个锁,会发生阻塞

Synchronized是可重入锁

读写锁:

将读操作和写操作区别对待,因为当对一个数据进行读取操作的时候,是不会发生线程安全问题的,但对数据进行写操作的时候,此时再对数据进行无论是写操作还是读操作,都是会引发线程安全问题的。读写锁就是做到了:读加锁和读加锁之间不互斥。写加锁和读加锁互斥,写加锁和写加锁互斥。

Synchronized 不是读写锁

偏向锁:

第一个加锁的线程,在执行的时候会进入偏向锁状态,记录这个锁属于哪个线程,这种状态执行任务不会第一时间加锁,只有当其他线程尝试来获取锁的时候才会加锁。

什么是偏向锁?

偏向锁不是真的加锁,⽽只是在锁的对象头中记录⼀个标记(记录该锁所属的线程).如果没有其他线程参与竞争锁,那么就不会真正执⾏加锁操作,从⽽降低程序开销.⼀旦真的涉及到其他的线程竞争,再取 消偏向锁状态,进⼊轻量级锁状态.

信号量:Semaphore 

信号量表示的是可用资源的个数,相当于计数器,通过 acquire()来增加,release() 来减少。

CountDownLatch

同时等待n个线程结束,在创建实例的时候,传入10,就代表等待10个任务结束。每完成一个任务就调用一次latch.countDown() 此时CountDownLatch 内部的计数器就会自动减少,当计数器为0,就代表10个任务全部结束了。

import java.util.concurrent.CountDownLatch;

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            int r = i;
            Thread t = new Thread(()->{
                System.out.println("第" + r +"个任务");
               count.countDown();
            });
            t.start();
        }
        count.await();
        System.out.println("任务结束");
    }
}

四:CAS(compare and swap)

一种无锁的原子操作技术,通过硬件指令保证线程安全

操作方式:将 A 与 V 比较,如果 A 与 V 相等,那么将 B 的值 写入 V,然后返回操作是否成功。

CAS 的弊端就是将 A 与 V 比较,如果 A == V,他就认为 V没有变过,这就导致了CAS的ABA问题

ABA问题:

当有两个线程 t1 和 t2同时对一个变量 test == 100进行修改的时候,t1将 test 的值更改为了 50,然后又将 test 的值改为了100,此时 t2 线程读取到的值还是100,就会认为 test的值没有发生改变,又将test的值改为了50.

实例:

小明存款100,想取出50元,创建了两个线程来执行任务,此时t1线程将 100元改为了50元,然后小明的朋友给小明转账50,此时我们是希望 t2 线程执行失败的,毕竟已经进行过扣款了,但由于存款还是100,t2线程认为存款没有发生过改变,会将存款改为50,这个时候扣款2次,这就是ABA问题导致的。

五.Synchronized原理

根据上述相关知识,

1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.

2. 开始是轻量级锁实现,如果锁被持有的时间较⻓,就转换成重量级锁.

3. 实现轻量级锁的时候⼤概率⽤到的⾃旋锁策略

4. 是⼀种不公平锁

5. 是⼀种可重⼊锁

6. 不是读写锁

JVM将Synchronized分为了四种状态:无锁,偏向锁,自旋锁和重量级锁,会根据不同的情况改变。

初始状态 Sychronized为无锁,执行任务的时候转化为偏向锁,当遇到竞争的时候转化为自旋锁,当竞争十分激烈的时候,就会变为重量级锁。

六:优化操作

锁消除:

编译器和JVM会判断锁是否会可以消除,如果可以的话,会把锁消除掉。

当执行某些Synchronized的时候,其实并没有处在多线程下,没有必要进行上锁,此时就会执行锁消除优化。

锁粗化:

⼀段逻辑中如果出现多次加锁解锁,编译器+JVM会⾃动进⾏锁的粗化.

当频繁的进行加锁解锁操作是十分耗费资源的,此时就会进行锁的粗化,减少解锁操作,增加锁的粒度。

七.Callable 和 FutureTask

平时我们求 1+2+3+....+100都是创建线程来执行,代码比较麻烦,这时我们就可以使用Callable类,Callable 与 runnable 类似,但是 Callable 可以拥有返回值并且指定返回值的类型,通常搭配FutureTask来使用:

首先我们创建一个Callabe实例并且指定返回值类型 Integer ,重写call() 方法并且写入要执行代码,接下来创建FutureTask的实例并且传入 callable 参数,这里是让 future 来获取到 callable 的返回值,接下来创建 Thread 实例来执行 callable 任务,就可以得到结果。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable =new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i < 10; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        Thread t = new Thread(future);
        t.start();
        int result = future.get();
        System.out.println(result);
    }
}

ReentrantLock

ReentrantLock 也是一种可重入锁,通过 lock() 来加锁,unlock() 来解锁,相比于Synchronized来说,ReentrantLock 的解锁方式需要手动,但更加灵活

Synchronized 是一个关键字,JVM内部实现的

ReentrantLock是Java标准库里面的一个类,是JVM外实现的

ReentrantLock 可以搭配Condition类来使用,精确的唤醒某个线程

ReentrantLock是公平锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值