JUC 多线程-并发编程

 

多线程基础

  一个Java程序实际上是一个JVM进程,

  JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。

  此外,JVM还有负责垃圾回收的其他工作线程等。

  内存角度:单线程相当于栈空间里的函数压栈、串行运行;多线程是每个线程开辟一个栈空间,CPU给多个线程并发运行。

什么是JUC

 jdk包的三个首字母Java Util Concurrent

 

准备工作:

保证IDEA设置:

1)File=>Project Structure=>Modules + Project这两个都是java-8版本

2)File=>Setting=>Build=>Compiler=>Java Compiler是java-8版本


多线程状态&转换:


 

 sleep 休眠

  模拟网络延时、倒计时(单位是毫秒ms)

 

  

TimeUnit.SECONDS.sleep(5);  //休眠

 

yield 礼让

  运行态==>就绪态

  

   就绪态回到运行态后,会继续执行run()方法后面的内容

 

join 强制执行(插队)

   

 

interrupt 中断(他杀)

  thread.interrupt();  //中断线程是一个线程,让另一个thread中断

 

wait(配合notify)

wait必须在同步代码块中(可结合线程状态图来理解)

(sleep可以在任何地方) 


 

 

jvm中的runnable相当于上图中操作系统状态的running+runnable

统称为阻塞状态三种:它们分别是 Blocked(同步锁定阻塞)Waiting(等待)Timed Waiting(计时等待) .

 

 JVM 6状态:

 1) 观察线程状态(6种)

    

2)获取线程名字:

   

3)

 

创建 多线程(3种方法):

 

 

三种方法创建线程:

 

1. 继承Thread类,重写run()方法(不推荐,因为java单继承,如果继承Thread就占用了继承的名额)

  

 

   

   这时候main的线程和TestThread1的线程并发执行

 

2. 实现Runnable接口(无返回值)

 

   

 

3. Callable和Future submit接口(有返回值)

 

 例子:

  

   

 

 

CompleteFuture(Future的优化版):

  

 

Callable

callable和runnable的区别:

可以有返回值、可以抛出异常

方法不同:run() 和 call()

 

 

 


 

线程优先级 (大的优先) 

  

 


后台线程 daemon (也叫守护线程)

   

  实例:

   

 

 

同步锁 Synchronized

  1. 同步方法:临界资源 方法名前 加上"synchronized"关键字;加锁对象默认是this

  2. 同步代码块:创建同步的块,将临界资源及其操作放到里面:

只要在方法前加上一个关键词就好了

    public synchronized void sell(){

volatile 和 Synchronized 区别

作用级别:volatile 只用于变量;    变量、方法、类
可见性&原子性:volatile可见性,不保证原子性;synchronized 修改可见性、原子性
阻塞:volatile不阻塞;synchronized 阻塞。
编译器优化:volatile不会优化(内存屏障);synchronized可以优化

 

 

Lock锁

ReentrantLock 可重入锁 (re entrant lock)

   

   

 

 


 Lock锁示例:

    Lock lock = new ReentrantLock();
    public void sell(){
        lock.lock();
        try {
            if(number>0){ //限制票数大于0
                System.out.println(Thread.currentThread().getName() + "卖出了一张票,还剩下:" + (number--) + "张票。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    } //lock锁必须成对( 一个lock() + 一个unlock() , 不然会死锁 )

 

ReadWriteLock 读锁+写锁  (ReadLock、WriteLock;读锁==共享锁;写锁==独占锁

   

 

 例子:

   

 

 读锁、写锁分别使用:

 

 

Lock和Synchronized的区别:

相同点:

  都是可重入锁

区别:

  lock是显式锁(手动开关锁);  synchronized隐式锁(出了作用域自动释放)

  lock只有代码块锁,  synchronized有方法锁、代码块锁

  lock锁,JVM调度线程花费小、性能好。并且扩展性好(提供更多子类)

 

  优先使用顺序:lock>synchronized代码块>synchronized方法

 

 

乐观锁、悲观锁

乐观锁:

假定不发生冲突,提交时,检查版本、回退。

CAS(自旋锁:达到预期就改,否则自旋等待)

ABA问题(A=>B=>A)解决方法用版本机制,AtomicInteger.getStamp()

悲观锁:

假定发生冲突,用锁把事务锁起来:

代码块加锁synchronized

MySQL排它锁(写锁、X锁)

 

 

2)公平锁、非公平锁

公平锁:不能插队

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

3)可重入锁  ReentrantLock

可重入锁也叫:递归锁

    => 拿到外面的锁,就自动获得里面的锁

4)自旋锁  SpinLock

    => 不断循环尝试,直到成功为止

5)死锁排查

遇到死锁(不会爆异常),Terminal命令行操作:

    1)使用  jps -l  来定位进程号:

=> 可以找到死锁对应的进程号

    2)使用 jstack + 进程号 来查看具体信息:

  

 

 

进程和线程

区别

根本区别:

  进程是操作系统 资源分配 的基本单位,而线程是处理器任务 调度和执行 的基本单位

资源开销:

  每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:

  如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:

  同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:

  一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮

执行过程:

  每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行


多个线程共享进程的:堆 + 方法区资源,

每个线程有自己的:程序计数器 + 虚拟机栈 + 本地方法栈


进程:QQ.exe  Music.exe  有java环境的.jar

java默认有2个线程:main、GC垃圾回收

开线程方式:Thread(继承)、Runnable(实现接口)、Callable(实现接口+有返回值)

java无法直接开线程,start() 要调用native本地方法,用底层的C++来操作硬件

System.out.println(Runtime.getRuntime().availableProcessors());    //获取CPU核数

 并发编程的目的:充分利用多核CPU的资源


 

线程间通信

1)wait + notify 生产者消费者:

  • wait ()
  • notify() /notifyAll(

ps: notifyAll 更常用

ps: wait()还有种用法就是wait(1000)这样的加上时间参数,在等待时间结束之后,就不等notify()了

2)join() 方法==>插队

使用场景:

  主线程创建并启动子线程,如果子线程中进行大量的运算,主线程往往早于子线程结束。这时主线程要等待子线程完成之后再结束

  比如子线程处理一个数据主线程要取得这个数据中的值,就要用到join()方法

join()方法就是等待线程对象销毁。

join的实现其实是基于等待通知机制(wait+notify)。

3)Volatile (利用 可见性)内存共享

  • 每次修改变量后,立刻回写到主内存。
  • 在此过程中,会通知其他线程读取新值。

4) 管道通信

5)三个常用的辅助类=》计数(CountDownLatch、CyclicBarrier、Semaphore

(1)CountDownLatch 用来倒数

(2)CyclicBarrier  线程计数器

(3)Semaphore  信号量

 

 


进程间通信

套接字(socket)网络通信

消息队列(messagequeue)

管道(pipe)

  半双工

  管道分为pipe(无名管道)和fifo(命名管道)两种

  pipe:父子进程

信号量(semaphore) ==》PV操作(同步、互斥

共享内存(shared memory)

 

 


 

生产者消费者

例子:2个线程交替执行

/**
 * 线程之间通信问题,线程交替执行
 * wait notify
 * */
public class Test {
    public static void main(String[] args) {
        MyData myData = new MyData();  //新建对象
        new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    myData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"product").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    myData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"product-2").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    myData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"consumer").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    myData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"consumer-2").start();
    }
}

class MyData{//数字 资源类
    private int number =0;

    public synchronized void increment() throws InterruptedException {
        while(number != 0)this.wait();  //wait要放在while中     //不能用if,if会虚假唤醒
        number++;
        System.out.println(Thread.currentThread().getName()+ "=>" + "number = " + number);
        this.notifyAll();
    }
    public synchronized void decrement() throws InterruptedException {
        while(number == 0)this.wait();
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number);
        this.notifyAll();
    }
}

 

JUC版 生产者消费者(Condition + lock)

Synchronized => Lock

wait + notifyAll => Condition (await + signal)

 1)Condition + lock 替代原有锁的功能

class MyNewData{//数字 资源类
    private int number =0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while(number != 0)condition.await();  //wait要放在while中     //不能用if,if会虚假唤醒
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }
    public synchronized void decrement() throws InterruptedException {
        lock.lock();
        try {
            while(number == 0)condition.await();
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }
}

2)Condition + lock 精准通知唤醒

 例子:3个线程循环

package ProducerConsumer;

import lombok.Data;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 晋青杨 j50016344
 * @create 2021-04-26
 **/
public class Test_3 {
    public static void main(String[] args) {
        Data3 data3 = new Data3();
        new Thread(()->{
            try {
                for(int i =0;i<10;i++)data3.printA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread_A").start();
        new Thread(()->{
            try {
                for(int i =0;i<10;i++)data3.printB();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread_B").start();
        new Thread(()->{
            try {
                for(int i =0;i<10;i++)data3.printC();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread_C").start();
    }
}

@Data
class Data3{
    private Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();  //Alt+Enter自动生成等号左边
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();  //【重点】 建立3个condition,每个都具有自己的await和signal,可以精准的等待、唤醒private int number =1; //1A  2B  3C

    public  void printA() throws InterruptedException {
        lock.lock();
        try {
            while(number!=1)condition1.await(); //1等待
            System.out.println(Thread.currentThread().getName() + "=> A");
            //唤醒2:
            number =2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }

    public  void printB() throws InterruptedException {
        lock.lock();
        try {
            while(number!=2)condition2.await();
            System.out.println(Thread.currentThread().getName() + "=> B");
            //唤醒3:
            number =3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }

    public  void printC() throws InterruptedException {
        lock.lock();
        try {
            while(number!=3)condition3.await();
            System.out.println(Thread.currentThread().getName() + "=> C");
            //唤醒1:
            number =1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }
}

 

8锁问题

/**
 * ====== 8锁:关于锁的8个问题 ======
 * 1) 在中间有1s间隔情况下,两个线程是先 send 还是 call ? 先send => 因为先在synchronized那里抢到锁,就会先执行 => send第0s  call第1s
 * 2) send在1)的基础上,方法内部追加3s的延迟,两个线程是先 send 还是 call ? 先send => 补充:send和call都是在第3s执行
 * 3) 新定义一个不加锁的方法:hello,然后线程B来调用此方法。=> 先hello,再send => hello在第一秒运行到,此时也不用抢锁,直接执行
 * 4) 如果两个实例对象,就会有两个不同的锁;两个对象互不影响
 * 5) 6)将类中的方法用static修饰,于是锁的是类,于是两个实例对象也会共用一把锁
 * 7) 8)一个方法是 静态加锁; 另一个是 普通加锁 => 用的是两个锁,只有静态的方法才会参与类加锁
 * 上面的 5)7)是一个实例对象;  6)8)是两个实例对象
 * */

public class lock8 {
    public static void main(String[] args) throws InterruptedException {
//        Phone phone1 = new Phone();
//        Phone phone2 = new Phone();
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendMessage();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread_A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.hello();
        },"Thread_B").start();
    }

}

class Phone{
    // synchronized锁的对象是:方法的调用者 => 调用者:普通情况下是对象,加static后是类
    public static synchronized void sendMessage() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);  //由于这里会等2s所以:如果共用一把锁,则send会先执行,否则别的方法先执行
        System.out.println("send message");
    }
    public synchronized void call(){
        System.out.println("call");
    }
    //没有锁的方法:
    public void hello(){
        System.out.println("hello");
    }
}

 

集合类不安全

Java STL内置的线程安全的Concurrent Collection:

 

          

 

Atomic原子操作的封装类

  

getAndIncrement()  //num++

IncrementAndGet()  //++num

  • 原子操作实现了无锁的线程安全;

  • 适用于计数器,累加器等。

AtomicInteger

  利用 CAS (compare and set) + volatile 来保证原子操作,

  从而避免 synchronized 的高开销,执行效率大为提升。

UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。

另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

CAS(CompareAndSet,比较并更新)

    => 如果期望的expect值达到了,就set;否则就不更新,并一直循环等(自旋锁)

CAS缺点:

1、循环会耗时

2、一次性只能保证一个共享变量的原子性

3、ABA问题

原子引用 解决ABA问题

ABA问题就是:A->B->A,其他线程以为没有变化,但实际上是改变过了的

解决方法就是用时间戳来判断有没有被动过

 

 CAS比较并更新的对象是:

  值+stamp 2个内容

 


ConcurrentHashMap原理

HashMap:

Map<String, String> map = new HashMap<String, String>(16, 0.75f);
// HashMap 加载因子,初始容量

ConcurrentHashMap并发原理:

jdk1.7:

  Segment + HashEntry 数据结构,对Segment加锁;各个Segment之间互不影响

jdk1.8:

  数组 +(链表 / 红黑树),对链表头结点 / 红黑树根节点  加锁;

  各个(链表 / 红黑树)之间互不影响

CAS + synchronized实现更加细粒度的锁

JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★

  • 在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
  • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

 

读写锁  ReadWriteLock

读写分离,读可以并发,写只能用一个线程。

 

阻塞队列 BlockingQueue

BlockingQueue是Collection下和Set、List平级的类

运用:多线程并发处理,线程池

 

 

线程池

优点:

  • 避免重复创建、销毁线程;提高性能
  • 便于线程管理(可以控制最大并发数、时间等)

 线程池必会:3大方法、7大参数、4种拒绝策略

 

三大线程池:

SingleThreadExecutor:单线程池

FixedThreadPool:线程数固定的线程池

CachedThreadPool:线程数动态调整

 => 3种方法只是封了一层,使得输入的参数减少了。源码中都是通过ThreadPoolExecutor来产生的线程池,只是3种之间的参数不一样。

 

7大参数:(3个核心参数)

(建议用 ThreadPoolExecutor 7大参数来自定义,而不是 Executors 去创建,否则可能会导致 OOM。)

    public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小int maximumPoolSize,  //最大线程池大小。定义策略:CPU密集型=>几核就定义几个(如12个) 
                              long keepAliveTime,  //超时没有使用 就会释放
                              TimeUnit unit,    //超时 时间单位
                              BlockingQueue<Runnable> workQueue,  //阻塞队列(含同步队列)
                              ThreadFactory threadFactory,  //线程工厂:固定用于创建线程
                              RejectedExecutionHandler handler) { //拒绝策略:超过maximumPoolSize之后的拒绝策略   //7大参数

 

如果:需要的线程数 > 最大线程池(获取到线程的个数) + 阻塞队列(排队中),就会触发拒绝策略

  

 4种拒绝策略:

  Abort【默认】:超出线程,会抛出异常

  CallerRuns:超出线程,会还给main或者调用它的地方

  Discard:直接丢掉多余的任务,不抛异常

  DiscardOldest:尝试与最早使用线程池的线程竞争,而不是直接丢掉。也不会抛异常

 

最大线程池大小,定义策略(调优):

1)CPU密集型 =>几核就定义几个(如12个)

int coreNum = Runtime.getRuntime().availableProcessors(); //因为不同的电脑不一样,通过此方法动态获取CPU核数
System.out.println("coreNum = " + coreNum);

2)IO密集型 => 比如有15个大型io密集任务,就开辟大于任务数的线程数

设置线程池大小maximumPoolSize为 约2倍 的任务数即可。

 

 

Java8新特性

四大 函数式接口(重点)

函数式接口 Function、断定型接口 Predicate、消费型接口 Consumer、供给型接口 Supplier

1)函数型接口Function:只有一个方法的接口

典型例子:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

简化编程模型,在新版本的框架底层大量应用

函数式编程  例子:

import java.util.function.Function;

public class Test {
    public static void main(String[] args) {
        Function function = new Function<String,Integer>() {  //泛型中一个入参 + 一个出参,与里面重写的apply的入参、出参类型对应
            @Override
            public Integer apply(String str) {  //与上方的类型对应
                return str.length();
            }
        };
        System.out.println(function.apply("2199"));
    }
}

 简写为lambda表达式:

Function<String,Integer> function = (str)->{ return str.length(); };

 

2)断定型接口Predicate:

有一个输入值,和一个返回的bool值(用于判断返回真伪)

Predicate <String> predicate = s -> {return s.isEmpty();};  //lambda表达式来写
System.out.println(predicate.test(""));

 

3)消费型接口Consumer:

只有入参,没有返回值

Consumer <String> consumer =(str)->{ System.out.println("打印内容:" + str); };
consumer.accept("2199");

 

4)供给型接口Supplier:

没有入参,只有返回值

Supplier<String> supplier = ()->{return "2199";};
System.out.println(supplier.get());

 

 

Stream流式计算

大数据:存储 + 计算

  存储 => 集合、MySQL

  计算 => 交给流

public class StreamTest {
    public static void main(String[] args) {
        User u1 = new User(1,"a",21);
        User u2 = new User(2,"b",22);
        User u3 = new User(3,"c",23);
        User u4 = new User(4,"d",24);
        User u5 = new User(5,"e",25);
        User u6 = new User(6,"f",26);
        User u7 = new User(7,"g",27);
        //集合用于存储
        List<User> list = Arrays.asList(u1,u2,u3,u4,u5,u6,u7); //记住这套路
        System.out.println("list = " + list);

        /**
         * 题目要求:
         * 1) id是奇数
         * 2) age大于22
         * 3) 用户名转换为大写
         * 4) 按照用户名倒叙排列
         * 5) 限制输出2个用户
         * */

        //计算用流stream
        list.stream()
                .filter(u-> u.getId()%2==1)  //.filter里面直接放bool条件
                .filter(u-> u.getAge()>22)
                .map(u->{u.getName().toUpperCase(); return u;})
                .sorted((x,y)->{return y.getName().compareTo(x.getName());}) //倒序
                .limit(2)
                .forEach(System.out::println);
    }//流式计算 + 链式编程 + lambda表达式 + 函数式接口, jdk-8的四大要素都有了
}

jdk-8 时代程序员:lambda表达式、函数式接口、链式编程、Stream流式计算

 

分支合并 ForkJoin(jdk1.7)

 ForkJoin在jdk1.7,并行 执行任务,提高效率,大数据量

大数据MapReduce:把大任务拆分成小任务

 

 ForkJoin特点:工作窃取 => 一个线程干活太快,把别的线程的任务抢过来

 双端队列Deque:从两边都可以取出来

 代码理解:

public class Test {
    @org.junit.Test
    public void test1(){
        long startTime = System.currentTimeMillis();
        Long sum = 0L;
        for (Long i = 0L; i<=10_0000_0000; i++){  //10_0000_0000量级
            sum += i;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //朴素计算=>5.4秒
    }

    @org.junit.Test
    public void test2() throws ExecutionException, InterruptedException {
        long startTime = System.currentTimeMillis();

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
        Long sum = submit.get();

        long endTime = System.currentTimeMillis();
        System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //ForkJoin方法(类似递归)=>3.6秒
    }

    @org.junit.Test
    public void test3() throws ExecutionException, InterruptedException {
        long startTime = System.currentTimeMillis();
        Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0,Long::sum);
        long endTime = System.currentTimeMillis();
        System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //Stream方法=>0.14秒
    }

    @org.junit.Test
    public void test4(){
        long startTime = System.currentTimeMillis();
        Long sum = (0L+ 10_0000_0000L) * (10_0000_0000L-0 + 1) /2;
        long endTime = System.currentTimeMillis();
        System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms");  //公式作弊方法=>0毫秒
    }
}
//在10_0000量级:
//朴素计算6ms; ForkJoin用时8ms Stream用时45ms =>和上面的情况完全反过来了
//所以不在大数据量的情况下,Stream不一定就很快

 

 

异步回调Async

类似于Ajax(C与S之间) ,不过这里是Java内部的异步调用。

public class Future {
    @Test
    public void FutureTest1() throws ExecutionException, InterruptedException {
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2);//异步线程耗时,就会返回到主线程继续执行,等到有结果了才会返回来sout
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "runAsync");
        });
        System.out.println("在主线程ing...");
        completableFuture.get();    //获取阻塞执行结果
    }
    
    @Test
    public void FutureTest2() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());
//            int x = 2/0; //导致异常的语句
            return 2199;
        });
        completableFuture.whenComplete((t,u)->{
            System.out.println("success.");  //正常返回结果
            System.out.println("t=>" + t);
            System.out.println("u=>" + u);
        }).exceptionally((e)->{
            System.out.println("exception.");  //错误返回结果
            System.out.println(e.getMessage());
            e.printStackTrace();
            return 2333;
        });
    }
}

 

volatile

1)保证可见性

2)不保证原子性

3)禁止指令重排

 

JMM(Java 内存模型)(引出volatile)

指令重排:为了提高性能编译器和处理器常常会对指令做重排序。

内存屏障:用于保证指令顺序执行。内存屏障分为 LoadLoad、StoreStroe、LoadStore、StoreLoad 四种。(用在volatile前后加)

 

一、保证可见性

  • 每次修改变量后,立刻回写到主内存。
  • 在此过程中,会通知其他线程读取新值。

(不然各个线程,存有一个值的  不同副本)

   

二、不保证原子性

原因:

例如你让一个volatile的integer自增(i++),其实要分成3步:

1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见。

这3步的jvm指令为:

 在内存屏障之前的几步都是不安全的 ==》所以不保证原子性

 

解决方法:Atomic类

// 2)volatile 不保证原子性,所以就使用Atomic和CAS来保证原子性
public class Test2 {
//    private volatile static int num =0;
    private static AtomicInteger num = new AtomicInteger(); //使用原子类的Integer

    public static void add(){
        //num++; //不是一个原子方法
        num.getAndIncrement();  //AtomicInteger的+1方法  //用的是底层CAS方法(见下方)  //比锁高效非常多倍
    }

    public static void main(String[] args) {
        for (int i =0;i< 20;i++){
            new Thread(()->{
                for(int j =0;j<100_0000;j++){
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){  // main + gc留下,其他都停掉;相当于finally打扫战场
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + num);
    }
}
//不保证原子性的后果:多线程修改num时,会导致最终计算结果经常不正确(例如下图的例子中,结果就不是正确结果:)

下图中,左边使用AtomicInteger,右边使用普通int(计算结果不正确)

  

 

Atomic机理CAS(CompareAndSet,比较并更新)

    => 如果期望的expect值达到了,就set;否则就不更新,并一直循环等(自旋锁)

CAS缺点:

1、循环会耗时

2、一次性只能保证一个共享变量的原子性

3、ABA问题

public class CAS {

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //如果期望的expect值达到了,就set,否则就不更新。CAS是CPU的并发原语
        atomicInteger.compareAndSet(2020,2021);
        System.out.println(atomicInteger.get());  // true 2021
        atomicInteger.compareAndSet(2020,2021);
        System.out.println(atomicInteger.get());  // false 2021
    }
}

 

unsafe类:

 

 

 

ABA问题:版本号解决

ABA问题就是:A->B->A,其他线程以为没有变化,但实际上是改变过了的

解决方法就是用时间戳来判断有没有被动过

 

 CAS比较并更新的对象是:

  值+stamp 2个内容

 

 

 三、禁止指令重排

在符合上下指令之间的依赖性的前提下,编译器+执行器,会进行重排。

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

 volatile写 的前后,加内存屏障,避免指令重排现象。

 

volatile两种功能:

1)可见性

  每个线程都有一块单独内存,存储的共享变量会有不同副本

2)禁止(编译器)指令重排、顺序优化

  由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。

  这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。

 

 

volatile 和 Synchronized 区别

作用级别:volatile 只用于变量;    变量、方法、类
可见性&原子性:volatile可见性,不保证原子性;synchronized 修改可见性、原子性
阻塞:volatile不阻塞;synchronized 阻塞。
编译器优化:volatile不会优化(内存屏障);synchronized可以优化

 

 

深入单例模式(synchronized + volatile)

单例模式:一个类只能构造一个实例对象("构造器私有")

场景:

  Windows任务管理器、回收站

  项目中,配置文件的类,一般只有一个对象

  Spring中的Bean(缓存中取bean很快,减少jvm垃圾回收)(当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象)

1)饿汉式单例

  ==》上来直接new对象,所有类实例化。坏处是:大量浪费不必要的资源(因为很多类   不需要实例化)

2)懒汉式单例 

   ==》按需创建;如果单例已经创建,会返回之前创建的对象。

    线程不安全:多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,

    因此需要加锁解决线程同步问题( Synchronized 同步锁来修饰 getInstance 方法)

 


 

三种线程安全的方法:

  

方法一:双重锁检测 DCL

  ==》首先将类加同步锁(syn),但是new语句不是原子操作,所以对了类的实例加volatile锁(可见性)

第一把锁:synchronized锁

第二把锁:volatile锁

 

  

方法二:静态内部类

public class Singleton {
    private static class SingletonHolder {
         /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

  

方法三:枚举

  ==》直接把类名前的class替换成enum就好了,因为枚举无法反射

 

 

比较: 

使用选择:

一般情况下直接使用饿汉式就好了,

如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类(比DCL写起来简单),

如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例。

posted @ 2021-04-30 22:07  青杨风2199  阅读(111)  评论(0编辑  收藏  举报