java并发编程(2/2)

11、线程池

线程池:三大方式、七大参数、四种拒绝策略

池化技术

程序的运行,本质:占用系统的资源!优化资源的使用==> 池化技术

线程池、JDBC 连接池、内存池、对象池等等。

资源的创建、销毁十分消耗资源

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

11.1 线程池的好处:

  • 降低资源的消耗;

  • 提高响应速度;

  • 方便管理;

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

阿里代码规范,不推荐Executors创建线程,推荐ThreadPoolExecutor

oom:out of memory 内存溢出

ThreadPoolExecutor执行线程

submit和execute

ThreadPoolExecutor执行任务有submit和execute两种方法,这两种方法区别在于

  • submit方法有返回值,便于异常处理

  • execute方法没有返回值

    @Test
    public void Demo02(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 0; i < 1; i++) {
                //execute方式开启线程
                threadPoolExecutor.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
                //submit方式开启线程
                Future<?> runnableSubmit = threadPoolExecutor.submit(new RunnableDemo());
                System.out.println(runnableSubmit.get());//null
				//获取实现Callable接口的线程的返回值
                Future<?> CallableSubmit = threadPoolExecutor.submit(new CallableDemo());
                System.out.println(CallableSubmit.get());//pool-1-thread-1CallableDemo线程执行

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}


class RunnableDemo implements Runnable{
    @Override
    public void run() {
        //pool-1-thread-1RunableDemo线程执行
        System.out.println(Thread.currentThread().getName()+"RunableDemo线程执行");
    }
}
class CallableDemo implements Callable<String>{
    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName()+"CallableDemo线程执行";
    }
}

11.2 线程池:三大方法

  • ExecutorService threadPool = Executors.newSingleThreadExecutor();//创建单个线程

  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小

  • ExecutorService threadPool3 = Executors.newCachedThreadPool(); //创建可伸缩的

public class Demo01 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//只能创建一个线程
        ExecutorService threadPool2 = Executors.newFixedThreadPool(5);//最多可以创建5个线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();//执行100次开启线程,可以创建70多个线程

//        线程池用完必须要关闭线程池
        try {
            for (int i = 0; i < 100; i++) {
    //            通过线程池创建线程
                threadPool3.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭线程池
            threadPool3.shutdown();
        }
    }
}

Executors是Executor框架的工具类,提供了几种线程池创建方法,以及线程池中默认配置(如线程工厂)的处理,接下来对其中常用的几种创建线程池的方式进行说明。

newSingleThreadExecutor创建单个线程

** **SingleThreadExecutor使用Executors.newSingleThreadExecutor()创建,查看源码

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

其中:

  • corePoolSize和maximumPoolSize都是1

  • LinkedBlockingQueue是一个最大值为Integer.MAX_VALUE的无界队列

  • 当线程正在执行任务,新任务会被加入到LinkedBlockingQueue队列中,任务加入队列的速度远大于核心线程处理的能力时,无界队列会一直增大到最大值,可能导致OOM

因此newSingleThreadExecutor可用于处理任务量不多,但又不想频繁的创建、销毁需要与虚拟机同周期的场景。

newFixedThreadPool创建一个固定的线程池的大小**

FixedThreadPool使用Executors.newFixedThreadPool() 创建,查看源码

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

其中:

  • corePoolSize和maximumPoolSize值均为nTreads(由入参确定)核心线程和最大线程一致

  • 存活时间为0L,超过核心线程数的空闲线程会被立即销毁

  • 队列依然为LinkedBlockingQueue,当线程数达到corePoolSize时,新任务会一直在无界队列中等待

  • 线程池中的线程数不会超过corePoolSize,新建任务也会一直被加入到队列等待,不会执行拒绝策略

  • ThreadPoolExecutor中的7个参数,maximumPoolSize,keepAliveTime,RejectedExecutionHandler为无效参数

FixedThreadPool同SingleThreadExecutor,如果nThreads的值设置过小,在任务量过多的场景下,会有可能由于线程数过少,导致任务一直堆积在队列中而引发OOM,相对好处是可以多线程处理,一定程度提高处理效率。

newCachedThreadPool

CachedThreadPool使用Executors.newCachedThreadPool()创建,查看源码

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

其中:

  • corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,即maximumPool是无界的。最大线程池是2的21次方

  • keepAliveTime为60L,空闲线程等待新任务的最长时间为60秒,超过60秒后将会被终止

  • SynchronousQueue为线程池的工作队列,没有容量,不能存放任务

CachedThreadPool中,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,会不断创建新线程,最终导致创建过多线程而耗尽CPU和内存资源。忽略CPU和内存消耗,某种程度上CachedThreadPool可以快速解决短时间并发问题,由于核心线程数为0,并且设置了存活时间,这些临时开辟的线程会在任务处理后被回收掉。
Executors是为了屏蔽线程池过多的参数设置影响易用性而提供的,而实际上这些参数恰恰也不需要进行逐个设置,Executors也是源码中推荐的一种使用方式,但是需要熟知他们各自的特点。

11.3 线程池:七大参数

指的是创建ThreadPoolExecutor线程池的七种参数

ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小
ExecutorService threadPool3 = Executors.newCachedThreadPool();//可伸缩的

//这三种创建线程的方法底层都是使用ThreadPoolExecutor来创建线程,只不过调用的参数不同
//Executors.newFixedThreadPool(5); 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

//ThreadPoolExecutor创建线程池源码
public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小
                          int maximumPoolSize, //最大的线程池大小
                          long keepAliveTime,  //超时了没有人调用就会释放
                          TimeUnit unit, //超时单位
                          BlockingQueue<Runnable> workQueue, //阻塞队列
                          ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
                          RejectedExecutionHandler handler //拒绝策略
                         ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

代码图解

注意:最大线程(maxmumPoolSize)一定要比核心线程(corePoolSize)要大或相等,不然创建线程池会报错。

@Test
    public void Demo02(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,//核心线程
                5,//最大线程
                3,//等待时间
                TimeUnit.SECONDS,//等待时间单位
                new LinkedBlockingDeque<>(4),//阻塞队列
                Executors.defaultThreadFactory(),//线程池工厂
                new ThreadPoolExecutor.AbortPolicy()//拒绝策略
        );
        try {
            for (int i = 0; i < 20; i++) {
//                Thread.sleep(500);
                threadPoolExecutor.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }

同时创建2个线程:线程数量小于核心线程2,只会使用核心线程

同时创建12个线程:线程创建速度大于核心线程销毁速度,调用3-5空闲线程

同时创建22个线程:线程创建速度大于cup处理速度并且阻塞队列填满后,调用拒绝策略

11.4 线程池:4大拒绝策略

  • new ThreadPoolExecutor.AbortPolicy: // 该 拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常(默认)

  • new ThreadPoolExecutor.CallerRunsPolicy(): // //该拒绝策略为:哪来的去哪里 main线程进行处理

  • new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了无法执行的任务被简单地丢弃,不做任何处理。

  • new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,是把队列最靠前的任务丢弃,重新尝试执行当前任务

11.5 如何设置线程池的大小

  1. CPU 密集型:

电脑的核数是几核就选几;选择maximunPoolSize 的大小(N+1)

// 获取cpu 的核数
int max = Runtime.getRuntime().availableProcessors();
ExecutorService service =new ThreadPoolExecutor(
    2,
    max,
    3,
    TimeUnit.SECONDS,
    new LinkedBlockingDeque<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);
  1. I/O密集型:

假如在程序中有15个大型任务,io十分占用资源;I/O 密集型就是判断我们程序中十分耗 I/O 的线程数量,大约是最大 I/O 数的一倍到两倍之间。(2N+1)

12、四大函数式接口(必须掌握)

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

函数式接口:只有一个方法的接口(典型Runnable接口)
//Runnable源码
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

//@FunctionalInterface函数式接口

四大原生的函数式接口

  1. Consumer有输入无返回 foreach

  1. Funtion有输入有返回

  1. Predicate无输入有返回

  1. Supplier有输入返回true/false

12.1Funtion函数式接口

  • 特点:传入一个obj,返回一个obj

  • 抽象方法:apply()

底层源码

//底层源码
package java.util.function;
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    ...
}

实际操作

@Test
public void demo02(){
    //匿名内部类,重写apply方法
    Function function = new Function() {
        @Override
        public Object apply(Object o) {
            return o;
        }
    };

    //lambda表达式1
    Function function2=(str)->{return str;};
    //lambda表达式2
    Function function3=str->{return str;};
    //lambda表达式3
    Function function4=str->str;
    
    System.out.println(function.apply("1"));//1
    System.out.println(function2.apply("2"));//2
    System.out.println(function3.apply("3"));//3
    System.out.println(function4.apply("4"));//4
}

12.2 Predicate 断言型接口

  • 特点:传入一个参数T,返回值只能是boolean

  • 一个任意类型的入参,根据我们写的逻辑,然后进行一个判断返回true或者false的抽象吗?

//底层源码
package java.util.function;
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

代码操作

@Test
public void demo03() {
    //匿名内部类,重写test方法
    Predicate predicate = new Predicate<Integer>() {
        @Override
        public boolean test(Integer num) {
            return num > 0;
        }
    };
    //lambda表达式1
    Predicate<Integer> predicate2=(num)->{return num>0;};
    //lambda表达式2
    Predicate<Integer> predicate3=num->{return num>0;};
    //lambda表达式3
    Predicate<Integer> predicate4=num->num>0;

    System.out.println(predicate.test(-1));//false
    System.out.println(predicate.test(0));//false
    System.out.println(predicate.test(1));//true
    System.out.println(predicate.test(2));//true
}

12.3 Supplier生产者类型接口

生产者的抽象逻辑就是:什么都不用入参,返给外界一个生产出来的对象

  • 特点:没有传入参数,只有返回值。可返回传入的任何类型

//底层源码
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

代码操作

//定义一个方法,用于获取int类型数组中元素的最大值,方法的参数传递Supplier接口,泛型使用Integer
public static int getMax(Supplier<Integer> sup){
    return sup.get();
}

@Test
public void demo04() {
    //匿名内部类,重写get方法
    Supplier supplier=new Supplier() {
        @Override
        public Object get() {
            return "supplier";
        }
    };

    Supplier supplier2=()->{return "supplier";};
    Supplier supplier3=()->Math.random()*10;
	Supplier supplier4=()->"supplier";
    Supplier<Exception> supplier5 = () -> new NullPointerException();
    Supplier<User> supplier6=()->new User();
    
    System.out.println(supplier.get());//supplier
    System.out.println(supplier2.get());//supplier
    System.out.println(supplier3.get());//6.863732353697851
    System.out.println(supplier4.get());//supplier
  	System.out.println(supplier5);//functionInterface$$Lambda$3/971848845@123772c4
	System.out.println(supplier6.get().getName());//获取User对象中的名字
    
    //定义一个int类型的数组并赋值
    int arr[] = {100,23,456,-23,-90,0,-678,14};
    //调用getMax方法,方法的参数Supplier是一个函数式接口,所以可以传递Lambda表达式
    int maxValue = getMax(() -> {
        //获取数组的最大值,并返回
        //定义一个变量,把数组中的的第一个元素赋值给该变量,记录数组中元素的最大值
        int max = arr[0];
        //遍历数组,获取数组中的其他元素
        for (int i : arr) {
            if (i > max) {
                max = i;
            }
        }
        //返回最大值
        return max;
    });
    System.out.println("数组中元素的最大值:"+maxValue);//数组中元素的最大值:456
}

12.4 Consummer 消费者类型接口

  • 为消费者类型的接口。

底层源码

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

代码实现

@Test
public void demo05() {
    Consumer consumer = new Consumer() {
        @Override
        public void accept(Object o) {
            System.out.println(o);
        }
    };
    Consumer consumer2 = (s)->{System.out.println(s);};
    Consumer consumer3 = s->{System.out.println(s);};
    Consumer<Integer> consumer4 = s->System.out.println(s+1);

    consumer.accept("1111");//1111
    consumer2.accept("2222");//2222
    consumer3.accept("3333");//3333
    consumer4.accept(4444);//4445

}

列如foreach底层也是使用Consummer函数式接口

  • lambda 表达式写法

public class functionInterface {
    @Test
    public void demo01(){
        List<Object> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.forEach(obj-> {
            System.out.print(obj);//123
        });
        list.forEach(obj-> System.out.print(obj));//123
        //jdk1.8新写法
        list.forEach(System.out::print);//123
    }
}

12.5、四大函数总结

JDK8之所以要提供这四大函数式接口,主要是对于程序中存在的四种逻辑的抽象:

  • Predicate判断逻辑【比如if else场景】

  • Function映射逻辑【比如我们写的各个方法】

  • Consumer消费型逻辑【比如System.out.println这种对于元素的空返回值消费操作】

  • Suppiler生产型逻辑【比如new对象】

函数式接口

抽象方法

对应程序逻辑的抽象

具体场景

Function<T, R>

R apply(T t);

程序中映射逻辑的抽象

比如我们写得很多的函数:接收入参,返回出参,方法代码块就是一个映射的具体逻辑。

Predicate

boolean test(T t);

程序中判断逻辑的抽象

比如各种if判断,对于一个参数进行各种具体逻辑的判定,最后返回一个if else能使用的布尔值

Consumer

void accept(T t);

程序中的消费型逻辑的抽象

就比如Collection体系的ForEach方法,将每一个元素取出,交给Consumer指定的消费逻辑进行消费

Supplier

T get();

程序中的生产逻辑的抽象

就比如最常用的,new对象,这就是一个很经典的生产者逻辑,至于new什么,怎么new,这就是Suppiler中具体逻辑的写法了

12.6、函数式接口的扩展

甚至通过名字都能看出来,像是LongSuppiler,DoubleConsumer,这不就是泛型参数被具体化

1.BiXXX

其实就是Binary的缩写,Bi翻译过来就是【二元】的意思。

不外乎就是两个泛型入参,但是消费型逻辑是完全没变化的。

//原型
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
//Bi拓展
@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
 }

代码对比

@Test
public void demo06() {
    //传入一个obj 返回一个obj
    Function function=str->1;
    System.out.println(function.apply("hello"));//1

    //传入两个obj 返回一个obj
    BiFunction biFunction=(a,b)->{
        StringBuilder builder = new StringBuilder();
        builder.append(a);
        builder.append(b);
        return builder.toString();
    };
    BiFunction biFunction2=(str1,str2)->str1.equals(str2)?true:false;

    System.out.println(biFunction.apply("he", "llo"));//hello
    System.out.println(biFunction2.apply("1", "2"));//false
}
2.xxxFunction

有返回有输入

//IntFunction源码
@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}

//intFunction测试
IntFunction intFunction=num-> ++num;
//apply()只能传入int类型
System.out.println(intFunction.apply(1));//2

//IntToDoubleFunction源码
@FunctionalInterface
public interface IntToDoubleFunction {
    double applyAsDouble(int value);
}
//IntToDoubleFunction测试
 IntToDoubleFunction intToDoubleFunction=num -> num+1;
//apply()只能传入int类型,只能返回double类型
System.out.println(intToDoubleFunction.applyAsDouble(1));//2.0
3.xxxConsumer

有输入无返回

//IntConsumer源码
@FunctionalInterface
public interface IntConsumer {
      void accept(int value);
}

//IntConsumer测试
 IntConsumer intConsumer=str-> System.out.println(++str);//++str输出4 str++=>输出3
//只能输入int类型
intConsumer.accept(3);
4.xxxPredicate

有输入返回boolean

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}

IntPredicate intPredicate=str->str>0;
//只能输入int类型
System.out.println(intPredicate.test(-2));//false
5.xxxSupplier

无输入有返回

@FunctionalInterface
public interface IntSupplier {
    int getAsInt();
}

IntSupplier intSupplier =()->new Random().nextInt(10);
//返回值必须为int
System.out.println(intSupplier.getAsInt());//6
6.XXXOperator

都是在泛型参数统一化上面做的扩展,出参和入参必须一致

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}

JDK新特性之:接口默认方法

接口类中使用default关键字修饰抽象方法,则会自动有一个空实现。实现该接口类时,使用default修饰的方法可以不用重写,也不会报错

@Test
    public void demo07(){
        //方式一:使用匿名内部类,这里没法用Lambda,因为接口有多个抽象。
        A a=new A() {
            @Override
            public void show1() {
                System.out.println("调用show1方法");
            }

            @Override
            public void show2() {
                System.out.println("调用show2方法");
            }

            @Override
            public void show3() {
                System.out.println("调用show3方法");
            }

            //必须要重写全部抽象方法不然会报错,很麻烦。
            @Override
            public void show4() {

            }
        };
        a.show1();//调用show1方法

        //方式二:创建适配器类的匿名内部类,并且重写该类的show1即可
        MyAdapter myAdapter = new MyAdapter(){
            @Override
            public void show1() {
                System.out.println("调用show1方法");
            }
        };
        myAdapter.show1();//调用show1方法

        //方式三:在接口类的方法声明最前面加一个关键字:default 来让Java编译器知道这是一个接口的默认方法
        B b=()-> System.out.println("调用show1方法");
        //留一个抽象方法show1。这样,外部还可以用Lambda表达式来简化只需要实现show1的匿名内部类
        b.show1();//调用show1方法
        //加上default关键字之后,该方法已经有了默认实现了,所以lambda表达式没有生效
        b.show2();
    }
}

interface A{
    void show1();
    void show2();
    void show3();
    void show4();
}
interface B{
     void show1();
    default void show2(){};
    default void show3(){};
    default void show4(){};
}

//给这个接口创建一个适配器类【AdapterClass】里面写好每一个方法的空实现。
//我们以后再去创建A接口对象的时候,我们就可以重写我们指定的那个方法了
class MyAdapter implements A{

    @Override
    public void show1() {

    }

    @Override
    public void show2() {

    }

    @Override
    public void show3() {

    }

    @Override
    public void show4() {

    }
}

13、Stream流式计算

什么是Stream流计算

简介:

  • Stream用于进行计算,是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列

  • Stream并不会存储数据

  • Stream并不会改变数据源,它会返回一个含有结果集的Stream

  • Stream是延迟加载的,在需要结果时才执行。

代码解析

public class StreamDemo {
    /**
     * 题目要求:一分钟内完成此题,只能用一行代码实现!现在有个用户!
     * 筛选:1、ID必须是偶数
     * 2、年龄必须大于21岁
     * 3、用户名转为大写字母
     * 4、用户名字母倒着排序
     * 5、只输出一个用户!
     */
    @Test
    public void demo01(){
        User user1 = new User(1, "a", 21);
        User user2 = new User(2, "b", 22);
        User user3 = new User(3, "c", 23);
        User user4 = new User(4, "d", 24);
        User user5 = new User(5, "e", 25);

        List<User> list = Arrays.asList(user1, user2, user3, user4, user5);
//        lambda、链式编程、函数式接口、流式计算
        list.stream()
                //ID必须是偶数
                .filter(user -> {return user.getId()%2 == 0;})//User(id=2, name=b, age=22) User(id=4, name=d, age=24)
                //年龄必须大于21岁
                .filter(user -> {return user.getAge()>21;})//User(id=2, name=b, age=22) User(id=4, name=d, age=24)
                //用户名转为大写字母
                .map(user -> {user.setName(user.getName().toUpperCase());return user;})//User(id=2, name=B, age=22) User(id=4, name=D, age=24)
                //用户名字母倒着排序
                .sorted((uu1,uu2)->{return uu2.getName().compareTo(uu1.getName());})//User(id=4, name=D, age=24) User(id=2, name=B, age=22)
                //只输出一个用户!
                .limit(1)//User(id=4, name=D, age=24)
                .forEach(System.out::println);
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
    private int id;
    private String name;
    private int age;
}

14、ForkJoin

什么是ForkJoin

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想->把大任务拆分为小任务!

14.1、ForkJoin特点:工作窃取

实现原理:双端队列!从上面和下面都可以去拿到任务进行执行!

14.2、如何使用ForkJoin?

  • 通过 ForkJoinPool 来执行

  • 计算任务 execute(ForkJoinTask<?> task)

  • 计算类要去继承 ForkJoinTask;

ForkJoin 的计算类

public class ForkJoinTest{
    private static final long SUM = 20_0000_0000;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1();//时间:912
        test3();//时间:835
    }

    /*
     * 使用普通方法
     *
     * */

    public static void test1(){
        long star = System.currentTimeMillis();
        long sum = 0L;
        for (int i = 0; i < SUM; i++) {
            sum+=i;
        }
        long end = System.currentTimeMillis();
        System.out.println(sum);
        System.out.println("时间:" + (end-star));
        System.out.println("---------------");
    }


    /*
     * 使用流计算
     * */
    public static void test3(){
        long star = System.currentTimeMillis();
        long sum = LongStream.range(0L,20_0000_0000L).parallel().reduce(0,Long::sum);
        System.out.println(sum);
        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end - star));
        System.out.println("-------------");
    }
}

.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。

15、 异步回调

多线程情况下,我们不知道线程什么时候执行完毕,或者不知道如何处理子线程的结果?

Future 设计的初衷:对将来的某个事件结果进行建模!

15.1、CompletableFuture

资料:https://blog.csdn.net/u012060033/article/details/124469978

CompletableFuture在创建时,如果传入线程池,那么会去指定的线程池工作。如果没传入,那么回去默认的ForkJoinPool

ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个小任务,把多个小任务放到多个处理器核心上并行执行;当多个小任务执行完成之后,再将这些执行结果合并起来即可。

15.2、CompletableFuture创建方式

  1. 构造函数创建

最简单的方式就是通过构造函数创建一个CompletableFuture实例。如下代码所示。由于新创建的CompletableFuture还没有任何计算结果,这时调用join,当前线程会一直阻塞在这里。

CompletableFuture<String> future = new CompletableFuture();
String result = future.join();
System.out.println(result);

此时,如果在另外一个线程中,主动设置该CompletableFuture的值,则上面线程中的结果就能返回。

future.complete("test");
  1. supplyAsync创建

CompletableFuture.supplyAsync()也可以用来创建CompletableFuture实例。通过该函数创建的CompletableFuture实例会异步执行当前传入的计算任务。在调用端,则可以通过get或join获取最终计算结果

CompletableFuture<String> future 
	= CompletableFuture.supplyAsync(()->{
      System.out.println("compute test");
      return "test";
});
 
String result = future.join();
System.out.println("get result: " + result);

异步任务中会打印出compute test,并返回test作为最终计算结果。所以,最终的打印信息为**get result: test**

  1. runAsync创建

CompletableFuture.runAsync()也可以用来创建CompletableFuture实例。与supplyAsync()不同的是,runAsync()传入的任务要求是**Runnable类型的,所以没有返回值**。因此,runAsync适合创建不需要返回值的计算任务。

CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
            System.out.println("compute test");
        });

System.out.println("get result: " + future.join());

//由于任务没有返回值, 所以最后的打印结果是"get result: null"。

15.3 、异步回调方法

同Future相比,CompletableFuture最大的不同是支持流式(Stream)的计算处理,多个任务之间,可以前后相连,从而形成一个计算流。比如:任务1产生的结果,可以直接作为任务2的入参,参与任务2的计算,以此类推。

CompletableFuture中常用的流式连接函数包括:

  • thenApply——有入参有返回

thenApplyAsync——有入参有返回

  • thenAccept——有入参无返回

thenAcceptAsync——有入参无返回

  • thenRun——无入参无返回

thenRunAsync——无入参无返回

  • thenCombine

thenCombineAsync

  • thenCompose

thenComposeAsync

  • whenComplete

whenCompleteAsync

  • handle

handleAsync

其中,带Async后缀的函数表示需要连接的后置任务会被单独提交到线程池中,从而相对前置任务来说是异步运行的。除此之外,两者没有其他区别。

public class CompletableFutureDemo {

    //没有返回值的runAsync异步回调
    @Test
    @SneakyThrows
    public void dome01(){
        //        发起一个请求
        System.out.println(System.currentTimeMillis());
        System.out.println("-----------");

        //CompletableFuture.runAsync 没有返回值 不能使用 return
        CompletableFuture future = CompletableFuture.runAsync(()->{
//            发起一个异步任务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"......");

        }).thenAccept((oil)->{
            System.out.println("获取值");
        });

        CompletableFuture<Void> voidCompletableFuture = CompletableFuture
                //让儿子去打酱油
                .supplyAsync(() -> {
                    try {
                        System.out.println("儿子跑步去打酱油");
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println("酱油打好了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "酱油";
                })
                //thenAccept 有入参有返回
                .thenAccept(oil -> {
                    System.out.println("做菜用酱油:" + oil);
                });

        System.out.println(System.currentTimeMillis());
        System.out.println("-------------------------");
        System.out.println(future.get());//获取执行结果
    }

    //有返回值的异步回调supplyAsync
    //调用成功返回
    @Test
    @SneakyThrows
    public void dome02(){
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1024;
        });

        System.out.println(
            completableFuture
                    //有入参有返回
                    .thenApply((me)->{
                        System.out.println(me);//1024
                        return 1080; //get()到的是这个值
                    })
                    //有入参无返回
                    .thenAccept((num)->{
                        System.out.println(num);//1024
                    })
                    //无入参无返回
                    .thenRun(()->{
                        System.out.println("thenRun");
                    })
                    //任务完成时的回调通知逻辑
                    .whenComplete((t,u)->{
                         //t:获取正常返回结果 没有返回则为null
                        //u:捕获异常返回信息
                        System.out.println("t=>" +t);//t=>1024
                        System.out.println("u=>" +u);//u=>null
                    })
                    .exceptionally((e)->{
                        // error 回调
                        System.out.println(e.getMessage());
                        return 404;
                    })
                    .get()//1024
        );
    }
    //调用失败返回
    @Test
    @SneakyThrows
    public void dome03(){
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());//ForkJoinPool.commonPool-worker-1
            try {
                TimeUnit.SECONDS.sleep(2);
                //设置算术异常代码
                int i=10/0;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1024;
        });
        System.out.println(
                completableFuture.whenComplete((t,u)->{
                    // success 回调
                    // t:正常返回结果
                    System.out.println("t=>" +t);//t=>null
                    // u:异常赶回 错误信息
                    System.out.println("u=>" +u);//u=>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
                }).exceptionally((e)->{
                    //捕获error ,没有这个捕获异常 会把异常抛出到控制台程序结束
                    System.out.println(e.getMessage());//java.lang.ArithmeticException: / by zero
                    return 404;
                }).get()//404 --程序异常get到的是异常返回的404
        );
    }
}

15.4、thenApply / thenAccept / thenRun互相依赖

这里将thenApply / thenAccept / thenRun放在一起讲,因为这几个连接函数之间的唯一区别是提交的任务类型不一样 :

  • thenApply提交的任务类型需遵从Function签名,也就是有入参和返回值,其中入参为前置任务的结果

  • thenAccept提交的任务类型需遵从Consumer签名,也就是有入参但是没有返回值,其中入参为前置任务的结果

  • thenRun提交的任务类型需遵从Runnable签名,即没有入参也没有返回值

CompletableFuture<Integer> future1 
	= CompletableFuture.supplyAsync(()->{
     	System.out.println("compute 1");
     	return 1;
 });
 CompletableFuture<Integer> future2 
 	= future1.thenApply((p)->{
	     System.out.println("compute 2");
	     return p+10;
 });
 System.out.println("result: " + future2.join());

15.5、thenApply

thenApply 表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中,测试用例如下:

@Test
public void test5() throws Exception {
    //CompletableFuture 默认线程 ForkJoinPool
    ForkJoinPool pool=new ForkJoinPool();
    // 创建异步执行任务:
    CompletableFuture<Double> cf = CompletableFuture.supplyAsync(()->{
        System.out.println(Thread.currentThread().getName()+" 线程1执行");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName()+" 线程1结束");
        return 1.2;
    },pool);
    //cf关联的异步任务的返回值作为方法入参,传入到thenApply的方法中
    //thenApply这里实际创建了一个新的CompletableFuture实例
    CompletableFuture<String> cf2=cf.thenApply((result)->{
        System.out.println(Thread.currentThread().getName()+" 线程2执行");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName()+" 线程2结束");
        return "test:"+result;
    });
    System.out.println("主线程开始 获取线程1 开始 时间"+System.currentTimeMillis());
    //等待子任务执行完成
    System.out.println("线程1返回结果"+cf.get());
    System.out.println("主线程开始 获取线程1 结束 时间"+System.currentTimeMillis());
    System.out.println("线程2返回结果"+cf2.get());
    System.out.println("主线程+ForkJoinPool线程 结束 时间"+System.currentTimeMillis());

    /**cf.thenApply((result) 执行结果
     *
     * ForkJoinPool.commonPool-worker-1 线程1执行  说明是异步进行
     *主线程开始 获取线程1 开始 时间1678437309369
     *ForkJoinPool.commonPool-worker-1 线程1结束
     *ForkJoinPool.commonPool-worker-1 线程2执行
     *线程1返回结果1.2
     *主线程开始 获取线程1 结束 时间1678437311384
     *ForkJoinPool.commonPool-worker-1 线程2结束
     *线程2返回结果test:1.2
     *主线程+ForkJoinPool线程 结束 时间1678437313391
     ForkJoinPool.commonPool-worker-1说明两个CompletableFuture是同一个线程执行
     */
ob1执行结束后,将job1的方法返回值作为入参传递到job2中并立即执行job2。thenApplyAsync与thenApply的区别在于,前者是将job2提交到线程池中异步执行,实际执行job2的线程可能是另外一个线程,后者是由执行job1的线程立即执行job2,即两个job都是同一个线程执行的。将上述测试用例中thenApply改成thenApplyAsync后,执行结果如下:

    /**cf.thenApplyAsync((result) 执行结果
     *
     *ForkJoinPool-1-worker-1 线程1执行
     * 主线程开始 获取线程1 开始 时间1678437606176
     * ForkJoinPool-1-worker-1 线程1结束
     * ForkJoinPool.commonPool-worker-1 线程2执行
     * 线程1返回结果1.2
     * 主线程开始 获取线程1 结束 时间1678437608187
     * ForkJoinPool.commonPool-worker-1 线程2结束
     * 线程2返回结果test:1.2
     * 主线程+ForkJoinPool线程 结束 时间1678437610196
     *ForkJoinPool-1-worker-1 与 ForkJoinPool.commonPool-worker-1不同 说明是不同的线程执行 异步执行
     */

 结论:
      每个方法都有两个以Async结尾的方法,一个使用默认的Executor实现,一个使用指定的Executor实现,
     不带Async的方法是由触发该任务的线程执行该任务,
     带Async的方法是由触发该任务的线程将任务提交到线程池,执行任务的线程跟触发任务的线程不一定是同一个

}

16、JMM

资料:https://blog.csdn.net/m0_46845579/article/details/125944917

JMM即为JAVA 内存模型(java memory model)。不存在的东西, 是概念,是约定。因为在不同的硬件生产商和不同的操作系统下, 内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了 屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。即达到Java程序能够“ 一次编写,到处运行”。

16.1、JMM内存模型

JMM规定了所有的变量都存储在 主内存(Main Memory)中。每个线程还有自己的 工作内存(Working Memory), 线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。 不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存

问题:当一个线程修改了自己工作内存中变量,对其他线程是不可见的,**会导致线程不安全的问题。**因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。

16.2、JMM的三大特性

JMM的三大特性:原子性、可见性、有序性。

1.原子性

一个或多个操作, 要么全部执行,要么全部不执行(执行的过程中是不会被任何因素打断的)。

为什么会有原子性问题

  • 因为CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。

a = true;  //原子性
a = 5;     //原子性
a = b;     //非原子性,分两步完成,第一步加载b的值,第二步将b赋值给a
a = b + 2; //非原子性,分三步完成
a ++;      //非原子性,分三步完成:1、读取a的值,2、计算a的值+1,3、赋值

如何保证原子性

  1. synchronized一定能保证原子性,因为被其修饰的某段代码,只能由一个线程执行,所以一定可以保证原子操作。

  1. juc(java.util.concurrent包)中的lock包和atomic包(原子类),他们也可以解决原子性问题.

2.可见性

只要有一个线程对共享变量的值做了 修改,其他线程都将 马上收到通知,立即获得最新值

为什么会有可见性问题

  • 根据JMM内存模型,可以看到主内存和线程工作内存之间存在时间差(延迟)问题。这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题

如何保证可见性

  1. volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。

  1. 除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。

synchronized,Lock:通过线程私有的工作内存中的值写入到主内存进行同步

final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值

3.有序性

在本线程内观察,所有的操作都是有序的;代码按顺序执行
  • 有序性可以总结为:**在本线程内观察,所有的操作都是有序的;而在一个线程内观察另一个线程,所有操作都是无序的。**前半句指 as-if-serial 语义:线程内似表现为串行,后半句是指:“指令重排序现象”和“工作内存与主内存同步延迟现象”。

  • as-if-serial语义:

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义)

为什么会有有序性问题

  • 处理器为了提高程序的运行效率,提高并行效率,可能会对代码进行优化。编译器认为,重排序后的代码执行效率更优。这样一来,代码的执行顺序就未必是编写代码时候的顺序了,在多线程的情况下就可能会出错。

在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(在单线程情况下)。

有序性问题 指的是在多线程的环境下,由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致计算结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。

如何保证有序性

  • Java 语言提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“**一个变量在同一个时刻只允许一条线程对其进行 lock 操作”**这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行进入。

16.3、JMM同步的规定

1.线程解锁前,必须把共享变量的值刷新回主内存。

2.线程加锁前,必须将主内存的最新值读取到自己的工作内存。

3.加锁解锁是同一把锁。

16.4、8种内存交互操作

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

  1. lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态

  1. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来释放后的变量才可以被**其他线程锁定

  1. read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用**

  1. load(载入):作用于工作内存的变量,它把read操作从主存中变量**放入工作内存中

  1. use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令**

  1. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放**入工作内存的变量副本中

  1. store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用**

  1. 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操作之前,必须把此变量同步回主内存

分析:
  1. 首先,主内存中,initFlag=false。

  1. 线程1经过 read、load,工作内存处,initFlag = false,use 之后, 执行引擎处 !initFlag = true,此时,卡在while处。

  1. 线程2同理,经过 read、load 之后,此时工作内存处,initFlag=false,经过 use (initFlag=true)、assign后,initFlag=true。

  1. 因为线程2处的cpu修改了 initFlag 的值,会 马上回写 到主内存中(经过 store、write两步)。

  1. 线程1处的cpu 通过 总线嗅探机制 嗅探到变化,会将工作内存中的数据 失效(initFlag=false失效)

  1. 线程1会 重新 去主内存 read 最新的数据(此时,主内存中的数据 initFlag=true)。

  1. 那么,线程1在读取最新的数据时,执行引擎处,!initFlag = false,结束循环,输出 “success”。

加上volatile关键字保证可见性

17、volatile

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存

3)如果是写操作,它会导致其他CPU中对应的缓存行无效

17.1、保证有序性

(禁止指令重排)

什么是指令重排?

  • 我们写的程序,计算机并不是按照我们自己写的那样去执行的

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

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

int x=1; //1
int y=2; //2
x=x+5;   //3
y=x*x;   //4

//我们期望的执行顺序是 1_2_3_4  可能通过编译之后的执行的顺序会变成2134 1324。但是不会导致程序逻辑错误。
//可不可能是 4123? 不可能的

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序不会重排

内存屏障:CPU指令。作用:

1、保证特定的操作的执行顺序

2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

volatile的内存屏障策略:

  • volatile写之前插入StoreStore屏障;(规则a,防止重排)

  • volatile写之后插入StoreLoad屏障;(规则c,保障可见性)

  • volatile读之后插入LoadStore屏障;(规则b,防止重排)

  • volatile读之后插入LoadLoad屏障;(规则b,防止重排)

LoadLoad Barriers

排队,当第一个读屏障指令读取数据完毕之后,后一个读屏障指令才能够进行加载读取

(禁止读和读的重排序)

StoreStore Barriers

当A写屏障指令写完之后且保证A的的写入可以被其他处理器看见,再进行B的写入操作

(禁止写与写的重排序)

LoadStore Barriers

前一个读屏障指令读取完毕后,后一个写屏障指令才会进行写入

(禁止读和写的重排序)

StoreLoad Barriers

全能屏障,同时具有前三个的类型的效果,但开销较大。

先保证A的写入会被其他处理器可见,才进行B的读取指令

(禁止写和读的重排序)

volatile保证有序性原理:在每一个写的volatile前后插入写屏障,读的volatile前后插入读屏障

  • 写,在每一次写入之前屏障拿到其他的线程修改的数据(因为可见性和重排序)。写入后的屏障可以被其他线程拿到最新的值

  • 读,在每一个读之前屏障获取某个变量的值的时候,这个值可以被其他线程也获取到。读取后的屏障可以在其他线程修改之前获取到主内存变量的当前值

volatile写 内存屏障原理

volatile读 内存屏障原理

多线程它是怎么保证数据的原子性的哪?

举个例子:

假设线程A和线程B两个线程同时执行getAndAddlInt操作(分别跑在不同CPU上) :

(一). AtomicInteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

(二).线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。

(三)线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。

(四).这时线程A恢复,执行compareAndSwaplnt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。

(五).线程A重新获取value值,因为变量value被volatle修饰所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功

17.2、保证可见性

volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存

  1. 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效

volatile修饰的共享变量在执行写操作后,会立即刷回到主存,以供其它线程读取到最新的记录。

17.3、不保证原子性

volatile只有写操作是原子性的,也就是数据操作完成后会立刻刷新到主内存中。但是被volatile修饰的变量在读的时候可能会被多个线程读。也就是说int i = 1;i++;

A线程读 i = 1同时B线程也读了i = 1,然后自增完成刷新入主内存。i的值是2。

所以如果该变量是volatile修饰的,那可以完全保证此时取到的是最新信息。但在入栈和自增计算执行过程中,该变量有可能正在被其他线程修改,最后计算出来的结果照样存在问题,因此volatile并不能保证非原子操作的原子性,仅在单次读或者单次写这样的原子操作中,volatile能够实现线程安全。

public class JMMDemo {
    private static volatile int i=0;
    public static void add(){
        i++;
    }
    @SneakyThrows
    public static void main(String[] args) {
        //创建20个线程,每个线程执行100次i++操作  理论结果输出i=2000
        for (int j = 0; j < 20; j++) {
            new Thread(()->{
                for (int k = 0; k < 100; k++) {
                    add();
                }
            }).start();
        }

        //如果活动线程大于2 (默认有 main gc) 让当前正在执行的线程暂停,但不阻塞
        //将线程从运行状态转为就绪状态
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+",i = "+i);//main,i = 1970
    }
}
  • i++;底层代码不是一个原子操作,是非原子性的

  • 加的次数是固定的,有的线程取的是旧的值,结果肯定小了

  • volatile只保证了变量在内存中的可见性但无法保证对变量的操作为原子性,在不使用除锁机制的条件下可以使用atomic包下的原子类

使用原子类AutomicInteger

public class JMMDemo {
    //AtomicInteger 原子类能保证操作数据原子性  unsafe类涉及到CAS
    private static volatile AtomicInteger i=new AtomicInteger(0);
    public static void add(){
        i.incrementAndGet();//底层是 CAS 保证原子性
    }
    @SneakyThrows
    public static void main(String[] args) {
        //创建20个线程,每个线程执行100次i++操作  理论结果输出i=2000
        for (int j = 0; j < 20; j++) {
            new Thread(()->{
                for (int k = 0; k < 100; k++) {
                    add();
                }
            }).start();
        }

        //如果活动线程大于2 (默认有 main gc) 让当前正在执行的线程暂停,但不阻塞
        //将线程从运行状态转为就绪状态
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+",i = "+i);//main,i = 2000
    }
}

那么你知道在哪里用这个volatile内存屏障用得最多呢?单例模式

18、 单例模式

任何时候都只能有一个实例。且该类需自行创建这个实例,并对其他的类提供调用这一实例的方法。

  • 单例类只能有一个实例。

  • 单例类必须自己创建自己的唯一实例。

  • 单例类必须给所有其他对象提供这一实例。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

18.1 饿汉式

饿汉式,就是“比较饿”,实例在初始化的时候就已经自行实例化,不管你有没有用到。

优点:线程安全;在类加载(ClassLoader)的同时已经创建好一个静态对象,调用时反应速度快。

缺点:对象提前创建,所以会占据一定的内存,内存占用大 以空间换时间。

//饿汉式单例 在类初始化时,已经自行实例化
public class SingleHungryDemo {
    /*
    * 可能会浪费空间
    * */
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
    //SingleHungryDemo通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,SingleHungryDemo的唯一实例只能通过getInstance()方法访问。
    private SingleHungryDemo(){
        
    }
     /**
     * 私有实例,静态变量会在类加载的时候初始化,是线程安全的
     */
    private final static SingleHungryDemo hungry = new SingleHungryDemo();
    /**
     * 唯一公开获取实例的方法(静态工厂方法)
     *
     * @return
     */
    public static SingleHungryDemo getInstance(){
        return hungry;
    }
}

多线程安全。

18.2 DCL 懒汉式

懒汉式就是“比较懒”,就是在用到的时候才去检查有没有实例,如果有则直接返回,没有则新建。

优点:起到了Lazy Loading的效果,但是只能在单线程下使用。

缺点:如果在多线程下,两个线程同时进入了if (singleton == null)判断语句块,这时便会产生多个实例。所以

在多线程环境下不可使用这种方式。

//懒汉式单例类.只在调用的时候判断有没这个实例。有则返回已经创建好的,没有就新建一个
public class Lazy {
    private Lazy(){}

    private static Lazy lazy=null;
    /**
     * 唯一公开获取实例的方法(静态工厂方法),
     */
    public static Lazy getInstance(){
        if (lazy==null){
            return new Lazy();
        }
        return lazy;
    }
}

线程不安全

要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,

1.在getInstance方法上加同步

/**
 * 唯一公开获取实例的方法(静态工厂方法),
 */
public static synchronized Lazy getInstance(){
    if (lazy==null){
        return new Lazy();
    }
    return lazy;
}

2、双重检查锁定

/**
 * 双重检测锁模式
 */
public static synchronized Lazy getInstance(){
    //第一个 if 语句用来避免 lazy 已经被实例化之后的加锁操作
    if (lazy==null){
        // 加锁 对这个实例加锁防止多线程多次创建
        synchronized (Lazy.class){
            //第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 lazy == null
            // 时两个线程同时进行实例化操作
            //判断有没有创建这个实例
            if (lazy==null) {
                return new Lazy();
            }
        }
    }
    return lazy;
}

18.3、静态内部类

该模式利用了静态内部类延迟初始化的特性,来达到与双重校验锁方式一样的功能。

缺点: 可以被反射破坏

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton(){}


    /**
     * 唯一公开获取实例的方法(静态工厂方法)
     *
     * @return
     */
    public static StaticInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    /**
     * 私有静态内部类
     */
    private static class LazyHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
}

反射破坏静态内部类单例模式附上代码:

public static void main(String[] args) {

        try {
            Constructor<StaticInnerClassSingleton> con = StaticInnerClassSingleton.class.getDeclaredConstructor();
            //值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查
            //方便获取私有属性
            con.setAccessible(true);
            StaticInnerClassSingleton singleton = (StaticInnerClassSingleton) con.newInstance();
            System.out.println(singleton);

            StaticInnerClassSingleton singleton5 = (StaticInnerClassSingleton) con.newInstance();
            System.out.println(singleton5);
//singleton singleton5 不是同一个对象。破坏了单例模式
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

18.4、枚举

优点:枚举式单例不会被反射破坏 , 内存中只存在一个对象实例,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰。

public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

通过IDEA和 javap -p EnumSingle.class 里面有一个无参的构造方法

结果通过软件反编译 枚举类型的最终反编译源码:里面有个一带参的构造方法

public final class EnumSingle extends Enum
{

  。。。。

    private EnumSingle(String s, int i)
    {
        super(s, i);
    }

    ....
}

18.5 饿汉式与懒汉式的区别

  • 饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

  • 懒汉比较懒,只有当调用getInstance的时候,才会去初始化这个单例。

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是

  • 在getInstance方法上加同步

  • 双重检查锁定

  • 静态内部类

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了.

结论:由结果可以得知单例模式为一个面向对象的应用程序提供了 对象惟一的访问点,不管它实现何种功能, 整个应用程序都会同享一个实例对象

19、深入理解CAS

19.1 什么是CAS?

CAS,compare and swap的缩写,中文翻译成比较并交换它是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。

CAS 操作包含三个操作数 —— 内存位置(V)、 预期原值(A)和**新值(**B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

CAS通俗的解释就是:

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止.

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    //    CAS: compareAndSet 比较并交换
    public static void main(String[] args){
        AtomicInteger atomicInteger = new AtomicInteger(2020);

//        boolean compareAndSet(int expect, int update)
//        期望值、更新值
//        如果实际值 和 我期望值相同,那么就更新
//        如果实际值 和 我期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());

//        因为期望值是 2020 ,实际值却变成了2021 所以会修改失败
//        CAS 是 CPU 的并发原语
        atomicInteger.getAndIncrement();//++操作
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
    }
}

19.2、原子类

JUC 并发包中原子类 , 都存放在 java.util.concurrent.atomic 类路径下:

根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:

  • 基本原子类

  • 数组原子类

  • 原子引用类型

  • 字段更新原子类

19.3、原子类AtomicInteger

1、常用的方法:

AtomicInteger 案例:

 private static void out(int oldValue,int newValue){
        System.out.println("旧值:"+oldValue+",新值:"+newValue);
    }
    public static void main(String[] args) {
        int value = 0;
        AtomicInteger atomicInteger= new AtomicInteger(0);
        //取值,然后设置一个新值
        value = atomicInteger.getAndSet(3);
        //旧值:0,新值:3
        out(value,atomicInteger.get());
        //取值,然后自增
        value = atomicInteger.getAndIncrement();
        //旧值:3,新值:4
        out(value,atomicInteger.get());
        //取值,然后增加 5
        value = atomicInteger.getAndAdd(5);
        //旧值:4,新值:9
        out(value,atomicInteger.get());
        //CAS 交换
        boolean flag = atomicInteger.compareAndSet(9, 100);
        //旧值:4,新值:100
        out(value,atomicInteger.get());
    }

AtomicInteger 源码解析:

public class AtomicInteger extends Number implements java.io.Serializable {
    // 设置使用Unsafe.compareAndSwapInt进行更新
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
 
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }
    ...省略
    private volatile int value;
 
    //自动设置为给定值并返回旧值。
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    
    //=========================getAndSet底层开始====================
    var1 Atomiclnteger对象本身。
	var2该对象值得引用地址。
	var4需要变动的数量。
	var5是用过var1 var2找出的主内存中真实的值。
	用该对象当前的值与var5比较:
	如果相同,更新var5+var4并且返回true,
	如果不同,继续取值然后再比较,直到更新完成。

 public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
// =================================getAndSet底层结束=====================
    //以原子方式将当前值加1并返回旧值。
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
 
    //以原子方式将当前值减1并返回旧值。
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
 
    //原子地将给定值添加到当前值并返回旧值。
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    ...省略
}

public final int getAndIncrement()源码解析

  • 这里的原子类方法用了do while,无限循环,其实就是一个标准的自旋锁。

通过源码我们发现AtomicInteger的增减操作都调用了Unsafe实例的方法,下面我们对Unsafe类做

19.4、Unsafe类

Unsafe 是位于 sun.misc 包下的一个类,Unsafe 提供了CAS 方法,直接通过**native 方式(封装 C++代码)**调用了底层的 CPU 指令 cmpxchg

Unsafe类,翻译为中文:危险的,Unsafe全限定名是 sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。

1、Unsafe 提供的 CAS 方法

主要如下: 定义在 Unsafe 类中的三个 “比较并交换”原子方法

/*
@param o 包含要修改的字段的对象
@param offset 字段在对象内的偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);

Unsafe 提供的 CAS 方法包含四个入参:

包含要修改的字段对象字段内存位置预期原值及新值

在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比

较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true ;如果不相匹配,处理器不做任何操作,并返回 false

19.5、CAS存在的缺点

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。

  • ABA问题

  • 循环时间长开销大

  • 只能保证一个共享变量的原子操作

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A - 2B-3A

  1. 循环时间长开销大 高并发下N多线程同时去操作一个变量,会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。解决 CAS 恶性空自旋的较为常见的方案为:

  • 分散操作热点,使用 LongAdder 替代基础原子类 AtomicLong。

  • 使用队列削峰,将发生 CAS 争用的线程加入一个队列中排队,降低 CAS 争用的激烈程度。JUC 中非常重要的基础类 **AQS(抽象队列同步器)**就是这么做的.

  1. 只能保证一个共享变量的原子操作。一个比较简单的规避方法为:把多个共享变量合并成一个共享变量来操作。 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,可以把多个变量放在一个 AtomicReference 实例后再进行 CAS 操作。比如有两个共享变量 i=1、j=2,可以将二者合并成一个对象,然后用 CAS 来操作该合并对象的 AtomicReference 引用。

20、解决ABA问题

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

20.1、AtomicStampReference 的构造器:

/**  
* @param initialRef初始引用  
* @param initialStamp初始戳记  
*/
AtomicStampedReference(V initialRef, int initialStamp)

20.2、AtomicStampReference 的常用方法:

public class CASDemo {
    public static void main(String[] args) {
        //integer默认缓存-128->127,超过这个范围就要new对象了,就会分配新的地址,
        // compareAndSet源码是expectedStamp == current.stamp,如果实在缓存范围之内就地址值相同int 2022与2023地址值不相同
        // 在比较之前我们手动创建一个int对象让堆里面存在对象这个地址,对比的就是两个对象的引用地址

        Integer i = new Integer(2023);
        Integer o = new Integer(2024);
        boolean state = false;
        //设置一个初始值-(一般为对象)和初始版本号
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(i, 0);
        //获取最新版本号
        int stamp = atomicStampedReference.getStamp();
        //(期望值,更新值,版本号,版本号+1)
        state = atomicStampedReference.compareAndSet(i, o, stamp, stamp + 1);
        System.out.println("是否更新成功:" + state + ";更新值:" + "" + atomicStampedReference.getReference() + ";版本号:" + atomicStampedReference.getStamp());
        //是否更新成功:true;更新值:2024;版本号:1
        //修改印戳,更新失败
        stamp = 0;
        state = atomicStampedReference.compareAndSet(o, i, stamp, stamp + 1);
        System.out.println("是否更新成功:" + state + ";更新值:" + "" + atomicStampedReference.getReference() + ";版本号:" + atomicStampedReference.getStamp());
        //是否更新成功:false;更新值:2024;版本号:1
    }
}

Integer缓存问题

21、各种锁的理解

lock、synchronized默认都是可重入锁,非公平锁

21.1、公平锁和非公平锁

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

非公平锁:非常不公平,可以插队(默认都是非公平)

//ReentrantLock()有参调用公平锁,无参调用非公平锁
public ReentrantLock {
sync = new Nonfairsync();        //默认非公平
  
}
 
public ReentrantLock(boolean fair) {
sync = fair ? new Fairsync() : new Nonfairsynco;       //如果为true则设置为公平锁

21.2、可重入锁

  • 解释一:可重入锁也就是某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

  • 解释二:可重入锁又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的**内层方法会自动获取锁(**前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。

可重入锁是某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。再次获取锁的时候会判断当前线程是否是已经加锁的线程,如果是对锁的次数+1,释放锁的时候加了几次锁,就需要释放几次锁。

0

  1. Synchronized 锁

package com.sjmp.demo21Lock;
public class SynchronizedDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }
}
class Phone{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName()+"=> sms");
        call();// 这里也有一把锁
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"=> call");
    }
}

2.Lock锁

21.3、自旋锁

1.spinlock

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

2.自定义自旋锁

public class TestSpinLock {
    // 默认
    // int 0
    //thread null
    AtomicReference<Thread> atomicReference=new AtomicReference<>();

    //加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===> mylock加锁");

        //自旋锁
        while (!atomicReference.compareAndSet(null,thread)){
            System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
        }
    }


    //解锁
    public void myUnlock(){
        Thread thread=Thread.currentThread();
        System.out.println(thread.getName()+"===> myUnlock解锁");
        atomicReference.compareAndSet(thread,null);
    }
}

测试自定义自旋锁

public class SpinlockDemo {
    public static void main(String[] args) throws InterruptedException {
        //使用CAS实现自旋锁
        TestSpinLock spinlockDemo=new TestSpinLock();
        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myUnlock();
            }
        },"t1").start();

        //T1 T2 拿到的锁的是同一个对象spinlockDemo,所以在同一个时间内只能有一把锁
        //T1先拿到锁后休眠3(sleep)秒钟,但不释放锁(atomicreference)
        //T2 过了1秒钟后开启线程,发现锁(tomicreference)被T1占用了,所以一直自旋等待拿锁.
        //T1 3秒休眠之后释放锁,T2拿到了锁结束自旋(自旋时长2秒)
        //T2 拿到锁,休眠3秒后解锁,程序结束

        TimeUnit.SECONDS.sleep(1);


        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myUnlock();
            }
        },"t2").start();
    }
}

测试结果

t1===> mylock加锁
t1===> myUnlock解锁
t2  ==> 自旋中~ (两秒)
t2===> mylock加锁
t2===> myUnlock解锁

22、死锁

死锁产生的原因

  1. 互斥条件一个资源只能被一个线程占有,当这个资源被占有后其他线程就只能等待

  1. 不可剥夺条件:当一个线程不主动释放资源时,此资源一直被拥有线程占有

  1. 请求并持有条件:线程已经拥有了一个资源后,又尝试请求新的资源

  1. 循环等待条件:产生死锁一定是发生了线程资源环形链

package com.ogj.lock;

import java.util.concurrent.TimeUnit;

public class DeadLock {
    public static void main(String[] args) {
        String lockA= "lockA";
        String lockB= "lockB";

        new Thread(new MyThread(lockA,lockB),"t1").start();
        new Thread(new MyThread(lockB,lockA),"t2").start();
    }
}

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

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

    //T1x线程拿到A想要B,但是此时T2拿到B想要A,两个人都不放下锁的但是都要获取对方的锁,造成死锁
    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
            }
        }
    }
}

如何解开死锁

死锁状态下

  1. 使用 jps 定位进程号,jdk 目录 bin 下 :有一个 jps

命令: jps -l

  1. 使用 jstack 进程进程号找到死锁信息

命令:jstack 进程号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值