JUC并发编程

JUC并发编程

  1. Future接口(FutureTash实现类)定义了操作异步任务的一些方法,如获取异步任务的执行结果,取消任务的执行,判断任务是否被取消,判断任务是否执行完毕.

  2. 主线程让子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他的事情了,忙完其他的事情或者执行完成了,过了一会才去获取子任务执行结果或者变更任务的状态

  3. Future是java5新加的一个接口,他提供了一种异步并行计算的功能,如果主线程需要执行一个很耗时的计算任务,我们就可以把这个任务放到异步线程中执行,主线程继续处理其他的任务或者先行结束,然后再通过Future获取到计算的结果,三个特点,多线程/有返回/异步任务

案列:

package com.hema.demo;

import java.util.concurrent.*;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
//        FutureTask继承
        FutureTask<String> futureTask = new FutureTask<>(new Thread1());
        Thread thread = new Thread(futureTask);
        thread.start();
        //FutureTask接口的话,容易出现阻塞的问题,FutureTask接口提供了进行异步请求的方法,但是FutureTask.get()方法很容易造成程序的阻塞,
        // 一般建议放到程序后面,如果调用的话不见结果不继续向下进行,不管是否计算完成,容易造成程序的阻塞
        String s = futureTask.get();
        System.out.println(s);
        //针对futureTask接口下的get方法很容易造成程序的阻塞,因此的话,提供了超时结束的方法,如果拿不到结果,就会抛出异常
        futureTask.get(20, TimeUnit.MICROSECONDS);

    }
}

/**
 * Callable不能创建thread,但是有返回值
 */
class Thread1 implements Callable {

    @Override
    public Object call() throws Exception {
        return "ssss";
    }
}

/**
 * Runnable接口可以创建线程,但是没有返回值,线程创建的构造方法,thread创建接收一个runnable对象
 */
class Thread2 implements Runnable {

    @Override
    public void run() {

    }
}


1.Future+线程池异步多线程配合

Future线程池异步多线程任务配合,可以提高程序的执行效率

package com.hema.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class TestDemo {
    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(3);

        Long startTime = System.currentTimeMillis();

        FutureTask<String> futureTask = new FutureTask<>(()->{
            try {
                TimeUnit.MICROSECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task1 start";
        });
        executorService.submit(futureTask);
        FutureTask<String> futureTask2 = new FutureTask<>(()->{
            try {
                TimeUnit.MICROSECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task1 start";
        });
        executorService.submit(futureTask2);
        FutureTask<String> futureTask3 = new FutureTask<>(()->{
            try {
                TimeUnit.MICROSECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "task1 start";
        });
        Long endTime = System.currentTimeMillis();
        Long consumerTime = endTime - startTime;
        System.out.println("花费的时间"+consumerTime);
        executorService.shutdown();
    }

    private static void m1() {
        Long startTime = System.currentTimeMillis();
        try {
            TimeUnit.MICROSECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            TimeUnit.MICROSECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            TimeUnit.MICROSECONDS.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        Long consumerTime = endTime - startTime;
        System.out.println("花费的时间"+consumerTime);
  
    }
}

2.Future接口获取到异步任务返回的结果只能通过阻塞或者轮询的方式进行获取

package com.hema.demo;

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

public class TestDemo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用while来进行轮询异步请求结果
        FutureTask<String> futureTask = new FutureTask<>(
                new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        try {
                            TimeUnit.MICROSECONDS.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return "aaaa";
                    }
                }
        );
/**
    要么会被阻塞,要么耗费cpu的资源
    轮询的方式会耗费cpu的资源,如果想要获取到异步的结果,
    通常情况下会以轮询的方式获取结果,尽量不要用阻塞,Future对于结果的获取,
    只能通过阻塞或者轮询的方式获取到任务的结果
 */
        while(true) {
            if (futureTask.isDone()) {
                System.out.println(futureTask.get());
            } else {
//                如果执行的时间比较的长,就可以定时的打印一些信息
                TimeUnit.MICROSECONDS.sleep(200);
                System.out.println("不要在催了,再催熄火了");
            }
        }
    }



}



2.1.CompletableFuture接口(解决get阻塞和CPU消耗)

package com.hema.demo;

import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.DoubleStream;

public class CompleteableFutureDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

//        CompleteableFuture的话可以有效的进行减少线程的阻塞和cpu消耗
        CompletableFuture.supplyAsync(()->{
            //如果不传入线程池的话,会使用默认的ForkJoinPool线程池,
            // 该线程池中的线程是守护线程,当主线程耗时时间比较短就会出现该异步线程未被执行的情况
//            因此的话,我们可以使用自定义的线程池
            System.out.println("获取到当前线程的名字"+Thread.currentThread().getName());
            if (Thread.currentThread().isDaemon()) {
                System.out.println("该线程是守护线程");
            }
            DoubleStream doubles = new Random().doubles();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return doubles;
        }).whenComplete((v,e)->{
            if (e == null) {
                System.out.println("进入方法中---------");
                System.out.println("打印" + v);
            }
        }).exceptionally(e->{
            e.printStackTrace();
            System.out.println("有情况发生,请注意");
            return null;
        });
        System.out.println(Thread.currentThread().getName()+"去忙其他的事情了");
    }

    /**
     * CompletableFuture继承自Future,跟Future没有差别
     * @throws InterruptedException
     * @throws ExecutionException
     */
    private static void trouch1() throws InterruptedException, ExecutionException {
        CompletableFuture completableFuture = CompletableFuture.supplyAsync(()->{

            DoubleStream doubles = new Random().doubles();
            System.out.println("线程名字:"+Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return doubles;

        });

        System.out.println("主线程"+Thread.currentThread().getName());
        System.out.println(completableFuture.get());
    }
}


2.2.函数式接口


Runnable: run方法,没有参数,没有返回值
Function: apply方法,功能函数式接口,有一个参数,有返回值
consume: accept方法,消费型函数接口,有一个参数,无返回值
supplier: get方法,没有参数,有返回值
BiConsumer:accept方法,有两个参数,无返回值

2.3.completableFuture之获取到结果

get(): 容易造成线程的阻塞
get(long timeout,TimeUnit unit): 如果超过规定的时间没有获取到结果,就会抛出异常
join(): 跟get()的作用是一样的
getNow(): 没有返回结果的话,可以返回一个预定的结果,防止阻塞发生

2.4.completableFuture对计算结果进行处理

thenApply: 计算结果存在依赖关系,这两个线程存在串行化关系,当前步奏出错,就不往下面走了,任务A执行完成后执行任务B,B需要A的结果,同时任务B会有返回值

handle: 计算结果存在依赖关系,这两个线程串行化,如果有异常的话也可以往下走,可以根据带的异常参数做进一步的处理
thenRun:任务A执行完成后执行B,并且B不需要A的结果


代码:


package com.hema.demo;

import java.util.concurrent.*;

public class CompletableFutureDemo1 {

    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
        CompletableFuture.supplyAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return 1;
        },scheduledExecutorService).thenApply(f->{
            f=f+20;
            return f;
        }).thenApply(f->{
            f=f+30;
            return f;
        }).whenComplete((v,e)->{
            if (e == null) {
                System.out.println(v);
            }
        }).exceptionally(
                (e)->{
                    e.printStackTrace();
                    return null;
                }
        );
    }
}


2.5.CompletableFuture对计算结果进行消费

thenAccept:接收任务处理结果,并且消费处理,无返回结果


2.6.CompletableFuture运行线程池的选择

  1. 没有传入自定义的线程池,都用默认的线程池ForkJoinPool
  2. 传入一个自定义的线程池,
    如果你执行的第一个任务的时候,传入了一个自定义的线程池
    调用thenRun方法执行第二个任务的时候,则第二个任务和第一个任务是共用同一个线程池
    调用thenRunAsync执行第二个任务的时候,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin 线程池
  3. 有可能处理的太快了,系统优化切换原则,直接使用main线程做处理
    thenAccept和thenAcceptAsync,thenApply和thenApplyAsync,区别是一样的

2.7.CompletableFuture对计算结果合并

两个CompletionStage任务都完成之后,最终需要把两个,任务的结果一起交给thenCombine来进行处理,先完成的先等着,等待其他的分支任务 thenCombine

package com.hema.demo;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureDemo3 {
    public static void main(String[] args) {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"-----线程进行启动");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 10;
        });

        CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"-----线程启动");

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return 20;
        });
        CompletableFuture<Integer> completableFuture2 = completableFuture.thenCombine(completableFuture1, (x, y) -> {
            System.out.println("开始合并两个异步执行的结果");
            return x + y;
        });
        /**
         * join操作获取异步执行的结果不用抛出异常,get操作获取异步执行的结果需要抛出异常
         */
        System.out.println(completableFuture2.join());
    }
}

3.多线程知识锁的概念

3.1锁的概述(乐观锁和悲观锁)

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此再获取数据的时候会先枷锁,确保数据不会被别的线程修改。
适用范围:适合写操作多的场景,先加锁可以保证写操作的时候数据正确
乐观锁
认为自己在使用数据的时候不会有别的线程修改数据或者资源所以不会添加锁
在java中是通过使用无锁编程实现的,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据
如果这个数据没有被更新,当前线程将自己修改的数据进行成功的写入,
如果这个数据已经被其他的线程更新,则根据不同的实现方式执行不通的操作,比如放弃修改,重试抢锁

判断规则:

  1. 版本号机制
  2. 最常用的是CAS算法,java原子类中的递增操作就通过CAS自旋实现的

适用的范围,适合读操作多的场景,不加锁的特点就是可以使其读操作性能大幅度提升,乐观锁直接去操作同步资源,是一种无锁算法

高并发时,同步调用应该去考量锁的性能损耗,能用无锁数据结构,就不要使用锁,能用锁区块,就不要锁整个方法体,能用对象锁,就不要使用类锁,尽可能的使锁的代码块工作量尽可能的小一点,避免在锁代码块中调用RPC方法

如果一个对象中有多个synchronized方法,某一时刻内,只要一个线程区调用其中的一个synchronized方法,那么其他的线程都只能等待,某一个时刻内,只能有唯一的一个线程去访问这些

3.1.1.锁的粒度(类锁和对象锁)

对象锁:
synchronized方法,锁的对象this,被锁定后,其他的线程都不能进入到当前对象的其他synchronized方法

普通方法后,发现和同步锁无关
换成两个对象后,不是同一把锁了,情况发生改变

对于普通的同步方法,锁的是当前的实例对象,通常是指this,具体的一部手机,所有的同步方法用的都是同一把锁,实列对象本身

类锁
对于静态的同步方法,锁的是当前类的class对象,对于同步方法块,锁的是synchronized括号内的对象

3.1.2.锁的实现原理

同步代码块的实现:
通过反编译,我们可以发现同步代码块使用的是monitorenter和monitorexit指令

11: monitorenter
      12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #4                  // String 进入同步方法内部!请注意查收
      17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_2
      21: monitorexit

调用指令会将检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后在方法完成后,释放掉monitor锁

Monitors管程

管程(Monitors也称为监视器)是一种程序结构,结构内的多个子程序(对象或者模板)形成多个工程互斥访问共享资源,这些共享资源一般是硬件设备或者是一群变量,对共享变量能够进行的所有操作都集中在一个模块中,管程实现了在一个时间节点,最多只能有一个线程在执行管程的某个子程序,管程提供了一种机制,管程可以看作一个软件模块,他是将共享的变量和对于这些共享变量操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制

java虚拟机可以支持方法级别的同步和方法内部的一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将其称为锁)来实现

方法级别的同步是隐式的,无需通过字节码指令来控制,它实现在方法的调用和返回操作中,虚拟机可以从方法常量池中的方法表结构ACC_SYNCHRONIZED访问标志中得到一个方法是否被声明为同步方法,当方法调用的时候,调用指令会将检查方法得ACC_SYNCHRONIZED访问标识是否被设置,如果设置了,执行线程就要要求先成功得持有管程,然后才能执行方法,最后当方法完成后,释放掉管程,在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程,如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有得管程将在异常抛出到同步方法边界之外时进行自动释放

同步一段指令集序列,通常是由java语言中得synchronized语句来表示得,java虚拟机得指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要java编译器和java虚拟机两者共同协作支持

3.2.锁的概述(公平锁和非公平锁)

公平锁:
是指多个线程爱找申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的

Lock lock = new ReentrantLock(true)
非公平锁:
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的环境下,有可能造成优先级反转或者饥饿状态(就是某个线程会一直得不到锁)
Lock lock = new ReentrantLock(false)//标识为非公平锁,后来的也可能先获取到锁
Lock lock = new ReentrantLock();//默认是非公平锁

默认使用非公平锁的原因
恢复挂起的线程到真正锁的获取还是有时间差的,从CPU的角度看,这个时间差存在的还是很明显的,所以非公平锁能够重复的利用CPU的时间片,尽量减少CPU空闲状态的时间

使用多线程很重要的考量点是线程切换的开销,当采用非公平锁的时候,当一个线程请求锁获取到同步状态,然后释放同步状态,所以刚释放的锁的线程在此刻再次获取到同步状态的概率非常大,所以减少了切换线程开销的时间

3.2.3锁的概述(可重入锁又叫做递归锁)

可重入锁(递归锁)
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁的对象是一个对象),不会因为之前已经获取过还没释放而阻塞(内层和外层可以使用同一把锁,而且不会被阻塞)。
java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点就是一定程度上上可以避免死锁

3.2.3.1.synchronized可重入锁(隐式锁)

可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不会发生死锁,这样的锁就叫做可重入锁,在一个synchronized修饰方法或者代码块内部调用本类中其他synchronized修饰的方法或者代码块时,是可以永远得到锁的

3.2.3.2 ReentrantLock(显式锁)

每一个锁对象都拥有一个锁计数器和一个指向持有该锁的线程的指针

当执行monitorenter时,如果目标锁对象的计数器为零,说明他没有被其他的线程所持有,java虚拟机会将该锁对象持有的线程设置成当前线程,并且将该计数器加1
在目标锁对象的计数器不为零的情况下,如果锁对象持有的线程是当前的线程,那么java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
当执行monitorexit时,java虚拟机则需要将锁对象的计数器减一,计数器为零代表着锁已经被释放掉了

3.3.锁的概述(死锁)

死锁是指两个或者两个以上的线程在执行的过程中,因争夺资源而造成一种相互等待的现象,如果没有外力干涉,那将无法向下推进,如果系统资源充足,进程资源请求可以得到满足,出现死锁的可能性很低,否则就会因为争夺资源陷入死锁
排查死锁

jps-l
jstack 进程编号
jconsole 图形化界面

4.线程中断机制

4.1 简介

一个线程不应该由其他线程来强制中断或者停止,而是应该由线程自己自行停止,在java中没有办法立即停止一个线程,但是停止线程却十分的重要,如取消一个耗时操作,因此java提供了一种用于停止线程的协商机制–中断,也叫做中断标识协商机制

中断只是一种协商机制,java没有给中断增加任何语法,中断过程完全需要我们自己实现,如果要中断一个线程,我们需要手动的调用该线程的interrupt方法,该方法也只是将线程对象的中断标识设置为true,戒指我们需要自己写代码不断的检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,然后再进行其他的操作
每个线程对象都有一个中断标识为,用于标识该线程是否被中断,true表示中断,false表示未中断,通过调用线程的interrupt方法将线程的标识位设置为true,可以在别的线程中调用,也可以在自己的线程中调用

4.2.中断机制的三大方法

这三个方法都是在thead类下面


interrupt(): 实列方法,仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
interrupted(): 静态方法,判断线程是否被中断,并且清除当前中断状态
1.返回当前线程的中断状态,测试当前线程是否已经被中断
2.将当前线程的中断状态清零并且重新设置为false,清除线程的中断状态

isInterrupted(): 实列方法,判断当前线程是否被中断

4.2.1 如何停止中断运行中的线程

1.通过一个volatile变量实现

package com.hema.lock;

import java.util.concurrent.TimeUnit;

public class Intreeuption {

    static volatile boolean flag = false;
    public static void main(String[] args) {


        new Thread(()->{
            while (true) {
                if (flag) {
                    System.out.println("线程被中断"+Thread.currentThread().getName());
                    break;
                }
                System.out.println("volatile---------handle-----------"+Thread.currentThread().getName());
            }
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            flag = true;
        }, "t2").start();
    }
}

  1. 通过AtomicBoolean实现线程中断
package com.hema.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class Intreeuption {

    static volatile boolean flag = false;
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    public static void main(String[] args) {
        new Thread(()->{
            while (true) {
                if (atomicBoolean.get()) {
                    System.out.println("线程被中断"+Thread.currentThread().getName());
                    break;
                }
                System.out.println("volatile---------handle-----------"+Thread.currentThread().getName());
            }
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            atomicBoolean.set(true);
        }, "t2").start();

    }

  1. 使用interrupt实现线程的中断停止
package com.hema.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class Intreeuption {

    static volatile boolean flag = false;
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                //默认isInterrupted是false
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程被中断" + Thread.currentThread().getName());
                    break;
                }
                System.out.println("volatile---------handle-----------" + Thread.currentThread().getName());
            }
        }, "t1");
        thread.start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            thread.interrupt();
        }, "t2").start();

    }

当一个线程调用interrupt()方法时

  1. 如果线程处于正常活动状态的时候,那么会将该线程的中断标志设置为true,并不会直接将该线程停止,回去调用底层的native方法,被设置中断标志的线程会继续的正常运行,不受影响,所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才可以
  2. 如果线程处于被阻塞状态(sleep,wait,join等状态),在别的线程中调用当前线程对象的interrupt方法,该线程会立刻推出被阻塞状态,抛出interruptedException异常,如果该线程阻塞的调用wait(),join(),sleep(),这个的方法,那么他的中断状态将会被清除,然后收到一个异常,因此的话在异常抛出的地方需要再次重新调用interrupt()
  3. interrupt()方法对不活动方法不会产生影响

静态方法Thread.interrupted

4.3.LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除线程

三种等待唤醒方法

  1. object类中的wait和notify方法实现线程的等待和唤醒
  2. condition接口中的await后signal方法实现线程的等待和唤醒
  3. lockSupport类中的park等待和unpark唤醒

前两种方式,线程必须要先获取并且持有锁,必须在(synchronized或者lock)中,必须要先等待后唤醒,线程才能够被唤醒

最后一种方式是采用了许可证的机制,permit许可证默认没有不能放行,所以一开始调park()方法当前线程就会被阻塞,直到别的线程给当前线程发放permit,park方法才会被唤醒

package com.hema.lock;

import java.util.concurrent.locks.LockSupport;

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("方法将进入,随后该线程将会被阻塞!"+Thread.currentThread().getName());
//           调用该方法线程将会被阻塞
            LockSupport.park();
            System.out.println("线程被释放掉!");
        },"t1");
        thread.start();
        new Thread(()->{
            System.out.println("进入T2线程"+Thread.currentThread().getName());
//            为t1线程发放通行许可证,许可证不会累计,这个凭证只会有一个
            LockSupport.unpark(thread);
        }).start();
    }
}

5.java内存模型JMM

JMM(java Memory Model),本身就是一种抽象的概念,他并不是真实存在的,他描述的是一组约定或者规范,通过这组
规范定义了线程中尤其是多个线程,各个变量的读写访问方式并决定一个线程对共享变量的何时写入以及如何变成对另一个线程可见,关键都是围绕着多线程的原子性,可见性,和有序性展开

  1. 通过JMM来实现线程和主内存之间的抽象关系
  2. 屏蔽各个硬件平台和操作系统中的内存访问差异以实现让java程序在各个平台下都能达到一致的内存访问效果

5.1.JMM规范下,三大特性

  1. 可见性
    当一个线程修改了某一个共享变量,其他的线程是否能够立刻知道该变更,JMM规定了所有的变量都存储在主内存中来完成

系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下可能会出现脏读,所以每个线程都有自己的工作内存,线程自己的工作内存保存了该线程使用到的变量的主内存副本的拷贝,线程对变量的所有操作(读取,赋值)都必须要在线程自己的工作内存中先进行,而不能够直接读写主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递都是需要通过主内存来完成

java线程->工作内存->save和load操作->主内存

  1. 原子性
    指的是一个操作是不能被打断的,也就是多线程环境下,操作不能被其他的线程干扰
  2. 有序性
    对一个线程的执行代码而言,我们总是习惯性的认为代码的执行顺序是从上到下的,有序执行,但是为了提高性能,编译器和处理器通常会对指令序列进行重新排序,java规范规定了JVM线程内部维持顺序化语义,即只要程序的最终结果与他的顺序化执行结果相等,那么指令的执行顺序可以和代码顺序执行不一致,此过程叫做指令重排

JMM可以根据处理器的特性(CPU多级缓存系统,多核处理器)适当的对机器的指令进行重新排序,使得机器指令能够更符合CPU的执行特性。最大限度的发挥机器性能,但是指令重排可以保证串行语义一致,但是没有义务保证多线程之间的语义也一致,两行以上不相干的代码在执行的过程中有可能先执行的不是第一条

源代码->编译器优化重排->指令并行重排->内存系统重排->最终执行指令
处理器在进行重排的时候必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也是无法预测的

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

6.Volatile与JMM

被Volatile修饰的变量有两大特点

  1. 可见性

  2. 有序性

当写一个Volatile变量的时候,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
当读一个Volatile变量的时候,JMM会把该线程对应的本地内存设置无效,重新回到主内存中读取最新的共享变量,所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取的

内存屏障(一类同步屏障指令),是cpu或者编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重新排序,内存屏障是一种JVM指令,java内存模型中的重排规则会要求java编译器在生成JVM指令的时候插入特定的内存指令屏障,通过这些指令屏障,Volatile实现了Java内存模型中的可见性和有序性
内存屏障前的所有写操作都要回写到主内存
内存屏障后的所有读操作都能获得内存屏障之前所有的写操作最新的结果

6.1内存屏障的分类

  1. 读屏障:
    在读指令之前插入读屏障,让工作内存或者CPU高速缓存当中的缓存数据失效,重新回到主内存中获取到最新的数据
  2. 写屏障
    在写入指令之后插入写屏障,强制把写缓冲区的数据刷新到主内存中
    Volatile变量只能保证可见性,i++不是原子性操作,因此,在多线程使用Volatile进行操作的时候,就会出现并发问题,因此的话,需要我们进行加锁

volatile写之前的操作,都禁止重排序到volatile之后
volatile读之后的操作,都禁止重新排序到volatile之前
volatile写之后的volatile读,禁止重新排序

7.CAS

7.1.CAS简介

CAS就是实现并发算法时常用到的一种技术
它包含三个操作数–内存位置,预期原始值和更新值
执行CAS操作的时候,将内存位置的值和预期原值进行比较:
如果相匹配,那么处理器就会自动的将该位置的值更新为新增
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功

package com.hema.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CasDemo {
    public static void main(String[] args) {
        //CAS是一个轻量级的自旋锁
        AtomicInteger atomicInteger = new AtomicInteger(5);
//        将预期值和原始值进行比较,如果相等的话,就将新值赋值给原始值做更新操作
        atomicInteger.compareAndSet(5,2222);
        System.out.printlns(atomicInteger.get());
    }
}


CAS是一条CPU并发原语他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的,AtomicInteger类主要利用了CAS+volatile和native方法来保证原子操作,从而避免了synchronized的高开销,执行的效率大大的提高

7.2原子引用(AtomicReference)


package com.hema.cas;

import java.util.concurrent.atomic.AtomicReference;

class People {
    public String name;
    public int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class Demo2 {
    public static void main(String[] args) {
        /**
         * 原子操作扩展到自定义类的一个接口AtomicReference,
         */
        AtomicReference<People> atomicReference = new AtomicReference<People>();
        People p1 = new People("zhangsan",23);
        People p2 = new People("lisi",24);
        atomicReference.set(p1);
        System.out.println(atomicReference.compareAndSet(p1,p2)+atomicReference.get().toString());
    }
}


7.2.1.自旋锁

CAS 是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,用来达到锁的效果,至于自旋,就是指尝试获取锁的线程不会被立即阻塞,而是采用循环的方式去尝试获取锁,当锁被占用后,会不断的循环判断锁的状态,知道获取,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU性能

简易自旋锁的实现:

package com.hema.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();


    /**
     * 加上锁
     */
    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"加上锁");
        //不断的比较,去抢占锁
        while (!atomicReference.compareAndSet(null,thread)) {

        }
    }

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

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        new Thread(
                ()->{
                    spinLock.lock();
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    spinLock.unlock();
                },"A"
        ).start();

//        保证A线程先执行
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unlock();
        },"B").start();

    }
}


7.2.2.CAS算法的缺点
  1. 如果CAS失败,就会一直的进行尝试,如果一直获取不到,CPU就会有很大的消耗
  2. CAS算法实现的一个重要的前提是需要取出内存种某个时刻的数据并在当下时刻比较并且替换,那在这个时间差类会导致数据的变化,比如说线程1从内存位置V取出A,这个时候另外一个线程2也可以从内存中取出A,并且线程2进行了一些操作将值变成B,然后线程2又将V位置数据变成A,这个时候线程1进行CAS操作发现内存中仍然是A,解决方案,使用邮戳

7.3 原子类操作

基本的原子类:AtomicInteger,AtomicBoolean,AtomicLong
数组类型的原子类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
引用类型原子类型:AtomicReference,AtomicStampedReference(修改过多少次),AtomicMarkableReference(解决是否被修改过)
对象属性修改原子类:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater


以线程安全的方式操作非线程安全对象的某些字段
没有用对象修改原子类,使用synchronized粗粒度锁,锁整个对象:

package com.hema.cas;

import java.util.concurrent.CountDownLatch;

class BankAccount {
    public String BankName = "CIC";
    public int money = 0;

    public synchronized void setMoney() {
        money++;
    }
}
public class Demo {

    public static void main(String[] args) throws InterruptedException {
        /**
         * 在一个或者一组线程操作的过程中,必须要等到其他线程执行完才可以
         */
        CountDownLatch countDownLatch = new CountDownLatch(10);
        BankAccount bankAccount = new BankAccount();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        bankAccount.setMoney();
                    }
                } finally {
                    //一个线程执行完成后,才可以去执行另一个线程
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();

        }
        //一个线程执行完成后,才可以去执行另一个线程
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"///"+bankAccount.money);
    }

}


使用对象修改原子类:

package com.hema.cas;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

class BankAccount2 {
    public String BankName = "CIC";
    //    使用对象属性修改原子类,要修改类的属性就要使用public volatile关键词修饰
    public volatile int money = 0;

//    public  void setMoney() {
//        money++;
//    }

    AtomicIntegerFieldUpdater<BankAccount2> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount2.class,"money");

    public void getMoney(BankAccount2 bankAccount2){
        atomicIntegerFieldUpdater.getAndIncrement(bankAccount2);
    };


}

public class Demo3 {


    public static void main(String[] args) throws InterruptedException {
        /**
         * 在一个或者一组线程操作的过程中,必须要等到其他线程执行完才可以
         */
        CountDownLatch countDownLatch = new CountDownLatch(10);
        BankAccount2 bankAccount = new BankAccount2();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        bankAccount.getMoney(bankAccount);
                    }
                } finally {
                    //一个线程执行完成后,才可以去执行另一个线程
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();

        }
        //一个线程执行完成后,才可以去执行另一个线程
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "///" + bankAccount.money);
    }
}

7.3.1 LongAdder和Atomiclong

AtomicLong是一个CAS自旋锁的原子类,适用于线程少,数据量小的前提,如果放到高并发,大数据的条件下,如果并发很大就会出现问题,大量的线程在进行自转,等待获取到锁,就会出现cpu占用过高的问题,

LongAdder的基本思路就是分散热点,如果将value分散到一个Cell数组中,不同线程会命中到数组不同的槽中,各个线程只对自己槽中的值进行CAS操作,这样热点就被分散了,冲突的概率就会很小,如果要获取到真正的long值,只要将各个槽中的变量值累加返回化整为零,分散热点,sum()会将所有Cell数组中value和base累加作为返回值,核心四线就是将之前的AtomicLong一个value更新压力分散到多个value中去,从而降级更新热点,典型的空间换取时间

8.ThreadLocal

8.1.ThreadLocal的详细解读

ThreadLocal 提供线程局部变量,这些变量和正常的变量不同,因为每一个线程在访问ThreadLocal实列的时候(通过get和set方法)都有自己的,独立的初始化的变量副本,ThreadLocal实列通常是类中的私有的静态字段,它使用的目的是想要将状态(列如用户的ID或者事务ID)与线程相关联起来(traceId)

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量,不麻烦别人,不和其他人共享,人人有份),主要解决的问题是让每个线程都绑定了自己的值,通过使用get()
和set()方法,获取默认值或者将其值更改为当前线程所存储的副本值,从而避免了线程安全问题

必须回收自定义的ThreadLocal变量,尤其是在线程池场景下,线程经常会被复用,如果不清理干净自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏的问题,所以,应当尽量使用try-finally块进行ThreadLocal本地线程变量的回收,因为在线程池条件下,会出现本地线程复用

8.2ThreadLocal原理

因为每个Thread内部都有自己的实列副本,并且该副本只有当前线程自己使用,既然其他的Thread不可访问,就不存在多线程之间共享的问题,统一的设置初始值,但是每个线程对这个值得修改都是各自线程互相独立的
ThreadLocalMap实际上就是一个以threadLocal实列为key,任意对象为Value的Entry对象,当我们为threadLocal变量赋值的时候,实际上就是以当前的threadLocal实列为key,值为Value的键值对往ThreadLocalMap中进行存放

8.3ThreadLocal内存泄漏

8.3.1.什么是内存泄漏

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏(就是说内存被占用无法被释放)

强引用:
    内存不足的时候,JVM开始进行垃圾回收,对于强引用的对象,就算出现了OOM也不会对该对象进行回收,在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,当对象被强引用变量引用时,他是可达的状态,因此不可能被垃圾回收机制回收,对于一个普通的对象,如果没有其他的引用关系,只要超过了引用作用域或者显式的将相应的强引用赋值为null,这样可以被垃圾收集掉了
软引用:
    软引用是一种相对强引用弱化的一些引用,需要使用java.lang.ref.softReference类来实现,可以让对象豁免一些垃圾收集,,对于只有软引用的对象来说,当系统内存充足的时候,他不会被回收,当系统内存不足的时候,他会被回收 

弱引用:
    他比软引用的生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存

虚引用:
    虚引用必须和引用队列(ReferenceQueue)联合进行使用,虚引用的主要作用是跟踪对象被垃圾回收的状态,设置虚引用关联对象的唯一目的,就是在则会个对象被收集器回收的时候收到一个系统通知或者后续添加进一步处理,如果虚引用被GC后,就会添加到引用队列中

Thread中的ThreadLocal是强引用的,ThreadLocal中得ThreadLocalMap两者之间是弱引用的,因此的话,当弱引用被GC后,即ThreadLocalMap中的key设置为null后,会导致map中的值无法被释放掉,因此的话,如果是线程池环境下,会出现内存的泄漏问题,需要调用remove,将threadLocal进行释放
ThreadLocal对象使用的是static修饰,ThreadLocal无法解决共享对象更新的问题

8.4.对象内存布局

在hotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三部分:对象头(Header)、实例数据、对齐填充

对象头(各占8个字节)

  1. 对象标记:包括GC的标记,GC的次数,hash码,以及锁的标识
  2. 类元信息(又叫做类型指针)

实例数据:
存放类属性数据信息,包括父类属性信息
对齐填充:
虚拟机要求对象的起始地址必须是8字节的整数倍,填充数据不是必要存在的,仅仅是为了字节对齐,这部分内存按照8字节进行补充对齐

对象的分代年龄最大为15

查看虚拟机启动的参数:
java -XX:+PrintCommandLineFlags -version

9 synchronized与锁升级

用锁可以实现数据的安全性,但是会带来性能的下降,无锁能够基于线程并行提升程序的性能,但是会带来安全性下降
高并发时,同步调用应该去考量锁的性能损耗,能用无锁的数据结构,就不要用锁,能锁区块,就不要锁整个方法体,能锁对象,就不要用类锁,加锁的代码块工作量尽可能要小,避免在锁代码块中调用RPC方法

无锁->偏向锁->轻量级锁->重量级锁

在java早期版本中,synchronized属于重量级锁,效率低下,因为监视器Monitor是依赖于底层操作系统的,挂起线程和恢复线程都需要转入内核去完成,阻塞或者唤醒一个java线程需要操作系统切换CPU 完成,这种状态切换需要耗费处理时间,所以synchronized效率比较低下,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁

9.1.偏向锁

偏向锁:
MarkWord 存储的是偏向的线程ID,当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问就会自动的获得到锁
多线程情况下,锁不仅存在多线程竞争,还存在锁由同一个线程多次获得的情况,偏向锁就是在这个情况下出现的,他的出现是为了解决只有一个线程执行同步时提高性能
偏向锁会偏向于第一个访问锁的线程,如果在接下来运行的过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步,也偏向锁在资源没有竞争的情况下消除了同步语句,提高程序性能

开启偏向锁:
-XX:+UseBiasedLocking
关闭延迟
-XX:BiasedLockingStartupDelay=0

关闭偏向锁
-XX:-UseBiasedLocking

偏向锁在JDK1.6以上默认是开启的,开启后程序启动几秒后才会被激活,可以使用JVM参数来关闭延迟,如果关闭偏向锁,默认就会进入到轻量级锁

9.2.轻量级锁

轻量锁:
MarkWord存储的是指向线程栈中的lock Record的指针

重量级锁:
MarkWord存储的是指向堆中的monitor对象的指针
轻量级锁是为了在线程近乎交替执行同步块时提高性能,主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥产生的性能消耗,升级的时机,当关闭偏向锁功能或者多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如S线程已经拿到锁,这个时候线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到了,当前锁已经是偏向锁,而线程B在争抢的时候发现了对象头中的Mark Word中的线程ID不是线程B自己的线程ID,那么线程B就会进行CAS操作希望能够获取到锁,如果获取锁成功,直接替换掉Mark Word中的线程ID为B自己的ID,重新偏向于其他的线程,将偏向锁交给其他的线程,相当于当前线程被释放,该锁会保持偏向锁的状态,A线程Over,B线程上位,如果获取锁失败了,则偏向锁就会升级为轻量级锁,此时轻量级锁由原持有的偏向锁的线程持有,继续执行代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁

10.AQS

字面意思抽象队列同步器,是用来实现锁或者其他同步器组件的公共基础部分抽象的实现,是重量级基础框架以及整个JUC体系的基石,主要用于解决锁分配给谁的问题,整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,通过一个int类变量表示持有锁的状态

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁的分配,这个机制主要是CLH队列的变体实现的,将暂时获取不到锁的线程放到队列中,这个队列就是AQS同步队列的抽象表现,他将要请求的共享资源的线程以及自身的等待状态封装成队列的节点对象,通过CAS,自旋,以及LockSupport.park()的方式,维护state变量的状态,使得并发达到同步的效果

11.读写锁

ReentrantReadWriteLock
一个资源可以被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值