【一篇吃透面试八股】——Java并发编程:JUC篇

目录

JUC——java高级并发

思考题:交替执行的思路

  • ①wait与notify,一个执行完了就notify,同时自己wait【Plus版就是多个Condition精准通知】
  • ②纯标志控制【不推荐,难以检查】
  • ③synchronousQueue:同步队列,生产了,必须消费完才能再生产另一个【虽然确实是ABAB,但是打印这个动作可能延后,如果严格要求还是使用方案①】

零、解决线程同步的三大方法

  • 互斥量法——锁机制【即Lock、Syn等】
  • 事件控制法——阻塞与唤醒【wait、notify】
  • 信号量法——生产者消费者模型【此方法归根到底还是前两种方法,或者看成前两种方法的一种应用】

一、JUC——java-util-concurrent包

1
是一个并发处理的工具包

Runnable:没有返回值,效率比Callable低

二、线程和进程

2.1 程序、进程、线程

程序: 用户编写的代码

进程: 程序的一次执行过程【一个可运行jar包的一次执行过程就是一个进程】

线程: 一个进程往往包含多个线程

java默认几个线程——2个

  • main线程

  • gc线程

线程举例:Typora软件,一个线程负责程序输入,一个线程负责自动保存,一个线程负责统计字数等。

2.2 线程管理

要明白,线程的创建不是java所能实现的,其本质是通过C++的本地方法来开启。

对于java而言:三种开启线程的方式Thread、Runnable、Callable、还可以通过Lambda表达式

2.3 并发VS并行

并发:在一核CPU中模拟多个线程,通过快速交替造成同时执行的假象。(宏观多个,微观1个)

并行:多个线程同时执行(始终多个)

8核心16线程:实际上只有8个核心,但是通过超线程技术,让cpu最多可以运行16个线程

并发编程的本质:充分利用CPU的资源

public static void main(String[] args) {
    // 得出cpu逻辑核心数,即最大能运行的线程数
    System.out.println(Runtime.getRuntime().availableProcessors());
}
2.4 线程状态(java实现的)

获取状态

Thread thread = new Thread();
thread.getState();
// 结果一定是以下枚举之一

状态枚举

public enum State {
    // 新生
    NEW,
    // 运行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待
    WAITING,
    // 超时等待
    TIMED_WAITING,
    // 终止
    TERMINATED;
}
2.5 wait/sleep区别

①来自不同的类

  • wait——Object类

  • sleep——Thread类【很少用】

我们很少直接使用sleep方法,而是使用TimeUnit方法

TimeUnit.SECONDS.sleep(100);

②锁的释放

  • wait——释放锁
  • sleep——抱着锁睡觉【不会释放锁】
  • TimeUnit睡觉不会释放锁

③使用的范围不同

  • wait:在同步代码块中使用【你得先使用】,否则报IlleaglMonitorStateException

  • sleep:任何都可使用

三、Lock锁

3.0 原理

Lock锁其实锁的是这个锁本身,就像syn锁对象一样,只不过它锁的是自己。

当多个线程尝试去拿到锁时,发现已经被拿了,就会阻塞!

而且Lock是可重入的,可以多次lock

3.1 锁的体系

在这里插入图片描述

3.2 ReentrantLock构造方法

默认是非公平锁。如果传true——公平锁,传false——非公平锁

在这里插入图片描述

公平锁:公平——必须先来后到【容易造成效率问题,如3s钟进程等3h进程,低效】

非公平锁:不公平——可以插队(默认)

3.3 Lock的使用
public class Test{
    // 1、创建锁
	private Lock lock = new ReentrantLock();    
	public void test(){
        // 2、加锁 
        lock.lock();
        try{
            // 业务代码    
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            // 3、解锁
            lock.unlock();
        }
    }
}
// 
public static void main(){
        Test ticket2 = new Test();
        new Thread(()->{for (int i = 0; i < 50; i++) ticket2.sell();},"A").start();
        new Thread(()->{for (int i = 0; i < 50; i++) ticket2.sell();},"B").start();
        new Thread(()->{for (int i = 0; i < 50; i++) ticket2.sell();},"C").start();
}
3.4 Syn和Lock的区别

1、syn是内置的java关键字、Lock是一个类!

2、syn无法是隐式锁,不利于判断,Lock是显式锁,可以直观判断是否获取到锁

3、syn是全自动的,执行完后会自动释放锁。Lock必须手动释放锁【如果不释放锁,可能会造成死锁问题】

4、tryLock方法

  • 使用syn,如果一个线程获得了锁,另一个线程就一直等待。如果拿着锁的线程阻塞了,另一个线程就持续等待。

  • lock锁有一个trylock方法,可以避免等待情况【使用场景:先尝试,尝试成功就执行同步方法,否则先去执行其他的】

5、公平性

  • syn一定是非公平的;
  • lock可以自己设置公平与非公平

6、syn 适合锁少量的代码同步问题,lock适合大量的同步代码

总结:Lock非常灵活!Syn就像手动挡,而Lock就像自动挡

四、生产者和消费者问题

4.0 问题描述

生产者和消费者在同一时间段内共用同一个存储空间,如下图所示,生产者向空间里存放数据,而消费者取用数据。我们需要通过代码保证该模型的正确执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDY7d52S-1649568717503)(java并发编程——JUC/2011091018554595.gif)]

注意以下几点:

  • 消费者消费完了应该等待生产者生产,产品数量不能为负数
  • 生产者生产的产品数量应当小于空间容量,当空间容量满时停止生产,将空间使用权交给消费者
4.1 Syn实现
package juc.pc;

/**
 * 生产者-消费者问题
 * 问题描述:
 * 两个线程交替执行操作num,即缓冲区容量为1的情况
 * A num+1
 * B num-1
 * 如果没有线程通信机制,是无法实现的,可以通过《等待-唤醒》来操作
 */

public class OldVersion {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

// 等待-业务-通知
// 判断是否等待,不需要等待就干活,干完就通知
class Data {
    private int number = 0;

    // +1
    public synchronized void increment() throws InterruptedException {
        // 这个判断是起一个保证作用,实际上顺序如果比较巧合的话,不会有任何线程被挂起!
        while (number != 0) {
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知
        this.notifyAll();
    }

    // -1
    public synchronized void decrement() throws InterruptedException {
        while (number == 0) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知
        this.notifyAll();
    }
}


// 曾经疑惑
// 用if:
// 如果生产者还没生产,CPU两次调度消费者怎么办,第二次就接着判断向下了
// 不用担心,因为调度一次后,发现需要等待,就会把这个线程挂起了,除非有别人notify,所以该线程是无法被调度的
// 但是,这个问题并没有彻底解决!
// 如果只有两个线程,确实不会有问题。但如果是多个线程,例如一个生产者,两个消费者,那么生产者A生产完之后notify,会醒来一个消费者B,消费者消费完后又notify,就会把另一个消费者C也唤醒了,此时货物为0,但是C已经经过判断,所以会继续向下执行,就会造成货物数量为-1

// 所以,一定要用while
// 用while之后,循环判断,被唤醒后会再次检查判断条件,从而避免了虚假唤醒的问题

/**
 * 虚假唤醒问题:
 * 如果用if作为判断wait的条件,那么假如此线程挂起后再次被唤醒,将会从wait之后的代码继续进行!
 * 但是用while作为判断条件,该线程再次被唤醒时又回到了判断条件,符合才能继续执行。
 *
 * 所谓虚假唤醒:就是本不该唤醒你,结果给你唤醒了。
 * --------
 * 唤醒信号丢失问题:
 * 那么,为什么要用notifyAll这种东西呢。因为当有多个线程时,可能会出现信号丢失问题,比如这个例子,有三个线程,两个线程+1,第三个线程-1,那么A线程+1后挂起,又调用B线程,B线程发现已经加过1了,也挂起,然后,C线程-1,唤醒A【先进先出】,C也被挂起。
 *
 * A被唤醒后,发现条件满足,+1后唤醒的是B【】,接着被挂起到C后面
 * B被唤醒后,发现已经+1过了,便挂起。
 * 此时三个线程都被永远挂起,再也无法醒来!!
 *
 * 所以要用notifyAll + while循环,notifyAll唤醒所有的线程,所有线程竞争,不符合运行条件的【即被虚假唤醒的】再次被挂起!从而解决了虚假唤醒的问题。也解决了notify丢失的问题!
 *
 * 总结:如果真的是随机唤醒的话,notify有概率解决问题,因为只唤醒一个,如果恰好唤醒的是对的,一点问题都没有,如果唤醒错的,则造成notify丢失。
 * 引入notifyall会导致所有线程唤醒,此时while就可以让他们回去睡觉,保证程序正常执行!
 */

总结:

1、代码设计思路:

一个Data类,里面放着生产方法和消费方法,通过创建多个线程,每个线程分别只调用生产或消费方法,来模拟生产者和消费者!

这样由于一个实例只有一把锁,所以很安全!

2、当设计多个生产者和多个消费者时,我们通常用while循环检查+notifyAll全部唤醒来解决!

  • 为什么while——防止虚假唤醒
  • 为什么notifyAll——防止唤醒信号丢失

不足:浪费cpu资源,如果线程很多,notifyall会造成大量切换上下文

4.2 Lock版的实现
①Condition
  • 旧的三板斧:syn + wait + notify

  • 新的三板斧:lock + await + signal

condition替换了原来老的对象监视器

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await();
condition.signalAll();

深入理解:

这里的condition是由lock创建的,这一点已经很清楚的告诉我们了为什么wait、notify要配合syn使用。

syn关键字出现的时候,锁的一定是某个对象【即使锁的是Class,其本质也是锁的Class对象】

wait需要知道将当前线程放在哪个对象的等待队列中。所以需要配合监视器【Monitor】。就像这里的condition.await(),就能得知,要将当前线程放在lock对象的等待队列中。

syn锁某个对象时,相当于声明了接下来要监视的就是这个对象。此时再配合wait方法,就知道要将当前线程放在syn的对象中了!

  • 任何对象都有一个monitor,当它被持有后,将处于锁定状态。

  • syn本质是在代码段首和段位分别添加了monitorenter和monitorexit指令

根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁【可重入】,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。当另一个线程来检查该计数器,发现大于0时,就获取失败

②实现
class Data1 {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    // +1
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 通知
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    // -1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 通知
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
  • lock与wait

如上,wait后,线程会释放锁,而唤醒只是让这些线程可以重新竞争锁,竞争成功的获取到锁,这个过程是隐式的,其他的仍然没有锁,处于等待状态。

当然,这里面的详细过程是,消费者消费了一个,唤醒全部,有可能其他消费者拿到锁了,上CPU,结果循环检查不通过,又wait,同时释放了锁

4.3 condition精准唤醒

问题:传统的notify只能做到随机唤醒,通过condition,我们可以做到精准唤醒

jvm规范种对于notify的规范是随机唤醒,但是实际上取决于每个jvm的实现。而广为使用的hotspot对于notify的实现是先进先出的唤醒!!

这里还是有一些区别的,condition是由lock创建的,但是实质上阻塞的时候是挂在condition上了,即把condition当作资源。但是一定要配合锁使用,即只有持有锁的时候,才有资格去await

class Method {
    private Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    State state = State.A;

    public void funcA() {
        lock.lock();
        try {
            while (state != State.A) {
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAAA");
            condition2.signal();
            state = State.B;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

五、syn详解

5.0 syn能锁什么

首先要明白,syn只能锁对象

  • 类的实例
  • 类对象

在此基础上,根据具体情况判断

  • 锁static方法,则锁的是类对象;否则锁的是实例
  • syn同步块,传入类对象,则锁类对象;否则锁实例
5.1 syn方法

syn方法锁的是方法的调用者,所以如果是static的,就会把类给锁了

public synchronized void sendMs1(){
    //锁实例
}

public static synchronized void sendMs2(){
    //锁类对象
}

public void sendMs3(){
    //普通方法不受锁影响 
}
5.2 syn块
public void send(){
    synchronized(Test.class){
	// 锁类
    }
    
    synchronized(this){
	// syn(this)就是锁当前实例
    }
}
5.3 说明
  • 每个实例对象都有一把锁,如果创建了两个对象,则锁一个不影响另一个执行
  • 每个类对象只有一把锁,如果类的锁被占用,则必须等待
  • 普通方法不受锁的影响

锁类对象的方法有三种

  • 直接syn静态方法

  • syn (类.class)

  • syn (this.getClass())

六、集合类不安全!

并发条件下,现有的集合类是不安全的。同时写入会报错:ConcurrentModificationException

根本原因是:modCount和expectedModCount不一致

6.1 ArrayList
public static void main(String[] args) throws InterruptedException {
    List<String> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        new Thread(()->{
            list.add("a");
        }).start();
    }
    Thread.yield();
    System.out.println(list.size());
}

解决手段

①Vector

Vector就是线程安全的ArrayList,它的每个方法都加了syn,但这样会严重影响效率【因为读的时候Vector也加锁】

②Collections工具类
List<String> list = Collections.synchronizedList(new ArrayList<>());
③CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

传统的读写:

读:从主内存中拷贝一份到工作内存

写:从主内存中拷贝一份到工作内存,完事再写回去

CopyOnWrite:写入时复制思想

读:直接用主内存中的数据【即不拷贝!】

写:当线程要增删改数据时,才从主内存拷贝出来一个副本,然后在副本中中修改,再把修改好的写回主存【拷贝是为了防止脏读】写入时复制不能避免不可重复读的问题

写的过程是加锁的,原理并不是多个线程各自弄再合并,就是普通的加锁写,一次只能写一个

优点:读的时候不需要锁,效率提高

缺点:可能导致读到旧数据;如果对象大,频繁进行复制会消耗内存

通过狂神的结果确实发现了问题,COW会导致数数据读错【这就很明显了,因为写完还没刷新,导致读的是旧数据】【最终数据是一致的!可以看到这里一共10条记录】

6.2 HashSet
1)collections
Collections.synchronizedSet(new HashSet<>());
2)copyonwrite
CopyOnWriteArraySet<>();

hashset的底层是什么?

底层是hashmap!

set的本质是map的key,无法重复的,无序的

看看HashSet的add方法:

public boolean add(E e) {
 return map.put(e, PRESENT)==null;
 //PRESENT是不变的,一个常量对象而已
}
6.3 HashMap
①Collections
Map map = Collections.synchronizedMap(new HashMap<>());
②ConCurrentHashMap
Map map = new ConcurrentHashMap();

Concurrent还有几个有用的:

如ConcurrentLinkedDeque、ConcurrentLinkedQueue、ConcurrentSkipListMap等

concurrentHashMap是分段的数组+链表,线程安全,效率更高

七、线程创建的方式

7.1 Callable
①概念

Callable接口类似于Runnable,可以有返回值和抛出异常,但用到的方法和runnable不同

请看callable的定义:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

在实现接口时,需要同时传入一个泛型,这个泛型就是Callable的返回值类型。

我们可以根据以上信息自定义一个类,如下:

class MyThread implements Callable<String>{
    @Override
    public String call(){
        sout("xx");
        return "xx";
    }
}
②使用

线程的启动方式只有一种:

new Thread(Runnable实现类).start();// lambda表达式本质上也是Runnable实现类

所以我们要想办法把Callable接口和Runnable接口扯上关系。这里我们看一下FutureTask类

FutureTask这个类的第一条构造方法,可以接收一个Callable作为参数。

而且这个类本身实现了Runnable接口,所以这不正是我们想要的类吗,我们把实现了Callable接口的类传入FutureTask,再把这个FutureTask丢到Thread里,就可以执行了!

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask<Integer>(myThread);
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start(); // 结果会被缓存,提高效率,确保相同的任务只执行一次
        // 通过get来得到返回值
        // get方法可能会产生阻塞,因为要等待结果返回,如果call方法需要运行很久,这里就会阻塞
        // 要么放到最后,要么用异步通信的方式
        Integer integer = (Integer) futureTask.get();
        System.out.println(integer);
    }

}

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return Integer.valueOf("123456");
    }
}

注:

  • 通过FutureTask的get方法获得返回值
  • 如果有异常也会通过get方法抛出
  • FutureTask能确保相同的任务被唯一的执行
7.2 实现Runnable接口

重写run方法,并丢入Thread.start

new Thread().start();
7.3 继承Thread类【不推荐】

重写run方法,实例化并调用start方法

7.4 lambda表达式
new Thread(()->{});

八、常用辅助类

1、CountDownLatch

两个重要方法:

  • countDown():让计数器数量-1

  • await():让当前线程等待,直到计数器归0,再向下进行

public static void main(String[] args) throws InterruptedException {
    // 总数是6
    CountDownLatch countDownLatch = new CountDownLatch(6);
    for (int i = 0; i < 6; i++) {
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"线程已完成任务");
            countDownLatch.countDown();
        },String.valueOf(i)).start();
    }
    countDownLatch.await();
    System.out.println("所有线程都完成了任务,主线程继续执行!");
}
2、CyclicBarrier

这个东西叫做循环栅栏

主要方法是await方法:作用是让当前线程等待,直到所有线程都处于等待态后,让最后一个线程执行构造方法中的runnable方法体。

并且这个锁可以循环使用!一次齐了之后就可以开启下一次。

场景就是同学聚会,每个人来的时间不一样,但是得等到所有人都来齐之后再吃饭!

public class CyclicBarrierDemo {

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
            System.out.println(Thread.currentThread().getName()+"执行收尾任务");
        });

        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println("第"+temp+"条线程到达,"+"线程名为"+Thread.currentThread().getName());
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }

}

坑:lambda表达式中的变量为final型的。所以这里创建一个final变量

3、Semaphore

两个方法:

这里信号量的数目是创建时就确定的。

acquire:占有信号量,如果已经没有信号量了,就挂起!

release:释放信号量,唤醒等待的线程

类似于生产者消费者

public class SemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        // 默认线程数量:停车位
        Semaphore semaphore = new Semaphore(3);
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"释放车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();
        System.out.println("所有线程都执行完毕!");

    }
}

九、读写锁

读写锁,类似昨天的COW,只读的时候可以并发。而写的时候必须是独占的

区别在于:读写锁将读与写互斥了【读的时候不让写,写的时候不让读】,COW没有控制读与写

9.1 定义

ReadLock接口,唯一实现类是ReentrantReadWriteLock,这类里面有两个内部类ReadLock和WriteLock【实际上是两把锁】,这两个类实现了Lock接口;

相比于ReentrantLock,粒度更小,可以更细微的控制

9.2 代码
class Locky{
    ArrayList arrayList = new ArrayList();
    ReadWriteLock lock = new ReentrantReadWriteLock();

    public void read(){
        lock.readLock().lock();
        System.out.println(arrayList.get(arrayList.size()));
        lock.readLock().unlock();
    }

    public void write(){
        lock.writeLock().lock();
        arrayList.add("a");
        lock.writeLock().unlock();
    }
}
9.3 共享锁、独占锁
  • 独占锁 = 写锁

  • 共享锁 = 读锁

如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。

如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

即:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)

十、阻塞队列

10.1 BlockingQueue接口

阻塞队列不是放阻塞线程的队列,而是具有阻塞功能的队列

  • 当线程向一个已满的阻塞队列写入时,就会阻塞
  • 当线程从一个已空的阻塞队列取出时,也会阻塞

关系图:

Abstract Queue优先级队列

Deque双端队列接口【双端队列:队列的每一端都能够插入数据项和移除数据项】

10.2 两组API

1、是否抛出异常

  • 抛出异常:add、remove、element系
  • 不抛出异常【用特殊返回值如null代替异常】offer、poll、peek系

2、是否持续阻塞

  • 持续等待,持续【本质是condition的await】
  • 超时等待,超过设定时间就停止【本质是condition的await】:offer、poll传入等待时间
方式抛出异常特殊值持续等待超时等待
添加addoffer(false)putoffer重载
移除removepoll(null)takepoll重载
检查elementpeek--
10.3 SynchronousQueue:同步队列

放一个,拿走后才能放下一个,否则不可。即队列容量为1

class Printer2 {
    SynchronousQueue synchronousQueue = new SynchronousQueue();
    public void printA() throws InterruptedException {
        int num = 1;
        for (int i = 0; i < 50; i++) {
            synchronousQueue.put("");
            System.out.println(num);
            num+=2;
        }
    }
    public void printB() throws InterruptedException {
        int num = 2;
        for (int i = 0; i < 50; i++) {
            synchronousQueue.take();
            System.out.println(num);
            num+=2;
        }
    }
}

十一、线程池(!)

11.0 Executor接口

11.1 池化技术:

程序的运行会占用系统的资源!【因为需要创建,销毁连接:如建立TCP,创建线程等】我们要做的就是优化资源的使用!=》池化技术

如:线程池、JDBC连接池、内存池

池化技术:事先准备好一些资源,有人要用,就来拿,用完再还给我。

线程池的好处:

1、降低资源的消耗

2、提高响应的速度

3、方便管理

线程复用、可以控制最大并发数、能够管理线程

11.2 线程池详解

直接用Executors创建线程的弊端:

FixedThreadPool和SingleThreadPool的请求队列长度为Interger.MAX_VALUE,可能会堆积大量的请求,导致OOM【Integer.MAX_VALUE=21亿】

CachedThreadPool和ScheduledThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM

ExecutorService executor2 = Executors.newCachedThreadPool();// 可改变,尽可能创建更多线程
try {
    for (int i = 0; i < 100; i++) {
        final int temp = i;
        executor2.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "------->" +temp);
        });
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 线程池要记得关闭
    executor.shutdown();
}

使用了线程池后,我们本来是new Thread.start 改为了 executor.execute,此时具体分配的线程数就由线程池决定了。用完记得关闭

11.3 线程池参数

去查阅既有的线程池的创建源码【无论是newCached还是SingleThread等等】,我们会发现,其本质是调用了ThreadPoolExecutor类的构造函数!

public ThreadPoolExecutor(int corePoolSize,// 核心线程池大小
                          int maximumPoolSize,// 最大核心线程池大小
                          long keepAliveTime,// 超时释放
                          TimeUnit unit,// 超时单位
                          BlockingQueue<Runnable> workQueue,// 阻塞队列
                          ThreadFactory threadFactory,// 线程工厂,用来创建线程,一般不动
                          RejectedExecutionHandler handler) {// 拒绝策略
}
A 四个常用的内置创建方法
ExecutorService executorService1= Executors.newCachedThreadPool();
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
ExecutorService executorService4 = Executors.newScheduledThreadPool(10);
B 七大参数

对于参数的理解,考虑银行办理业务的例子。银行一共有maximumPoolSize个窗口,其中有corePoolSize个窗口是常开的。银行的候客区是workQueue,候客区的容纳量就是队列的长度。

  • 平常只开放日常窗口,当新来的客户发现窗口已满时,去候客区等待
  • 如果某天特别火爆,候客区也满了,但还在进人,此时银行就会将剩余未开放窗口开放
  • 如果还是源源不断的来客人,则只能采取拒绝策略,要么等待,要么离开
  • 渐渐的,客户都办理完了,有一些窗口已经keepAliveTime都没人访问了,那么银行就会关闭这些窗口,即线程池释放这些线程

注意:

  • 只有请求的线程数大于(CoreSize+Queue的容量),才会创建新线程,相等是不会创建的
  • 最大请求数=最大线程数+Queue的容量,如果超过,则触发拒绝策略
C 4种拒绝策略
new ThreadPoolExecutor.AbortPolicy(); // 抛异常
new ThreadPoolExecutor.CallerRunsPolicy(); // 哪来的回哪去,一般我们都是主线程调用的,所以会把当前线程的任务返回给主线程去做
new ThreadPoolExecutor.DiscardPolicy(); // 队列满了,丢掉任务
new ThreadPoolExecutor.DiscardOldestPolicy(); // 尝试和最早的竞争,竞争失败就丢掉,成功了就执行
D 例
public class Demo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,5,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"ok");
            });
        }
        threadPoolExecutor.shutdown();
    }
}

最大线程数该如何定义呢

线程池处理的任务主要分为两类:

A、CPU密集型——几核,就定义为几——可以看任务管理器的逻辑处理器——或者调用Runtime.getRuntime().availableProcessors();

B、IO密集型——IO十分占用资源,预先估计程序中十分耗IO的线程数量,假设为N,那么最大线程数就应当设为超过N的数,因为IO会长期占着线程,但又不需要上CPU。【比如程序中有10个IO线程,那么最大线程就应该超过10,因为还要保留足够的给其他线程用】

一般我们可以定义为CPU核数+预估IO数

十二、四大函数接口(java8新特性)

新时代的java程序员

1、lambda表达式

2、链式编程【函数式编程】

3、函数式接口

4、Stream流计算

12.1 函数式接口

定义:有且只有一个抽象【不需要写abstract】方法,即只有一个需要实现的方法,其他方法必须是default或者static的,即不需要实现。

特征:接口上有@FunctionalInterface注解

如Consumer接口

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

函数式接口可以简化编程模型,在框架中大量使用

  • 可以先用匿名内部类来尝试使用接口
Consumer<Object> ob = new Consumer<Object>() {
    @Override
    public void accept(Object o) {

    }
};
  • 只要是函数式接口,就可以使用Lambda表达式
Consumer consumer = (str)->{
    System.out.println(str);
};

Lambda表达式本质就是对匿名内部类的简化

12.2 Consumer接口【消费型接口】

只有输入,没有返回值

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
12.3 Supplier接口【供给型接口】

只有输出,没有输入值

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
12.4 Function接口【函数型接口】

传入T类型的变量,返回R类型的变量

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
12.5 Predicate接口【断定型接口】

传入T类型的变量,做出对应检验

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
12.6 lambda表达式的重要特性:
  • **可选类型声明:**不需要声明参数类型,编译器可以统一识别参数值。
  • **可选的参数圆括号:**一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • **可选的大括号:**如果主体包含了一个语句,就不需要使用大括号。
  • **可选的返回关键字:**如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

十三、Stream流式计算

流式计算是把链式编程体现的最淋漓尽致的方式!

/**
 * 现在有5个用户,进行筛选
 * 1、ID必须是偶数
 * 2、年龄必须大于23岁
 * 3、只保留用户名,且将用户名转为大写字母
 * 4、用户名倒着排序
 * 5、只输出一个用户
 */

public class StreamDemo {
    public static void main(String[] args) {
        Student stu1 = new Student(1,"a",21);
        Student stu2 = new Student(2,"b",22);
        Student stu3 = new Student(3,"c",23);
        Student stu4 = new Student(4,"d",24);
        Student stu5 = new Student(5,"e",25);

        // 本质是返回了一个ArrayList
        List<Student> students = Arrays.asList(stu1, stu2, stu3, stu4, stu5);
        // stream来自Collection集合
        students.stream()
                .filter((stu)->{return stu.getId()%2==0;})
                .filter((stu)->{return stu.getAge()>23;})
                .map((stu)->{return stu.getName().toUpperCase();})
                // 就是字符串的比较,D在E前,得负数,所以倒序
                .sorted((student1,student2)->{return student1.compareTo(student2);})
                // 只要1个
                .limit(1)
                .forEach(System.out::println);
    }
}
常用方法
1、Filter方法:
Stream<T> filter(Predicate<? super T> predicate);
// Returns a stream consisting of the elements of this stream that match the given predicate.

即将流中不符合断定接口中条件的删掉【保留断定条件为true的】

2、Map方法
/**
 * Returns a stream consisting of the results of applying the given
 * function to the elements of this stream.
 */
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

即将流中只保留经过函数型接口中处理过后的东西

3、Sorted方法
/**
 * Returns a stream consisting of the elements of this stream, sorted
 * according to the provided {@code Comparator}.
 */
Stream<T> sorted(Comparator<? super T> comparator);

即将流中元素按照比较器排序

4、Limit方法
/**
 * Returns a stream consisting of the elements of this stream, truncated
 * to be no longer than {@code maxSize} in length.
 */
Stream<T> limit(long maxSize);

只保留maxSize个

5、ForEach
/**
 * Performs an action for each element of this stream.
 */
void forEach(Consumer<? super T> action);

对流中的每个元素都操作

原理
  • 流的核心原理就是,经过各种方法操作之后,仍然是一个流。这样做的好处是可以链式编程。

  • 流的计算方法实际上就是把流中的每个元素都拿出来,按照接口给操作一下,再将结果放回流或者舍弃

十四、ForkJoin

ForkJoin是JDK1.7引入的,JDK1.8的并行流底层就是用ForkJoin做的

并行执行任务,提高效率,适用于大数据场合

ForkJoin的核心思想是分治思想,将一个任务拆成几个任务并行执行,再合并起来

ForkJoin特点:工作窃取:线程A和线程B同时执行任务,线程B任务执行完了,他不会闲着,而是去把线程A的任务偷过来执行,从而提高效率

14.1 ForkJoin的使用

两种任务重写方式

  • 递归事件:没有返回值
public abstract class RecursiveAction extends ForkJoinTask<Void> {}
  • 递归任务:有返回值
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {}

使用

public class ForkDemo extends RecursiveTask<Long> {

    private long start;
    private long end;
    private long temp = 10000L;

    public ForkDemo(long start, long end) {
        this.start = start;
        this.end = end;
    }

    // forkjoin的核心在于找到拆分阈值,如果能找到一个合适的拆分阈值,将能达到最优效果
    // 计算方法
    // 挖到低就是个分治
    @Override
    protected Long compute() {
        if ((end - start) < temp) {
            long A = 0;
            for (long i = start; i < end; i++) {
                A+=i;
            }
            return A;
        } else {
            long medium = (start + end) / 2;
            // 递归
            ForkDemo fork1 = new ForkDemo(start, medium);
            ForkDemo fork2 = new ForkDemo(medium, end);
            fork1.fork();
            fork2.fork();
            return fork1.join() + fork2.join();
        }

    }

}

public static void test2() throws ExecutionException, InterruptedException {
    long a = System.currentTimeMillis();
    // 1、创建一个ForkJoinPool
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    // 2、创建计算任务,通过继承RecursiveTask并重写compute方法
    ForkDemo forkDemo = new ForkDemo(0L,10_0000_0001L);
    // 3、提交任务
    ForkJoinTask<Long> submit = forkJoinPool.submit(forkDemo);
    long sum = submit.get();
    long b = System.currentTimeMillis();
    System.out.println(b - a);
    System.out.println(sum);
}

补充——并行流方式:

public static void test3() {
    long sum = 0L;
    long a = System.currentTimeMillis();
    long reduce = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
    long b = System.currentTimeMillis();
    System.out.println(b - a);
    System.out.println(reduce);
}

关于java中的方法引用:

也就是所谓的双冒号

List<String> a1 = Arrays.asList("a", "b", "c");
al.forEach(System.out::println);
// 那么他的本质是
al.forEach((x)->{System.out.println(x)})

请注意,方法引用的前提是,这个方法已经存在,如果你在lambda表达式中的方法体是一个全新的,则不能用。

方法引用的含义就是,根据你的写法找到这个方法。

然后forEach的作用就是,对List中的每个元素都调用一下这个方法,即把每个元素当作参数。那么这种写法不就好理解了。本质就是去System.out找println方法,而且把每个元素当作参数传进去!

这个可以这么理解,这里面的函数接口必须和你提供的方法的返回值和参数一摸一样。即直接套用你传进去的方法。这里面流reduce里需要一个返回long,入参为两个数的方法,我们提供的Long.sum刚好也是两个入参,返回一个long!

  • 类方法引用,例 Long::sum
  • 实例方法引用,例 System.out::sum

十五、异步回调

JAVA Future类详解

Future:设计的初衷:对将来的结果建模

我们的前后端交互常用的axios,它的使用格式是这样的

// 异步执行
axios.get('/user',{
    params: {
        ID: 123
    }
})
// 成功回调
.then(function(response){
	console.log(response);
})
// 失败回调
.catch(function(error) {
    console.log(error);
});

总结一下,分为三步:

  • 异步执行
  • 成功回调
  • 失败回调

那么Java中是否也有对应的实现呢。答案是肯定的,这就是Future接口

那么我们为什么需要异步回调呢:

例:客户端有任务1,2,3

任务1需要服务端的一些计算结果,才能继续执行。那肯定不可能让这个线程就这样傻等着,而是先把他挂起,等服务端结果返回了再调用它,这就叫做异步回调

十六、JMM

16.1 JMM——java memory model

JMM:java内存模型,是java对内存模型做的约定和规范,并不是某种具体的实现

区分于JVM,JVM是对JMM的实现,虽然JVM本身也是一个抽象的

模型结构

在这里插入图片描述

说明

  • 内存分为主内存和工作内存。主内存是线程共享,工作内存是线程独占的
  • 工作流程如图所示,线程需要先将变量读到缓冲区中,再将缓冲区的内容加载到工作内存。同样,写回主存时,也是先将值存储到缓冲区中,再写回主存
16.2 内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
  • write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

16.3 JMM模型对三特特性的解决
原子性

一个操作中不可以在中途暂停然后再调度,要不执行完成,要不就不执行。

  • synchronized关键字
  • 各种Lock锁
可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  • 直接实现方式:

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

  • 间接实现方式:

synchronizedfinal两个关键字也可以实现可见性

有序性

程序执行的顺序按照代码的先后顺序执行。

  • volatile关键字:禁止指令重排
  • synchronized关键字保证同一时刻只允许一条线程操作。
HappenBefore规则:

前面我们提到虚拟机制定了8条规则,从而确保变量的线程安全性【即按照正确的顺序执行】。但是实际中我们一般采用HappensBefore规则,来分析线程安全问题。

  • 原则:
  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
  • 用法:

如果两个操作不存在上述(前面8条 + 后面6条)任一一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

下面就用一个简单的例子来描述下happens-before原则:

private int i = 0;
 
public void write(int j ){
    i = j;
}
 
public int read(){
    return i;
}

我们期望线程A执行write(),线程B执行read(),且线程A优先于线程B执行,那么线程B获得结果是什么?;我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 + 推导的6条可以忽略,因为他们和这段代码毫无关系):

  1. 由于两个方法是由不同的线程调用,所以肯定不满足程序次序规则;
  2. 两个方法都没有使用锁,所以不满足锁定规则;
  3. 变量i不是用volatile修饰的,所以volatile变量规则不满足;
  4. 传递规则肯定不满足;

所以我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,但是就是无法确认线程B获得的结果是什么【第一可能顺序错误,第二可能顺序正确但是不可见】,所以这段代码不是线程安全的。那么怎么修复这段代码呢?如果是顺序错误,用syn强制,如果是不可见,用volatile

具体规则和推导规则见:

【死磕Java并发】-----Java内存模型之happens-before - chenssy - 博客园 (cnblogs.com)

十七、Volatile关键字

17.0 Volatile关键字

Volatile是jvm提供的轻量级的同步机制,有如下三个特征

  • 保证可见性【JMM】

  • 不保证原子性

  • 禁止指令重排【有序性】

JMM的问题:程序不知道主内存中的变量发生了变化!!

public class VolatileDemo {
 static int num = 0;
 public static void main(String[] args) throws InterruptedException {
     new Thread(()->{
         while (num==0){
             // 如果加上打印,在变量改变后就会停止,因为打印的底层是syn的
             //System.out.println("abc");
         }
     }).start();
     TimeUnit.SECONDS.sleep(1);
     num = 1;
     System.out.println("num = 1");
 }
}

如代码所示:新建一个线程,线程中只要变量为0,就不断循环。而在外界改变了这个变量的值后,这个线程不知道,还使用着自己工作内存中的变量,所以不会停止

这就是可见性问题,即某条线程修改了变量的值,这种修改对外界不可见

将变量定义为volatile后就解决了这个问题

1、保证可见性

不加volatile,线程就不知道该变量发生了变化

加了volatile后,可以保证该变量对所有线程的可见性

2、不保证原子性

原子性问题的演示:

public class VolatileDemo {

    static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {

        for (int j = 0; j < 100; j++) {
            new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    num++;
                }
            }).start();
        }
	    // 默认情况下有两条线程:主线程和gc线程	
        while (Thread.activeCount()>2){

        }
        System.out.println(num);
    }
}

可以看到有时结果并不是10000,即volatile不能保证原子性

解释:本来应该是10000,但最后会比10000少很多,原因是,A线程读到的是100,切B,B线程读到的也是100,A+1变101,B+1也变101,两者写回后还是101!所以表面上加了两次,实际上只有1次

这个num++,根本不是原子性操作,别看他只有一行,实际上包括了

  • 读取
  • 加一
  • 写回

这三个操作!

在这里插入图片描述

如果不加锁,怎样保证原子性

  • 用原子类

int → atomicInteger

a.getAndIncrement();// 原子类的+1操作,但其底层用的是native的CAS操作,效率极高
17.1 指令重排问题

在程序写完之后,会经过一系列重排优化的过程,具体如下:

源代码—>编译器优化重排—>指令并行重排—>内存系统重排—>执行

处理器在进行指令重排时,会考虑数据之间的依赖性!如果两条语句存在严格的前后关系,处理器不会对其进行修改。但这样只能确保在单线程下没问题,多线程下可能造成问题

通过volatile,可以禁止指令重排

十八、单例模式!

18.1、单例模式介绍

单例模式是创建型模式,提供了一种创建唯一对象的最佳方式。因为要求类只能创建唯一对象,所以单例类的构造方法应该私有

应用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多【比如 I/O 与数据库的连接】,可以用单例,避免频繁创建
18.2 单例模式的实现
A、饿汉式实现

在加载类的时候就创建。问题是当单例占用大空间时可能浪费空间,因为暂时用不到。

/**
 * 饿汉式的问题:
 * 直接生成对象,可能造成内存空间浪费,因为暂时用不到
 */
public class Hungry {
    /**
     * 占用空间的属性
     */
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private Hungry(){}
    // 饿汉式:一上来就生成实例
    private final static Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }
}
B、懒汉式实现:

常用的实现就是懒汉式实现,懒汉式实现可以等到需要使用的时候再创建

// 懒汉式单例
public class Lazy {
	// 如何判断执行了几次
    // 在这里加一句话就行了,调用了几次构造器,就有几次构造
    // 正常情况这句话只被执行一次
    private Lazy(){
        System.out.println("11");
    }

    // 防止拿到空对象
    private static volatile Lazy LAZY;

    /**
     * @DCL懒汉
     * 双重检验,不是双重锁
     * 第一重检验的作用是提高效率,如果没有第一个j,大量线程阻塞在syn这里,而实际情况是如果已经有实例了,就不要再等了,直接拿着实例走人
     * 第二重检验的作用是保证单例,因为并发,可能两个线程同时通过了第一重检查,此时如果没有第二重锁,那不就错了!
     */
    public static Lazy getInstance(){
        if(LAZY==null){
            synchronized (Lazy.class){
                if (LAZY == null) {
                    LAZY = new Lazy();
                    /**
                    * 这并不是一条原子性语句
                    * 1、分配内存空间
                    * 2、执行构造方法,初始化对象 
                    * 3、把这个对象指向这个空间
                    * 我们期待的执行顺序是123,但实际执行的顺序可能是132
                    * 对某条线程而言,123还是132没区别,因为syn保证了这是原子性的
                    * 但是对另一条线程而言,132会有问题,因为13执行后,LAZY就不再指向null了,此时线程B过来,发现LAZY!=null,直接走最下面的语句,return LAZY,但LAZY虽然有空间,里面却啥都没,此时B再调用单例,就会报错,所以要加volatile防止重排
                 	*/                
                }
            }
        }
        return LAZY;
    }
}
C、静态内部类实现:
public class StaticInnerClass {

    private StaticInnerClass(){};

    public static StaticInnerClass getInstance(){
        return InnerClass.Single;
    }

    public static class InnerClass{
        private static final StaticInnerClass Single = new StaticInnerClass();
    }

}
18.3 单例模式存在的问题

以上三种实现方式都存在被反射破坏的可能

以常用的懒汉为例

// 懒汉式单例
public class Lazy {
    // 可以对该字段加密,从而更安全
    private static boolean marscarm = false;

    // 检验构造器执行次数,在这里加一句话就行了,调用了几次构造器,就有几次构造,正常情况只执行一次
    private Lazy(){
        synchronized (Lazy.class){
            if(marscarm==false){
                marscarm = true;
            }
            // 针对test2的解决
            else {
                throw new RuntimeException("小B崽子你是真没见过黑社会!");
            }
            // 针对test1的解决
            //if (LAZY!=null){
            //    throw new RuntimeException("不要试图用反射破坏单例!");
            //}
        }
        System.out.println("A new Instance has been created");
    }

    private static Lazy LAZY;

    public static Lazy getInstance(){
        if(LAZY==null){
            synchronized (Lazy.class){
                if (LAZY == null) {
                    LAZY = new Lazy();
                }
            }
        }
        return LAZY;
    }


}

class Test{
    public static void main(String[] args) throws Exception {
        //test1();
        //test2();
        test3();
    }


    // 一个正常获得,一个反射获得
    public static void test1() throws Exception{
        Lazy instance = Lazy.getInstance();
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Lazy lazy = declaredConstructor.newInstance();
        System.out.println(instance==lazy);
    }

    // 根本不鸟你的内部变量
    public static void test2() throws Exception{
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Lazy lazy1 = declaredConstructor.newInstance();
        Lazy lazy2 = declaredConstructor.newInstance();
        System.out.println(lazy1==lazy2);
    }

    public static void test3() throws Exception{
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Lazy lazy1 = declaredConstructor.newInstance();

        Field marscarm = Lazy.class.getDeclaredField("marscarm");
        marscarm.setAccessible(true);
        marscarm.set(lazy1,false);

        Lazy lazy2 = declaredConstructor.newInstance();
        System.out.println(lazy1==lazy2);
    }


}

以上过程模拟了通过反射破坏单例的三次升级过程。常规的方式其实是不能防止反射破坏的,因为反射可以获取类的构造器、字段等一切信息,还可以更改访问限制【setAccessible】。所以终究是无法防止的。

18.4 枚举单例

枚举单例是我们最推荐使用的,因为枚举类从底层源码上直接就限制死了不可能通过反射创建。

以下是一个使用枚举单例的例子,只初始化一次对象,在初始化比较费时间时,可以大幅提高系统效率

// 枚举是特殊的类
public enum EnumSingle {

    INSTANCE;

    private boolean isInit = false;

    private int init() {    //初始化函数,耗时较长
        System.out.println("开始初始化...");
        try {//模拟消耗资源
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int result = 0;//初始化成功返回0
        if(result == 0){
            isInit = true;
        }
        return result;
    }

    public int transfer(){  //转换函数【工作函数】
        // 只有第一次需要初始化,之后就不需要了
        if(!isInit){
            int res = init();
            System.out.println("核心初始化结果:"+res);
        }
        System.out.println("开始转换...");
        return 0;
    }

    public static void main(String[] args) {
        //在其他类使用示例
        EnumSingle enumSingle = EnumSingle.INSTANCE;//获取单例
        enumSingle.transfer();//转换函数
    }
}

十九、深入理解CAS(CompareAndSet)

19.0 CAS的原理

CAS即CompareAndSet,是计算机底层的并发原语。

CAS要求内存的实际值必须等于期望值,否则不更新。

19.1 Unsafe类

java无法操作内存,但java可以调用c++操作内存【native方法】

unsafe就是java用来调用c++的封装类,Unsafe类提供一系列增加Java语言能力的操作,如内存管理、操作类/对象/变量、多线程同步等【对java起到扩展作用】

19.2 getAndIncrement底层探究

我们知道,原子类的getAndIncrement可以解决变量++问题,那么它的底层实现是什么呢

①我们来首先看一下方法的实现:

public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

②这里的U是unsafe类的实例,请看

// 这里是用的反射获取内存地址的偏移值【理解成通过这个东西就能得到内存地址即可】
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
// 这个value就是AtomicInteger的值
private volatile int value;

这一段代码设计到两个变量,分别是value、VALUE

  • value:AtomicInteger的值
  • VALUE:这个对象的内存地址

③明白了U和value是什么,我们点进去看下getAndAddInt

// 源码解读:
/**
* o: 操作对象
* offset: 内存偏移值
* delta: 变量
*/
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
    // 这里本质是一个自旋锁
}

这段代码中出现了一个变量v,这个v是getIntVolatile得到的,而这个方法的意思是,由对象o和它的内存偏移地址得到对象o的值【从内存中】

④那我们去看一下while中的这个方法

public final boolean weakCompareAndSetInt(Object o, long offset,
                                          int expected,
                                          int x) {
    return compareAndSetInt(o, offset, expected, x);
}

发现while中的方法底层是c++的cas

@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
                                             int expected,
                                             int x);

那么到这里,我们就明白第三步的意思了,第三步意思是持续的尝试获取内存中的值,并将该值设为v+delta,直到成功。这步操作本质是一个自旋锁,因为while条件中期待值为v,这意味着一旦内存中的值发生变化,与期待值不一致,cas操作就会失败,while条件为true,进行下一次尝试,直到内存中的值确实是刚刚读取的值。从而通过自旋锁保证读-写操作的原子性。

对比:

  • syn的原子性是直接对读写整个操作过程加锁
  • 原子类的原子性是通过读+CAS来实现的,如果读的和期望的不一样,就重新进行
19.3 CAS总结

CAS底层是个轻量级的自旋锁,即自己不停的循环直到满足条件

  • 好处:自带原子性

  • 缺点:①循环耗时且CPU空转,浪费资源;②一次性只能保证一个共享变量的原子性;③会存在ABA问题【最严重的】

补充:

CAS底层是带Lock前缀的指令

intel手册对 lock 前缀的说明如下:

  1. 确保被修饰指令执行的原子性。
  2. 禁止该指令与前面和后面的读写指令重排序。
  3. 指令执行完后把写缓冲区的所有数据刷新到内存中。(这样这个指令之前的其他修改对所有处理器可见。)

十九、ABA问题和原子引用

19.1 ABA问题

ABA问题即所谓的偷梁换柱问题。

问题描述:

现在内存中有一个共享变量A=1

线程A读取了两次变量A,发现A都为1,于是A认为这个变量没有更改过。

线程B的任务是:期望A=1,将值改为3。然后再期望A=3,将值改为1。

那么可能出现这种情况:线程B执行的很快,即内存中的变量的变化情况如下:

A=1→A=3→A=1

然而A再来看内存中的A,发现仍是1,就以为啥也没有变,然后依然执行了CAS操作。然而实际上该量发生过改变

有些系统中对变量变化过程是十分敏感的。例如,银行卡账户。

系统中记录的是小明账户原有50元。突然系统发生故障,无法记录每笔交易,然而此时小强给小明转了50元,之后骗子盗刷了小明的卡50元。一段时间后,系统恢复,系统再次查看小明的账户,发现仍是50元,就认为没问题,然而事实是小明损失了50元。

这个例子刚好解释了如何应对ABA问题,那就是添加一个状态变化记录装置。每次状态变化就记录一下

19.2 ABA问题解决

那么我们该如何解决ABA问题呢:

可以通过类似数据库乐观锁的形式,每发生一次变化,就将版本号+1,通过对比版本号,就得知是否发生了变化

19.3 原子引用

原子引用:AtomicReference

原子类new的时候必须传入参数,否则会默认初始化零值。如Integer默认为0,其他类型默认为null

带版本号的原子引用:AtomicStampedReference类
public class AtomicStampDemo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomic = new AtomicStampedReference<>(10, 1);

        new Thread(() -> {
            System.out.println("A"+atomic.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomic.compareAndSet(10, 11, 1, 2);
            atomic.compareAndSet(11, 10, 2, 3);
            System.out.println("A"+atomic.getStamp());
        }, "A").start();

        new Thread(() -> {
            System.out.println("B"+atomic.getStamp());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomic.compareAndSet(10, 100, 1, 2));
        }, "A").start();
    }
}

如代码所示,我们在创建这个类的时候,需要同时传入一个版本号【stamp】,即使用这个类,既需要比较值,又需要比较版本号。在修改成功后,会更改版本号的值,从而解决了ABA问题

19.4 注意

如果传入的泛型Integer,然后范围超过-128-127,将会认为是不同的值

该类的底层是==比较的,所以你用Integer超过缓存范围就会创建新的对象,从而再比较地址认为不同。

该类始终是比较地址,而不调用equals方法,所以目前不知道它的使用场景是什么样的

二十、java中的各种锁

20.1 公平锁、非公平锁

公平锁:不能插队,必须先来后到!

非公平锁:可以插队,效率更高【默认都是非公平】

// 传入一个false就能得到公平锁 
ReentrantLock reentrantLock = new ReentrantLock(false);

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
20.2 可重入锁(递归锁)

可重入锁的意义就是一个同步方法内部可以调用另一个同步方法

  • syn方法内部可以调用syn
public class SYN {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.A();
        },"线程1,").start();
        new Thread(() -> {
            phone.A();
        },"线程2,").start();
    }
}

class Phone {

    // syn默认是可重入锁,所以A可以无缝执行衔接B!
    // 而且A不会执行完打印A后就释放锁,而是整个A方法执行完之后才释放锁
    // 那么可重入锁就可以理解为:
    // 对于同一把锁,持有该锁时可以同时执行其他需要该锁的方法!

    public synchronized void A() {
        System.out.println(Thread.currentThread().getName() + "A");
        B();
    }

    public synchronized void B() {
        System.out.println(Thread.currentThread().getName() + "B");
    }


}
  • lock内部可以重复lock
public class LOCK {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.A();
        },"线程1,").start();
        new Thread(() -> {
            phone.A();
        },"线程2,").start();
    }
}

class Phone1 {
    Lock lock = new ReentrantLock();
    public void A() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "A");
            B();
        } finally {
            lock.unlock();
        }
    }
    public void B() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "B");
        } finally {
            lock.unlock();
        }
    }
}
20.2 自旋锁

CAS操作实现自旋锁,自旋锁可以当作正常锁使用,只是浪费CPU

public class SpinLock {

    /**
     * 这里我们要理解自旋锁和普通锁的区别,普通锁是你拿到锁才可以进行接下里的操作,自旋锁是你不符合条件,就被阻拦在这里。
     * 那么显然,第一个线程调用完lock后,原子引用由null变为当前线程,那么第二个线程过来,发现原子引用已经不是null了,就被死循环卡住,除非第一个线程调用unlock解锁,将原子引用重新置为null
     * 由于底层是cas操作,所以自动保证了可见性【每次都是拿内存变量和期望值比,即每次都是拿最新的值来比】
     */
    
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock(){
        Thread thread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null,thread)){
        
        }
    }

    public void unlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
    }

}

class TestSpin{
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();

        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unlock();
        }).start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.lock();
            System.out.println("abcdef");
            spinLock.unlock();
        }).start();
    }
}

二十一、死锁

21.1 问题描述
public class DeadLock {
    public static void main(String[] args) {
        new Thread(new MyThread("A","B"),"T1").start();
        new Thread(new MyThread("B","A"),"T2").start();
    }
}
class MyThread implements Runnable{
    // 这里应该是锁住了对象,因为字符串一样,所以常量池中位置相同

    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"lock: "+lockA+" get:"+lockB);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"get another lock,over!");
            }
        }
    }
}
21.2 解决办法

1、jps——定位进程号【查看当前正在执行的java进程】

2、jstack——查看进程信息、检查死锁

在这里插入图片描述

在这里插入图片描述

面试,问如何排查问题

1)日志问题

2)堆栈信息

面试补充:Volatile、Synchronized、Final深入理解

Volatile:【我们在面试的时候,由volatile往单例上去引,然后就可以吹牛逼了】
A、可见性

实现原理:

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据会立即写回到系统内存。但是,仅此并不能保证可见性,因为其他线程看不到该变量发生了变化。所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,然后在需要使用时重新从系统内存中把数据读到处理器缓存里。

内存语义:

  • volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,并使其他线程的工作内存中该值无效。
  • volatile读的内存语义:当读一个volatile变量时,线程将从主内存中读取共享变量。
B、有序性

指令重排序对程序执行的影响:

// volatile num = 1; volatile count =2;
// 线程A
num = 3;
count = 4;
// 线程B
int tempCount = count;
int tempNum = num;

问题:本来我们预期的流程是A1,B1,B2,A2,这样tempCount值为2,tempNum值为3

但是可能存在指令重排,排成了A2,B1,B2,A1,这样tempCount值为4,tempNum值为1。

所以我们应当在关键部分禁止指令重排,CPU是不会管你指令重排会影响结果的,在CPU眼里,只要当前线程的两个操作互不影响【这里num、count就互不影响】,就可以给你重排喽。

Volatile通过内存屏障解决了这个问题。

JMM针对volatile指定的重排序规则:【其实就是对所有volatile变量的操作都不能重排序】

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序【禁止前后交换】。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

C、原子性

volatile保证了对变量的单独读、写操作的原子性,但是变量++这种复合操作无法保证。因为这个过程实际上是读、计算、写三个操作。想要实现该操作的原子性,可以通过

  • 同步技术(锁)
  • 原子操作类——AtomicInteger
Synchronized:

Synchronized是锁的一种,那么我们先讨论一下锁的内存语义

A、锁的内存语义:
  • 当线程释放锁【unlock】时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中【store和write操作】
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。然后执行引擎使用这个变量前需要重新执行read和load操作读取变量的值
B、锁的底层实现

①首先明白:所有锁都是锁的对象【即使锁类本质也是锁class对象】

②锁本质是在java锁对象头里操作的

  • 任何一个对象都有一个monitor与之关联,当monitor被某条线程持有后,锁对象就处于了所谓的“锁定状态”,即另一条线程无法再获得该锁

  • monitor的持有过程:

    synchronized会在编译时被转化成monitorentermonitorexit,前者被插入到同步代码块开始的地方,后者则被插入到方法的结束处和异常处

    当某条线程要执行该同步方法时,首先尝试执行monitorenter命令,如果发现锁的计数器为0,则执行成功,并将锁的计数器+1;否则获取锁失败,阻塞等待【JDK1.6之前,之后有优化,见下文】

    当拥有锁的线程执行完了,就会释放锁,即执行monitorexit,将锁的计数器减一。

  • syn的可重入性:

    如果同一个线程在已经获取锁之后又执行该对象的其他同步方法【即再次获得该锁】,会将锁的计数器再+1,以此类推

③锁的底层实现

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

==由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。==所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中:

synchronized与java.util.concurrent包中的ReentrantLock相比,==由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。==ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

C、Synchronized优化【AKA:锁升级过程】

JDK1.6 之后,syn不再是直接切到操作系统内核态去了,而是从最基础的偏向锁开始,在并发逐渐升级的情况下再最终升级到重量级锁,切换到操作系统内存去

这里我们大致了解锁升级过程就即可,十分底层的细节不再深究

无锁→偏向锁→轻量级锁→重量级锁

偏向所锁,轻量级锁都是乐观锁【通过偏向线程ID实现乐观】,重量级锁是悲观锁。

  • 一个Syn锁对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,锁对象持有偏向锁。偏向第一个线程,这个线程在修改锁对象头成为偏向锁的时候使用CAS操作,并将锁对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作
  • 一旦有第二个线程访问这个锁对象,因为偏向锁不会主动释放,所以第二个线程可以看到锁对象的偏向状态,这时表明在这个锁对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象恢复成无锁状态,然后重新偏向。
  • 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

注:CAS操作本确实是os内核态的指令,但java虚拟机的实现是在用户态运行了内核态的cas指令

D、其他优化

锁消除

锁消除即删除不必要的加锁操作。虚拟机即时编辑器在运行时,对一些“代码上要求同步,但是被检测到不可能存在共享数据竞争”的锁进行消除。

根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

在这里插入图片描述

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去(即StringBuffer sb的引用没有传递到该方法外,不可能被其他线程拿到该引用),所以其实这过程是线程安全的,可以将锁消除

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

在这里插入图片描述

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

E、关于轻量级锁的自旋
  • **引入自旋锁的原因:**互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
  • **自旋锁:**让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器额执行时间),看持有锁的线程是否会很快释放锁。自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启。
  • **自旋锁的缺点:**自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,例如让其循环10次,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。
  • **自适应的自旋锁:**JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
  • **自旋锁使用场景:**当一个线程获得了偏向锁,又有线程来竞争时,锁升级为偏向锁,一个使用,一个自旋
F、总结
  • synchronized保证了操作的原子性和可见性,但不能保证局部有序【实际上局部是否有序无所谓了,因为所有syn都是串行化进行的,这一整个操作弄完了才能到下一步】

  • Synchronized效率低的原因:

    ①额外操作【加锁、解锁】

    syn处于重量级锁时,阻塞和唤醒【挂起线程与恢复线程】涉及到操作系统内核与用户态的转换

Final

Final保证了可见性和有序性。

因为Final是一旦初始化就不可以改变的,所以保证了可见性。

  • JMM禁止把Final域的写重排序到构造器的外部。
  • 在一个线程中,初次读该对象和读该对象下的Final域,JMM禁止处理器重新排序这两个操作。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值