JavaEE多线程4

JavaEE12

一、多线程案例四:定时器

  定时器相当于"闹钟“,在代码中,也常常使用”闹钟“机制。在java标准库中,也提供了现成的定时器。

1、定时器的实现 

class MyTimerTask implements Comparable<MyTimerTask>{//实现比较接口
    private Runnable runnable;
    private long time;
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }


    public void run(){
       runnable.run();
    }

   public long getTime(){
        return time;
   }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time-o.time);
    }
}

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<MyTimerTask>();

    public MyTimer(){
        Thread t=new Thread(()->{
            while(true) {
                synchronized (this){
                    //这里涉及队列的修改,是一个线程,因此要加锁
                    if(queue.isEmpty()) {
                        //如果任务队列为空,先不执行任务
                        continue;
                    }
                    MyTimerTask current = queue.peek();
                    if (System.currentTimeMillis() >= current.getTime()) {
                        //执行过的任务要删除
                        current.run();
                        queue.poll();
                    } else {
                        //未到指定时间,先不执行任务
                        continue;
                    }
                }
            }
        });
        t.start();

    }

    //这里涉及队列的修改,是一个线程,因此要加锁
    public void schedule(Runnable runnable,long delay){
        synchronized (this){
            MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
            queue.offer(myTimerTask);
        }
    }
}
public class Demo18 {
    public static void main(String[] args) {
        MyTimer myTimer=new MyTimer();
        myTimer.schedule(()->{
            System.out.println("hello 3000");
        },3000);
        myTimer.schedule(()->{
            System.out.println("hello 2000");
        },2000);
        myTimer.schedule(()->{
            System.out.println("hello 1000");
        },1000);
    }
}

这种情况下:

  如果初始队列为空,程序先给这个线程1加锁,然后发现队列为空,继续进入循环,继续给线程1加锁,导致线程2无法进行加入任务操作,那么这是无意义的。因此要使用wait……notify…… 

2、代码改进

class MyTimerTask implements Comparable<MyTimerTask>{//实现比较接口
    private Runnable runnable;
    private long time;
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }


    public void run(){
       runnable.run();
    }

   public long getTime(){
        return time;
   }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time-o.time);
    }
}

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<MyTimerTask>();

    public MyTimer(){
        Thread t=new Thread(()->{
            while(true) {
                synchronized (this){
                    //这里涉及队列的修改,是一个线程,因此要加锁
                    while(queue.isEmpty()) {
                        //如果任务队列为空,先不执行任务
                        try {
                            this.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    MyTimerTask current = queue.peek();
                    if (System.currentTimeMillis() >= current.getTime()) {
                        //执行过的任务要删除
                        current.run();
                        queue.poll();
                    } else {
                        //未到指定时间,先不执行任务
                        try {
                            //线程阻塞会释放cpu等资源
                            this.wait(current.getTime()-System.currentTimeMillis());
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }

                        //不能使用sleep,原因是,如果此时插入一个更早时间的任务,我们会错过这个任务
                        //而使用wait,来了一个更早的任务,会唤醒该线程,重新计算wait时间
                        //sleep不会释放cpu
                    }
                }
            }
        });
        t.start();

    }

    //这里涉及队列的修改,是一个线程,因此要加锁
    public void schedule(Runnable runnable,long delay){
        synchronized (this){
            MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
            queue.offer(myTimerTask);
            this.notify();
        }
    }
}

二、常见的锁策略

1、乐观锁and悲观锁

1)乐观锁:加锁的时候,假设出现锁冲突的概率不大,因此接下来围绕加锁的工作就会做的很少

2)悲观锁:加锁的时候,假设出现锁冲突的概率很大,因此接下来围绕加锁的工作就会做的很多

2、重量级锁and轻量级锁

1)重量级锁:加锁的开销比较大,要做更多的工作

2)轻量级锁:加锁的开销比较小,要做的工作少

JavaEE13

3、挂起等待锁and自旋锁

1)挂机等待锁:就是 悲观锁 / 重量级锁 的一种典型实现

2)自旋锁:就是 乐观锁 / 轻量级锁 的一种典型实现

举个例子来说明这两种锁的区别:

  设想一个场景:你去追你的女神。你向女神表白(尝试对女神加锁),女神表示,自己已经有对象了(女神表示别人已经对她这个线程加锁了),此时你还是放不下自己对她的喜欢,因此你选择等待,但是等待分为两种:

1)比如,你选择每天还特别关注她的生活,对她嘘寒问暖,这里的情况类称为“自旋锁”

(特点:可以及时了解女神的感情状态,但是会比较消耗自己,在计算机中表现为不释放cpu资源,适用于锁冲突小的情况)

2)比如,你可以选择不关注她的生活,回归自己的生活,做自己的事情,若干年从别人那里听说她分手了,你再去联系她,这种情况称为“挂起等待锁”

(特点:无法及时了解女神的感情状态,但是不会很消耗自己,在计算机中表现为主动释放cpu资源,让cpu去做别的任务,适用于锁冲突大的情况)

4、公平锁and非公平锁

在多个线程竞争锁的时候,究竟该哪个线程获取锁,这就涉及锁的类型是公平锁还是非公平锁。

1)计算机对公平锁的定义是:会按照先来后到的顺序决定哪个线程可以优先获得锁,多个线程不能同时竞争锁

2)计算机对非公平锁的定义是:不会按照先来后到的顺序决定哪个线程可以优先获得锁,而是,多个线程可以同时竞争锁

举个例子:一个女生有多个追求对象A(追了一年)、B(追了一个月)、C(追了三天),但是,此时女生已经有对象了,这三个追求者不想放弃,都在等待。有一天,女生分手了,想要人安慰:

公平情况:A由于追的时间最长,因此他来安慰女生

非公平情况:三个追求者都可以同时去安慰女生,但是最后是谁去安慰取决于他们的努力

  而我们常用的synchronized锁就是典型的非公平锁

5、可重入锁and不可重入锁

  如果一个线程针对一把锁,连续加两次,就可能出现死锁情况,如果把锁设定成“可重入”就可以避免死锁了

 6、读加锁

  所谓的读加锁,就是把“加锁操作“分为两种情况:

1)读加锁

2)写加锁

这个锁将读和写分别进行加锁操作,依赖ReentrantReadWriteLock类中的内部类:

1)ReentrantReadWrite.ReadLock

2)ReentrantReadWrite.WriteLock

这两个类中有各自的lock、unlock方法

一、深入理解synchronized锁

1、锁升级

  使用synchronized加锁的时候,会经历锁升的过程!

  刚开始使用synchronized加锁,首先锁会处于”偏向锁“的状态,遇到线程之间的锁竞争,升级到”轻量级锁“,进一步统计竞争出现的频次,达到一定的程度之后,升级到”重量级锁“

  synchronized加锁的时候,会经历 无锁-->偏向锁-->轻量级锁-->重量级锁的过程

偏向锁:偏向锁只是做个标记,不是真的加锁,当它发现出现锁竞争,才会真的加锁,此时偏向锁就升级为轻量级锁(加锁会产生开销,偏向锁减少了不必要的开销)

  锁升级是不可逆的,只能升级,不能降级!

2、锁消除 

  锁消除是编译器的一种优化策略,编译器会根据synchronize代码做出判定,判定这个地方是否真的需要加锁,如果没有必要,它就会把锁消除掉!

3、锁粗化

  锁粗化也是编译器的一种优化策略,所谓粗不粗,和锁代码的”粒度“有关!代码越多,粒度越粗,代码越少,粒度越细。

  当一次加锁、解锁的代码粒度过细,且类似的细粒度加锁、解锁操作过多,编译器就会粗化其代码,使其合并为一次加锁,解锁操作!

  比如,领导安排你做三个工作,你已经完成并且准备打电话汇报领导!此时,你可以选择两种汇报方式:

1)分三次汇报,每次汇报一个工作已经完成

2)一次汇报三个工作已经完成 

  很明显,第一种方式效率很低,而第二种方式效率很高!

二、CAS操作

1、什么是CAS

 CAS全称Compare and swap,字面意思是“交换和比较”。一个CAS操作包括以下操作:

1)比较A和V是否相同(比较)

2)如果比较相同,将B写入V(交换)此处虽然叫做“交换”,实际达到的效果是“赋值”

3)返回操作是否成功

特点:虽然一个CAS操作包含3个具体操作,但真实的CAS是一个原子的cpu指令完成的。这样的指令是线程安全的!

2、CAS的具体应用场景1:基于CAS实现“原子类”

  实际上,int / long 类型的数据在进行++  或 -- 的时候,都不是原子的……基于CAS实现的原子类,对int / long 等这些类型进行了封装,从而可以原子的完成 ++ --等操作。

  该原子类,在标准库中也有实现。

  

  由于int 类型的++操作不是原子的,因此两个线程对count同时进行读写操作,会出现线程安全问题!那么可以使用原子类!

 

如何通过CAS实现原子类?

class AtomicInerger{
  private int value;//相当于内存数据

  public int getAndIncrement(){
     int oldValue=value;
     while(CAS(value,oldValue,oldValue+1)!=true){
          oldValue=value;
          //如果while循环条件判定为true,说明在CAS的过程中,没有其他线程穿插修改value
          //此时线程是安全的
     }
     return oldValue;
   }
}

 

 JavaEE14

一、JUC(java.util.concurrent)的常见类

1、Callable类:

  Callable和Runable相对,都是描述一个任务,Runnable描述的是一个不带返回值的任务,Callable描述的是一个带有返回值(Integer)的任务。

  Callable通常需要搭配FutureTask来使用,FutureTask用来保存Callable的返回值。如果不理解二者的关系,可以想象你去吃麻辣烫,当你点好菜之后,工作人员会递给你一张小牌子,Callable相当于你的”麻辣烫“,FutureTask相当于”号码牌“。你可以凭借号码牌去取你的麻辣烫。

public class Demo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int a=0;
                for(int i=0;i<5000;i++){
                    a++;
                }
                return a;

            }
        };
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        Thread t=new Thread(futureTask);
        t.start();
        //getf方法在call方法还没执行结束前发生阻塞
        System.out.println(futureTask.get());
    }

}

2、ReentrantLock:可重入锁

ReentantLock与synchronized的区别:

1)synchronized在申请锁失败的时候会死等,ReentrantLock可以通过trylock等待一段时间后放弃

2)synchronized是非公平锁,ReentrantLock默认是非公平锁,但也可以在其构造方法中传入true开启公平锁模式

3)synchronized的wait……notify……只能随机唤醒多个线程中的某一个线程,ReentrantLock可以具体唤醒某一个线程

3、Semaphore:信号量

  Semaphone相当于一个资源计数器。就好像记录产品个数的变量count,当产品被售出时,count--,当产品被产出时,count++。

案例一:

 

案例二:

 

案例三:当成锁使用(获取不到资源会阻塞)

public class Demo1 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore=new Semaphore(1);
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

 4、CountDownLatch

  CountDownLatch一般是搭配线程池使用,主要应用场景是将一个大任务拆分为多个子任务,可以使用CountDownLatch衡量出当前任务是否整体执行结束!

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch=new CountDownLatch(20);//拆分为20个子任务
        ExecutorService executorService= Executors.newFixedThreadPool(4);
        for(int i=0;i<20;i++) {
            int id = i;
            executorService.submit(() -> {
                System.out.println("下载任务 " + id + " 开始执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("下载任务 " + id + " 结束执行");
                countDownLatch.countDown();//完毕、over
            });
        }

            //当countDownLatch收到20个“完成”,所有任务就完成了
            countDownLatch.await();
            System.out.println("任务执行结束");

        }

}

 JavaEE15

一、集合类的线程安全问题

1、list类

  关于ArrayList类和LinkedList类,可以使用“读写拷贝”,意思就是在写的时候,将链表拷贝一份,写操作在新链表中进行!如果在写的同时也有其他线程正在读取链表,那么让它们读取旧链表的数据,等到写操作结束,将旧链表的引用指向新链表即可。

  不过,这种情况只能适用于多个线程读,一个线程写的情况,无法应对多个线程写的情况!

2、哈希表

  Hashtable的加锁,就是直接给put 、 get等方法加上synchronized,任何一个针对哈希表的操作,都会触发锁竞争,因此锁竞争冲突大。为了优化这种情况,推出了ConcurrentHashMap,它是给每个hash表中的“链表”进行加锁。

1)当两个线程修改哈希表上不同的元素,相当于修改不同的变量,这样是不会发生线程堵塞的,只有出现极端情况,两个线程同时修改同一个元素,才会引发锁冲突!

2)ConcurrentHashMap引入了CAS原子操作,针对修改size的操作,因此也不需要加锁

3)ConcurrentHashMap针对读操作,通过volatile以及一些精巧的设计,确保读操作不会读到“修改一半的数据”

4) 针对哈希表的扩容,进行了特殊处理,普通哈希表的扩容,需要创建新的哈希表,将全部的元素搬运过去,这样效率是非常慢的。ConcurrentHashMap进行了“化整为零”,不会在一次操作中,进行所有的数据搬运,而是一次只搬运一部分

  比如,在还不需要扩容的时候,ConcurrentHashMap的哈希表就已经创建好了一个比旧哈希表大的新哈希表,每次put元素的时候,都会将旧表中一部分数据拷贝到新表中,当新旧表内容相同的时候,再次插入元素,就只会插入到新表中

  另外,对于查询、修改、删除,新旧表都要进行同步的操作!

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值