Java并发编程学习(持续更新)

本文内容摘自:Java并发编程之美

1.并发编程线程基础

  • 什么是线程?操作系统在运行一个程序时会为其创建一个进程,而操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程;操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以说线程是CPU分配的基本单元
  • 在Java中我们启动main函数时其实就是启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程;
    在这里插入图片描述
  • 如上图,一个进程中有多个线程,多个线程共享进程的堆和方法区资源, 但是每个线程有自己的程序计数器和栈区域。
    另外每个线程都有自 己的战资源,用于存储该线程的局部变量,这些局部变量是该线 程私有的,其他线程是访问不了的,除此之外枝还用来存放线程的调用栈帧。
    堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用 new 操作创建的对象实例。
    方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的。

1.1线程创建与运行

  • Java中有三种线程创建方式,分别为实现Runnable接口的run方法继承Thread类并重写run的方法使用FutureTask方式
  • 方式一:(继承Thread)注意当创建完Thread对象后该线程并没有被启动执行,知道调用了start方法后才真正启动了线程;并且在调用start方法后并没有马上执行而是出于就绪状态,即该线程已经获取了CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态,一旦run方法执行完毕该线程就处于终止状态;
  • 方式二:(实现Runnable接口)优点是当多个线程执行一样任务时需要多份相同任务代码,而实现此接口可减少代码量;
  • 方式三:(FutureTask方式)实现Callable接口的call方法,并且该方式创建的线程执行完毕后有返回值;注:start()方法两个作用:启动新线程和调用run()方法;
public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        //方式一
        new MyThread1("good ").start();//启动线程
        new MyThread1("bad ").start();
        //方式二
        new Thread(new MyThread2("nice ")).start();
        new Thread(new MyThread2("haha ")).start();
        //方式三
        FutureTask<String> task = new FutureTask<>(new MyThread3());
        new Thread(task).start();
        try {
            System.out.println(task.get());//等待任务执行完毕并返回结果
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    //继承Thread类重写run方法创建线程
    static class MyThread1 extends Thread {
        private String message;
        public MyThread1(String message) {
            this.message = message;
        }
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.print(message);
            }
        }
    }
    //实现Runnable接口创建线程
    static class MyThread2 implements Runnable {
        private String message;
        public MyThread2(String message) {
            this.message = message;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.print(message);
            }
        }
    }
    static class MyThread3 implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "hello !";
        }
    }
}

1.2线程通知与等待

  • wait()函数
    当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事才返回:1.其它线程调用了该共享变量(对象)的notify()方法或notifyAll()方法;2.其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。注:若调用wait()方法的线程未事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。
    在这里插入图片描述另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒),即使该线程没有被其他线程调用 notify()、 notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒;以下代码是经典的调用共享变量 wait()方法的实例:
    在这里插入图片描述
//模拟生产者/消费者场景
private static volatile LinkedList<Integer> queue = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
        Thread product = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (queue) {
                        //消费队列满,则等待队列空闲
                        while (queue.size() == 10) {
                            //挂起当前线程,并释放通过同步块获取的queue上的锁
                            // 消费者线程可以获取该锁来获取队列里面的元素
                            queue.wait();
                        }
                        //空闲则生成元素,并通知消费者线程
                        queue.push(2);
                        queue.notifyAll();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread consumer = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (queue) {
                        //消费队列空
                        while (queue.size() == 0) {
                            //挂起当前线程,并释放通过同步块获取的queue上的锁
                            // 生产者线程可以获取该锁来往队列中增加元素
                            queue.wait();
                        }
                        //消费元素并通知唤醒生产者线程
                        queue.pop();
                        queue.notifyAll();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
  • wait(long timeout)函数
    如果一个线程调用了共享对象的该方法挂起后,没有在指定的timeout ms时间内被其它线程调用该共享变量的notify()方法唤醒,那么该函数还是会因为超时而返回;如果将timeout设置为0则和wait方法一样,因为在wait方法内部就是调用了wait(0),再如果传入负的timeout则会抛出IllegalArgumentException异常。
  • wait(long timeout,int nanos)函数
    在其内部调用的是 wait(long timeout)函数,如下代码只有在 nanos>0时才使参数timeout递增 1;
  • notify()函数
    一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程;一个共享对象上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
    被唤醒的线程不能马上从wait()方法返回并继续执行,他必须在获取了共享对象的监视器锁之后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程不一定会获取到共享变量的监视器锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (resourceA) {
                        System.out.println("A get resourceA lock");
                        System.out.println("A wait...");
                        resourceA.wait();
                        System.out.println("A end wait");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (resourceA) {
                        System.out.println("B get resourceA lock");
                        System.out.println("B wait...");
                        resourceA.wait();
                        System.out.println("B end wait");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread C = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println("C get resourceA lock");
                    resourceA.notify();
                }
            }
        });
        B.start();
        A.start();
        Thread.sleep(1000);//main线程休眠1s
        C.start();
  • notifyAll()函数
    不同于在共享变量上调用 notify()函数会唤醒被阻塞到该共享变量上的一个线程, notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

1.3等待线程执行终止的join方法

前面介绍的等待通知方法是object类中的方法,而join()方法则是Thread类直接提供的,是无参数且返回值为void的方法。

  • join()方法解决以下场景:就是需要等待某几件事完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。
public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("child threadone over");
            }
        });
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("child threadTwo over");
            }
        });
        threadOne.start();
        threadTwo.start();
        System.out.println("wait all child thread over");
//        threadOne.join();
//        threadTwo.join();
//加入join方法,会输出child threadone/Two over之后再输出main thread over;否则会先输出main thread over
        System.out.println("main thread over");
  • 注:线程A调用线程B的join方法后会被阻塞,当其它线程调用了线程A的interrupt方法中断了线程A时,线程A会抛出InterruptedException异常而返回(即不再被阻塞,继续执行之后的语句)

1.4让线程睡眠的sleep方法

  • Thread类中有一个静态的sleep()方法,当一个执行中的线程调用了Thread的sleep()方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源(比如:锁)还是持有不让出的!【对比wait()方法,当共享变量调用wait方法时,正在执行的线程会让出锁并阻塞】
    指定睡眠时间到了之后该函数会正常返回,线程处于就绪状态然后参与CPU的调度,获取到CPU资源就可继续运行了;
    总结:即没什么逻辑含义,就是物理意义上的延迟一点时间再执行
    如果在睡眠期间其他线程调用该线程的interrupt方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("child thread is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadOne is awake!");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        threadOne.start();
        Thread.sleep(2000);
        threadOne.interrupt();
        Thread.sleep(2000);
        System.out.println("main thread is over");
        //此例可知在一个线程处于睡眠状态时,另一个线程中断了它,会在调用sleep方法处抛出异常并返回;然后另一个线程继续执行

在这里插入图片描述
注:另外需要注意的是,如果在调用 Thread.sleep(long millis)时为 millis 参数传递了一个 负数,则抛出IllegalArgumentException 异常

1.5让出CPU执行权的yield方法

  • 正常情况下当一个线程把分配给自己的时间片使用完之后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
  • 总结: sleep 与 yield 方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度 时就有可能调度到当前线程执行!
  • 线程阻塞挂起就是不参与CPU调度!

1.6线程中断

Java中线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

  • void interrupt()方法:中断线程, 例如,当线程A运行时,线程 B可以调用钱程A的 interrupt()方法来设置线程A的中断标志为 true 并立即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断, 它会继续往下执行。 如果线程A因为调用了wait系列函数、 join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返;
  • boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false;
  • boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false;并且该方法如果发现当前线程被中断,则会清除中断标志,该方法为static方法通过Thread类直接调用(只作用于当前调用线程
//根据中断标志判断线程是否终止的例子
public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println(Thread.currentThread()+" hello");
                }
            }
        });
        threadOne.start();
        Thread.sleep(1000);
        System.out.println("main thread interrupt thread");
        threadOne.interrupt();
        threadOne.join();
        System.out.println("main thread is over");

1.7理解上下文切换

CPU同一时刻只能被一个线程使用,为了给用户感觉多个线程在同时执行,CPU资源的分配采用了时间片轮转的策略,即给每个线程分配一个时间片,线程在时间片内占用CPU资源执行任务;当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换!

  • 线程上下文切换的时机:当前线程的CPU时间片使用完处于就绪状态,当前线程被其他线程中断时。

1.8线程死锁

  • 什么是线程死锁?指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
    在这里插入图片描述
  • 死锁的产生具备一下四个条件:
    1)互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用,其他线程只能阻塞等待;
    2)请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会阻塞,但阻塞的同时并不释放自己已经获得的资源;
    3)不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源;
    4)环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…Tn中 的T0正在等待一个 T1占用的资源,T1正在等待T2占用的资源,…Tn正在等待己被 T0占用的资源。
//形成死锁
public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread()+" get resourceA");
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("waiting get resourceB");
                    //持有资源A了还要继续请求持有资源B
                    synchronized (resourceB){
                        System.out.println(Thread.currentThread()+" get resourceB");
                    }
                }
            }
        });
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread()+" get resourceB");
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("waiting get resourceA");
                    synchronized (resourceA){
                        System.out.println(Thread.currentThread()+" get resourceA");
                    }
                }
            }
        });
        threadOne.start();
        threadTwo.start();
    }
  • 如何避免死锁?想要避免死锁,只需要破环掉至少一个构造死锁的条件即可,目前只有请求并持有和环路等待条件是可以被破坏的!
  • 使用资源申请的有序性原则就可以避免死锁!!!意思是加入线程A和线程B都需要资源1,2,3,4… 时,对资源进行排序,线程A和线程B只有在获取了资源n-1之后才能去获取资源n

1.9守护线程与用户线程

  • 守护线程和用户线程的区别?区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程;也就是只要有一个用户线程还没结束正常情况下JVM就不会退出!【mian线程是一个用户线程,在JVM内部同时还启动了很多守护线程,如垃圾回收线程】
  • 如何设置守护线程?语句:thread1.setDaemon(true);//设置为守护线程
  • main线程运行结束后,JVM会自动启动一个叫作DestroyJava VM的线程, 该线程会等待所有用户线程结束后终止JVM进程。
  • 总结:如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程!

1.10ThreadLocal

同步的措施一般是加锁,那么有没有一种方式可以做到,在创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?

  • ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本;即创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存!
  • 其实每个线程的本地变量不是存放在ThreadLocal实例里面, 而是存放在调用线程的threadLocals变量里面; 也就是说, ThreadLocal类型的本地变量存放在具体的线程内存空间中。 ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来, 当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用!!!

代码

  • 总结 :在每个线程内部都有一个名为threadLocals的成员变量, 该变量的类型为HashMap, 其中key为我们定义的ThreadLocal变量的this引用 , value 则为我 们使用set方法设置的值。 每个线程的本地变量存放在线程自己的内存变量threadLocals中, 如果当前线程一直不消亡, 那么这些本地变量会一直存在, 所以可能会造成内存溢出 , 因此使用完毕后要记得调用ThreadLocal的remove 方法删除对应线程的threadLocals中的本地变量。

在这里插入图片描述

//同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。 
public static ThreadLocal<String> threadlocal = new ThreadLocal<>();//创建线程变量

    public static void main(String[] args) throws InterruptedException {
        threadlocal.set("hello !");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread: " + threadlocal.get());//子线程输出线程变量的值
            }
        });
        thread.start();
        System.out.println("main thread:"+threadlocal.get());//主线程(也是子线程的父线程)输出线程变量的值
    }

2.并发编程的其他基础知识

2.1多线程并发编程

  • 并行和并发:并发是指同一时间段内多个任务同时都在执行,且都没有执行结束;并行是说在单位时间内多个任务同时在执行!
  • 在一个时间段内,宏观来看有多个程序都在活动(每一瞬间只有一个在执行,只是在一段时间有多个程序都执行过)->并发;在每一瞬间,都有多个程序在同时执行,必须有多个CPU才可以 ->并行。
  • 为什么要进行多线程并发编程?因为CPU数量变多了,每个线程可以使用自己的CPU运行,减少了线程上下文切换的开销,所以速度更快。

2.2Java中的线程安全问题

  • 共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源
  • 线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题!!!

2.3Java中共享变量的内存可见性问题

Java内存模型规定,将所有的变量都放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫工作内存,线程读写变量时操作的是自己工作内存中的变量。
在这里插入图片描述

扫描二维码关注公众号,回复: 9292302 查看本文章
  • 当一个线程操作共享变量时,它首先从主内存复制变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完成后将变量值更新到主内存。
  • 对于共享变量X,线程B将X的值修改成了2,但是线程A获取的X的值还是1;这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。【主要是缓存的原因,线程会首先读取自己缓存中的值而不是主内存】
  • 如何解决共享变量内存不可见问题?使用Java中的volatile关键字就可以解决;

2.4Java中的synchronized关键字

  • synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用这看不到的锁被称为内部锁,也叫监视器锁;内置锁是排它锁,也就是一个线程获取这个锁之后,其他线程必须等待该线程释放锁之后才能获取该锁!
  • 由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。
  • synchronized的内存语义:进入synchronized语句块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取;
    退出synchronized语句块的内存语义就是把在synchronized块内对共享变量的修改刷新到主内存中。
    【其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存】

2.5volatile关键字

上面介绍了使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。 对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用 volatile关键字;

  • 该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。 当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。 volatile的内存语义和 synchronized 有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存),读取volatile 变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。

在这里插入图片描述

  • 注:volatile虽然提供了可见性保证,但不能保证操作的原子性。

2.6Java中的原子性操作

所谓原子性 操作,是指执行一系列操作时,这些操作要么全部执行完,要么不执行,不存在只执行其中一部分的情况;

2.7Java中CAS操作

在Java中 , 锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就 是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。 Java提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题, 这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读改写等的原子性问题。 CAS 即Compare and Swap,其是JDK提供的非阻塞原子性操作, 它通过硬件保证了比较更新操作的原子性;
在这里插入图片描述

2.8Unsafe类

JDK的rt.jar包中的Unasfe类提供了硬件级别的原子性操作!!!

2.9Java指令重排序

Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序;这里先看看指令重排序会造成什么影响,如上代码在不考虑、内存可见性问题的情况下 一定会输出 4 ? 答案是不一定,由于代码 (1 ) ( 2 ) ( 3 ) (4)之间不存在依赖关系,所以写线程的代码 (3) (4可能被重排序为先执行(4)再执行(3) ;那么执行(4)后, 读线程可能已经执行了(1)操作, 并且在(3)执行前开始执行(2)操作, 这时候输出结果为0而不是 4。

private static int num = 0;
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {
        Read read = new Read();
        read.start();
        Write write = new Write();
        write.start();
        Thread.sleep(10);
        read.interrupt();
        System.out.println("main exit");
    }

    public static class Read extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                if (ready) //(1)
                    System.out.println(num+num);//(2)
                System.out.println("read thread......");
            }
        }
    }
    public static class Write extends Thread {
        @Override
        public void run() {
            num=2;//(3)
            ready=true;//(4)
            System.out.println("write thread set over......");
        }
    }

2.10伪共享

当 CPU 访问某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个 Cache 行大 小的内存复制到 Cache中 。 由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache 行中 。 当多个线程同时修改一个缓存行里面的多个变量时, 由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享!!!
在这里插入图片描述

  • 如何避免伪共享?在 JDK 8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中;

2.11锁的概述

  • 乐观锁和悲观锁
    悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以 在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。 悲 观锁的实现往往依靠数据库提供的锁机制,即在数据库中 ,在对数据记录操作前给记录加 排它锁。 如果获取锁失败, 则说明数据正在被其他线程修改, 当前线程则等待或者抛出异 常。 如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。
    乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测!【有点CAS操作的意思】
  • 公平锁和非公平锁
    根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁 的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。 而非公平锁则在运行时闯入,也就是先来不一定先得。【在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销】
    在这里插入图片描述
  • 独占锁和共享锁
    根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁!!!
    独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读 操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线 程必须等待当前线程释放锁才能进行读取;
    共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
  • 可重入锁
    当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线 程再次获取它自己己经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可 重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严 格来说是有限次数)地进入被该锁锁住的代码;
  • 自旋锁
    当前线程在获取锁时,如果发现锁已经被其他线程占有, 它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可 以使用 -XX:PreB lockS pinsh 参数设置该值),很有可能在后面几次尝试中其他线程己经释放了锁。 如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。 由此看来自 旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白白浪费了

发布了4 篇原创文章 · 获赞 2 · 访问量 397

猜你喜欢

转载自blog.csdn.net/weixin_38172229/article/details/104260362