多线程高并发相关


@toc

ThreadLocal中只能线程独享

在这里插入图片描述

看看set方法的源码
在这里插入图片描述

ThreadLocal为啥要用弱引用

在这里插入图片描述
看看mao的set方法中的源码
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

可以看到ThreadLocal有一个强引用tl指向它,还有一个弱引用key指向它,假如key是强引用,我们一旦把tl=null了。然后tl和ThreadLocal之间的引用就断了,Key此时是强引用还是指向ThreadLocal的,它就不能被GC。这就造成了内存泄露。
是按照要求设计的弱引用,此时tl的强引用一断开,key又是个弱引用,垃圾收集器一看到弱引用就给它收集了,就会减少内存泄漏的机会,但是还是有,详情见下面

就是
在这里插入图片描述
中的key是个弱引用,当它被GC了后,key没有了指向 值为null,但是还是存在于Map中,无法访问占用着地方,会导致内存泄漏。所以set后要用tl.remove()来释放。

java和Go中用户线程和内核线程的比例关系

java 是用户线程和内核线程1:1
Go 是m:n 并且m>n
在这里插入图片描述

轻量级锁和重量级锁的优缺点

轻量级锁: 不需要操作系统调度的锁,比如自旋锁。
重量级锁: 需要进入队列等待操作系统调度的锁。

锁种类 优点 缺点 适用场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,会一直自旋会消耗CPU 追求相应速度,同步块执行速度非常快,线程数少
重量级锁 线程竞争f不适用自旋,不会消耗CPU 线程阻塞,相应时间缓慢 追求吞吐量,同步块执行速度较长,线程数多

AtomicInteger讲解

首先来看一个小程序

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;

public class TestAtomicInteger {
    
    private static int m=0;

    public static void main(String[] args) {
        Thread[] threads=new Thread[100];
        final CountDownLatch latch=new CountDownLatch(threads.length);

        for (int i = 0; i <threads.length ; i++) {
            threads[i] =new Thread(()->{
                for (int j = 0; j <10000 ; j++) {
                    m++;
                }
                latch.countDown();
            });
        }

        Arrays.stream(threads).forEach((t)->{t.start();});

        latch.await();
        System.out.println(m);
    }
}

简单描述一下,起了100个线程,每个线程对m加1w,m的初始值为
0。如果正常运行 答案应该是100w。
先来看看不加锁的结果
不加锁
然后给加个锁,我们可以实现目标,但是如果不加锁,怎么实现呢?
用AtomicInteger

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicInteger {
    
//    private static  int m=0;

    private static AtomicInteger m=new AtomicInteger(0);//修改处

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads=new Thread[100];
        final CountDownLatch latch=new CountDownLatch(threads.length);

        for (int i = 0; i <threads.length ; i++) {
            threads[i] =new Thread(()->{
                for (int j = 0; j <10000 ; j++) {
//                    m++;
                    m.getAndIncrement();
                }
                latch.countDown();//修改处
            });
        }

        Arrays.stream(threads).forEach((t)->{t.start();});

        latch.await();
        System.out.println(m);
    }
}

AtomicInteger

其实 m.getAndIncrement();里面用到了CAS。

unsafe类

atomicInteger.getAndIncrement();
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
this-当前对象
valueOffset-内存偏移量(内存地址)
1:要修改的值

Unsafe到时候就是根据当前对象(this)的内存偏移量(valueOffset)来获取数据的

为什么AtomicInteger能解决i++多线程下不安全的问题,靠的是底层的Unsafe类,那么Unsafe类到底是什么?我们来看一下

在这里插入图片描述
在这里插入图片描述

CAS的原理

CAS是什么

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

this.getIntVolatile(var1,var2) 获取var1这个对象在var2这个地址上的值
getandIncrement()方法底层调用的是Unsafe类的getAndAddInt()方法,底层是CAS思想,如果比较成功,加1;否则,重新获得再比较,直至成功

在这里插入图片描述

CAS修改值时候的原子性问题

原子性问题

可以打开源码看一下
在这里插入图片描述
unsafe.cpp:
在这里插入图片描述
atomic_linux_x86.inline.hpp 93行
在这里插入图片描述
然后一路跟踪下去可以得到最终实现:
lock cmpxchg 指令
在这里插入图片描述


CAS的缺点

首先看一个知识点:原子引用

即AtomicInteger可以让我们操作Integer类型的数字,其余的User等自定义类型要使用原子引用AtomicReference

@Getter
@ToString
@AllArgsConstructor
class User {
    String username;
    int age;
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User zs = new User("zs", 20);
        User ls = new User("lisi", 11);
        User ww= new User("ww", 42);

        AtomicReference<User> atomicReference = new AtomicReference<User>();//现在的主物理内存的值是new AtomicReference<User>();
        atomicReference.set(zs);//让主物理内存的new AtomicReference<User>();变为zs这个user

        System.out.println(atomicReference.compareAndSet(zs, ls)+"\t"+atomicReference.get().toString());//即如果主内存的值还是zs,那么就把它修改为ls

        System.out.println(atomicReference.compareAndSet(zs, ww)+"\t"+atomicReference.get().toString());//如果主存的值还是zs,就把它改为ww。
    }
}

①循环时间长开销很大

在这里插入图片描述

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

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

③ABA问题

ABA问题
解决办法
(版本号 AtomicStampedReference),基础类型简单值不需要版本号。

要实现这个版本号,需要借助:时间戳原子引用,下面用一个例子来看一下。

/**
 * ABA问题的解决     AtomicStampedReference
 */
public class ABADemo {

    static AtomicReference atomicReference = new AtomicReference(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100, 1);

    public static void main(String[] args) {
        System.out.println("-----------------ABA问题的产生--------------------");
        new Thread("t1"){
            @Override
            public void run() {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 100);
            }
        }.start();

        new Thread("t2"){
            @Override
            public void run() {
                try {
//线程t2休眠1秒钟,确保t1完成一次ABA操作
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
            }
        }.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("-----------------ABA问题的解决--------------------");

        new Thread("t3"){
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();//拿到版本号
                System.out.println(getName() + "\t第一次版本号:" + stamp);
                try {
//t3线程休眠1秒,确保t4也拿到初始的版本号
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                System.out.println(getName() + "\t第二次版本号:" + atomicStampedReference.getStamp());
                atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                System.out.println(getName() + "\t第三次版本号:" + atomicStampedReference.getStamp());
            }
        }.start();

        new Thread("t4"){
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();
                System.out.println(getName() + "\t第一次版本号:" + stamp);
                try {
//t4线程休眠3秒,确保t3完成一次ABA操作
                    sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //如果主存中的值还是100,且版本号还是为初始版本号stamp:1,那么就把100修改为2020
                boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
                System.out.println(getName() + "\t是否修改成功," + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
                System.out.println(getName() + "\t当前实际最新值:" + atomicStampedReference.getReference());
            }
        }.start();

    }
}

在这里插入图片描述

锁升级、偏向锁

偏向锁: 是一把必轻量级锁还要轻的锁,严格来说并不能算是一把锁。比如下面这个例子,最开始只有一个人,它使用偏向锁,根本不用抢,只是贴个条子而已。

理论一点的解释:

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

在这里插入图片描述

锁升级

有些人就会有疑问,你这个偏向锁又不可以真正的上锁,这不是多此一举么?为啥不直接上锁?

比如有些方法 百分之七八十的时间都是一个线程来访问,如果直接加锁的话,每次那个单独的线程来,都要竞争一下锁。为啥不用个偏向锁,只有一个线程来的话直接贴个条子就可以了,就不用竞争了,减少了上锁的开销。只有在有人来了,才开始竞争锁,进行锁升级

锁的四种状态

我们要查看锁的四种状态要借助JOL这个类

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
    </dependencies>

来看一下代码

import org.openjdk.jol.info.ClassLayout;

public class TestJOL {
    public static void main(String[] args) {
        Object o = new Object();

        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

在这里插入图片描述
我们给o上把锁,再来看一下markword部分的内容

   synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }

在这里插入图片描述


用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位

1、Object o = new Object()
锁 = 0 01 无锁态
注意:如果偏向锁打开,默认是匿名偏向状态


2、默认synchronized(o)
00 -> 轻量级锁
默认情况 偏向锁有个时延,默认是4秒
why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

3、如果有线程上锁,上偏向锁指的就是,把markword的线程ID改为自己线程ID的过程,偏向锁不可重偏向、批量偏向、批量撤销。下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁偏向加锁的第一个线程,hashCode备份在线程栈上,线程销毁,锁降级为无锁。偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁;

4、如果有线程竞争,撤销偏向锁,升级轻量级锁,线程在自己的线程栈生成LockRecord,用CAS操作将markword设置为指向自己这个线程的LockRecord的指针,设置成功者得到锁;自旋锁在JDK1.4.2 中引入,使用“-XX:+UseSpinning”来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁),JVM自己控制。


5、自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源;

6、如果竞争加剧:有线程超过10次自旋(通过“-XX:PreBlockSpin”设置自旋次数)或者自旋线程数超过CPU核数的一半,升级重量级锁需要向操作系统申请资源(互斥锁,linux mutex),CPU从3级->0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间,用户态与内核态的切换需要消耗资源。

JDK11,偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数,而JDK8默认对象头是无锁。

缓存行

首先要了解多核cpu的三级缓存分布图
在这里插入图片描述

根据时间局部性原理、空间局部性原理,我们从缓存往内存中读数据的时候一次读一个缓存行大小的数据,一个缓存行大小64Byte

在这里插入图片描述

下面来看两个例子:

import com.sun.deploy.pings.Pings;

import java.util.concurrent.CountDownLatch;
/**
 * 线程t1把T【0】修改一亿次
 * 线程t2把T【1】修改一亿次
 * 
 * 输出执行完这个程序的时间
 */
public class CacheLine01 {
    public  static  long COUNT=1_0000_0000L;//long 类型占8个字节

    private  static  class  T{
        public volatile  long x=0L;
    }

    public  static  T[] arr=new T[2];

    static {
        arr[0]=new T();
        arr[1]=new T();
    }


    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch=new CountDownLatch(2);

        Thread t1=new Thread(()->{
            for (long i = 0; i <COUNT ; i++) {
                arr[0].x=i;
            }

            latch.countDown();
        });

        Thread t2=new Thread(()->{
            for (long i = 0; i <COUNT ; i++) {
                arr[1].x=i;
            }

            latch.countDown();
        });

        final long start=System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime()-start)/100_0000);
    }
}

在这里插入图片描述

import java.util.concurrent.CountDownLatch;

public class CacheLine02 {

    public  static  long COUNT=1_0000_0000L;//long 类型占8个字节

    private  static  class  T{
        public  long  p1,p2,p3,p4,p5,p6,p7;
        public volatile long x=0L;
        public  long  p9,p10,p11,p12,p13,p14,p15;


    }

    public  static  CacheLine01.T[] arr=new CacheLine01.T[2];

    static {
        arr[0]=new CacheLine01.T();
        arr[1]=new CacheLine01.T();
    }


    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch=new CountDownLatch(2);

        Thread t1=new Thread(()->{
            for (long i = 0; i <COUNT ; i++) {
                arr[0].x=i;
            }

            latch.countDown();
        });

        Thread t2=new Thread(()->{
            for (long i = 0; i <COUNT ; i++) {
                arr[1].x=i;
            }

            latch.countDown();
        });

        final long start=System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime()-start)/100_0000);
    }
}

在这里插入图片描述

之所以2比1快是因为,2把T[0] T[1]用填充的方式放到了两个不同的缓存行。

如果是一个缓存行,那么t1先对T[0]做了修改后为了保持缓存行一致性,就会通知t2对T[0]进行更新。很耗时。如果在不同的缓存行就没有这个问题

缓存一致性

缓存一致性协议:为了保持各个缓存行之间的一致性而采用的一种协议
在这里插入图片描述

volatile

volatile是java虚拟机提供的轻量级同步机制,注意volatile 不保证原子性

主要作用:①线程可见性②禁止指令重排序

谈谈JMM

在这里插入图片描述
在这里插入图片描述

可见性

通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,
这种工作内存与主内存同步延迟现象就造成了可见性问题

解决可见性的原理

在这里插入图片描述

lock 指令会让其他CPU的缓存行无效(总线嗅探机制),然后CPU1把缓存行地址给锁了,其他CPU去缓存行读取数据,发现地址被锁就一直等待,此时CPU1把更新后的值写回主存,CPU2 此时可以访问,发现缓存行没有,就去主存读。

演示可见性代码

import java.util.concurrent.TimeUnit;

public class TestVolatile {

    /*volatile*/  boolean running=true;
    void m(){
        System.out.println("m启动");
        while (running){
        }
        System.out.println("m结束");

    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile testVolatile = new TestVolatile();

        new Thread(testVolatile::m,"1").start();

        TimeUnit.SECONDS.sleep(1);
        testVolatile.running=false;
    }
}

按道理说我们把running值改为false了就该结束了,但是程序依然还是死循环。
在这里插入图片描述
但是加上volatile后就可以结束了。
因为volatile修饰的内存,如果做了改变,那么其他线程马上可见!

注意:如果不加volatile,但是在死循环中写一个输出语句,也是可能结束循环的,因为输出语句中有可能会有主存数据和本地缓存同步的过程。但是你写别的语句就不一定能结束程序了

原子性

原子性 即不可分割。

演示原子性代码

见上面的标题为:AtomicInteger讲解的内容

有序性

在这里插入图片描述

重排案例

在这里插入图片描述

线程操作资源类,线程1访问method1,线程2访问method2,正常情况顺序执行,a=6
多线程下假设出现了指令重排,语句2在语句1之前,当执行完flag=true后,另一个线程马上执行method2,a=5

禁止重排的原理

volatile实现禁止指令重排优化,从而避兔多线程环境下程序岀现乱序执行的现象
先了解一个概念,内存屏障( Memory Barrier) 又称内存栅栏,是一个CPU指令,它保证特定操作的执行顺序,以及影响一些可见性。它其实就是一条LOCK指令,你执行代码的时候最终会变成一条一条指令去执行,你插入这个内存屏障之后,它会由操作系统去帮助我们避免这个屏障前后的指令顺序进行交换。

分为读屏障,写屏障
在这里插入图片描述

DCL单例模式要不要加volatile

前提知识:CPU的指令重排,乱序执行

在这里插入图片描述

前提知识:解释一下对象的创建过程

public class T {
    public static void main(String[] args) {
        int m=8;
    }
    T t=new T();
}

用jclasslib来看一下
在这里插入图片描述

前提知识:单例模式讲解

单例模式:只允许你new出来一个Class类的对象

public class TestSingleton {
    private  static  TestSingleton INSTANCE;//上来先new个对象

    private  TestSingleton(){};//再将构造方法私有化,让别人不能通过构造方法创建对象

    public static  TestSingleton getInstance(){
        if (INSTANCE==null){//确保这个类还没有对象,才给它new一个对象
            INSTANCE=new TestSingleton();
        }

        return  INSTANCE;}//别人要获得我这个类的对象只有通过getInstance()

    public static void main(String[] args) {
        TestSingleton m1= TestSingleton.getInstance();
        TestSingleton m2= TestSingleton.getInstance();
        System.out.println(m1==m2);
    }
}

在这里插入图片描述

如果是多线程下单例模式容易出现问题:
在这里插入图片描述

加一个synchronized(不能解决问题)
在这里插入图片描述
在这里插入图片描述

⭐回答DCL到底要不要加volatile

在这里插入图片描述
在这里插入图片描述

所以我们需要加volatile ,用volatile修饰的那块内存空间,也即是new出来的那个对象,在那个对象身上执行的指令禁止乱序

总结

在这里插入图片描述
在这里插入图片描述

禁止指令重排序的实现原理:內存屏障

JVM层面的内存屏障只是一种规范,具体的每个硬件的实现都不同。屏障两边的指令不可以重排!保障有序!

在这里插入图片描述

补充一个概念:as if serial: 不管如何重排序,单线程执行结果不会改变


hotspot实现内存屏障
bytecodeinterpreter.cpp

int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
              OrderAccess::fence();//注意这行
            }

orderaccess_linux_x86.inline.hpp

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

就是由lock; addl $0,0(%%esp)这条指令实现的。
addl $0,0(%%esp)是个空语句,没有作用。单纯为了给lock后面补充位置。因为lock后面必须跟一条语句。


LOCK用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。

我们应该能回答出来的问题

在这里插入图片描述


单例模式

在这里插入图片描述
在这里插入图片描述

饿汉式:直接创建对象,不存在线程安全问题

直接实例化

/*
饿汉式:
    直接创建实例对象,不管你是否需要它

    要点:
        ①构造器私有化
        ②自行创建对象,并且用静态变量保存
        ③对外提供这个实例
        ④强调这是一个单例我们用final修饰
 */
public class Singleton01 {
    public static  final Singleton01 INSTANCE=new Singleton01();
    private Singleton01(){

    }
}

枚举式

/*
    枚举类型:表示该对象的实例是有限的几个。
    我们定义为一个,那它就成为单例
 */
public enum  Singleton02 {
    INSTANCE
}

静态代码块饿汉式(适合复杂实例化)

public class Singleton03 {
    public static  final Singleton03 INSTANCE;
    private  String info;//想要构造对象的时候有一些属性

    static {
        INSTANCE=new Singleton03("想附加的一些信息");
    }
    private Singleton03(String info){
        this.info=info;
    }


}

懒汉式:延迟创建对象

线程安全(适用于多线程)

/*
懒汉式:
    延迟创建这个对象,当你需要的时候才会创建

    要点:
        ①构造器私有化
        ②自行创建对象,并且用静态变量保存
        ③对外提供一个方法获取这个实例,此时就不能用final修饰INSTANCE了,因为最开始没初始化,final要修饰初始化了的
 */
public class Singleton04 {
    public static Singleton04 INSTANCE;

    private Singleton04() {

    }

    public Singleton04 getInstance() {
        Object o = new Object();
        //为了多线程时候还保证为单例模式所以要用DCL
        if (INSTANCE == null) {
            synchronized (o) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton04();
                }
            }
        }
        return INSTANCE;
    }
}

静态内部类形式(适用于多线程)

/*
在内部类被加载和初始化时,才创建 INSTANCE实例对象
静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的(在使用时才加载,这里我们在return Inner.INSTANCE;才使用它)。
因为是在内部类加载和初始化时,创建的,因此是线程安全的

 */
public class Singleton05 {

    private Singleton05() {

    }

   private  static  class Inner{
        private static final  Singleton05 INSTANCE=new Singleton05();
   }

   public static Singleton05 getInstance(){
        return Inner.INSTANCE;
   }
}

ArrayList是线程不安全,请编码一个不安全的案例并给出解决方案

1、故障现象(不安全的例子)

三十个线程 同时往一个list中添加一个随机的字符串。会报java.util.ConcurrentModificationException

public class ListNotSafe {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

结果

2、导致原因

并发争抢修改导致,参考我们的花名册签名情况。
一个人正在写入,另外一个同学过来抢夺,笔在花名册上画了一长条线,导致数据不一致异常。产生并发修改异常

3、解决方案

①用new Vector<>
在这里插入图片描述
②用Collections.synchronizedList()
在这里插入图片描述
③用new CopyOnWriteArrayList<>()
在这里插入图片描述
我们详细看一下第三种解决方案:写时复制。
这是JUC下面的一个类。我们来看一下它的add()
在这里插入图片描述

Copyonwrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器 object[ ]添加,而是先将当前容器 object[ ]进行copy
复制出一个新的容器 object[ ] newELements,然后新的容器 object[ ] newELements里添加元素

添加完元素之后,
再将原容器的引用指向新的容器 setArray( newELements);。这样做的好处是可以对 Copywrite容器进行并发的读,
而不需要加锁,因为当前容器不会添加任何元素。所以 CopyonWrite容器也是一种读写分离的思想,读和写不同的容器


我们可以用一个例子来理解。现在还是一份花名册要签到。小明首先拿到了笔要签到。他不直接写在花名册上,而是把花名册复制一份,把原本的那份贴在黑板上。别人可以来看原来的那一份,而他自己却往复制的那一份上面去写上自己的名字,写完以后再通知别人,不要看之前的那个老版本了,来看我这个新版本

HashSet和HashMap也是线程不安全的

可以用上面的ArrayList的例子来证明。
解决方法:
① 对于HashSet:new CopyOnWriteArrayHashSet<>()
②对于HashMap:new ConcurrentHashMap<>()

Java锁

synchronized和lock有什么区别?用lock有什么好处?

1.原始构成
synchronized是关键字属于JVM层面
monitorenter(底层通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或方法中才能调用wait/notify等方法)
monitorexit
lock是具体类(java.util.concurrent.locks.lock)是api层面的锁

2.使用方法
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
ReentrantLock则需要用户去手动释放锁,若没有主动释放,就可能出现死锁

3.等待是否可以中断
synchronized不可中断,除非抛出异常或正常执行结束
ReentrantLock可中断, 设置超时方法tryLock(long timeout, TimeUnit unit)
lockInterruptibly()放代码块中,调用interrupt()方法可中断

4.加锁是否公平
synchronized非公平锁
ReentrantLock两者都可,默认是非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

5.锁绑定多个条件condition
synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized随即唤醒一个或者全部唤醒

公平锁和非公平锁

公平锁 是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁是 指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先申请的线程优先获取锁。
在高并发的情况下,有可能会造成优先级反转或者饥饿现象

并发包中 ReentrantLock的创建可以指定构造函数的 boolean类型来得到公平锁或非公平锁,默认是非公平锁
RrentrantLock的原码

二者区别

公平锁: 就是很公平,在并发环境中,每个线程在获取锁时会先査看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁: 比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

Java ReentrantLock而言,
通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大

对于 Synchronized而言,也是一种非公平锁

可重入锁(递归锁)

是什么
可重入锁(也叫做递归锁):假如有一个方法A上锁了。而方法里又掉用了一个上锁了的方法B,此时一个线程拿到了A的锁,那么它就拥有了该方法代码块内的所有代码,包括B。虽然B已经上锁了,但是可以被该线程拿到这个锁。

下面是官方一点的说法:
可重入锁指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁也即是说,线程可以进入任何一个它已经拥有的锁 所同步着的代码块

ReentrantLock/Synchronized就是一个典型的可重入锁

可重入锁的最大作用就是避免死锁

代码演示Synchronized是可重入锁

class Phone {//资源类
    public synchronized void sendEmail() {
        System.out.println(Thread.currentThread().getName() + "正在发邮件");
        sendSMS();
    }

    public synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getName() + "正在发短信");
    }
}

public class reentrantLockForSynchronized {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "t1").start();
        new Thread(() -> {
            phone.sendEmail();
        }, "t2").start();
    }
}

结果

代码演示ReentrantLock是可重入锁

class Phone2 {//资源类
    Lock lock = new ReentrantLock();

    public void sendEmail() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在发邮件");
            sendSMS();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void sendSMS() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在发短信");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class reentrantLockForReentrantLock {
    public static void main(String[] args) {
        Phone2 phone2 = new Phone2();
        new Thread(() -> {
            phone2.sendEmail();
        }, "t3").start();
        new Thread(() -> {
            phone2.sendEmail();
        }, "t4").start();
    }
}

在这里插入图片描述

自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

这块儿就用到了自旋锁

手写一个自旋锁

/**
 * 手写一个自旋锁
 * 实现方法其实就是CAS+while循环
 */
public class SpinLockDemo {
    //原子引用,最开始的时候AtomicReference是空的,只有当某个具体的线程来的时候才会变成那个具体的线程。
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();//拿到现在进入方法的线程
        System.out.println("线程" + thread.getName() + "进入了");
        while (!atomicReference.compareAndSet(null, thread)) {//利用CAS进行比较,如果内存中的AtomicReference对象为空,就把现在这个线程放进去


        }
    }

    public void MyUnLock() {//自己写的解锁方法
        Thread thread = Thread.currentThread();//拿到现在进入方法的线程
        atomicReference.compareAndSet(thread, null);//如果现在AtomicReference对象是自己这个线程,那么就要释放掉,让AtomicReference变成空。
        System.out.println("线程" + thread.getName() + "执行了解锁方法");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLock = new SpinLockDemo();

        new Thread(() -> {
            spinLock.myLock();//用自己的方法加锁
            try {
                TimeUnit.SECONDS.sleep(5);//睡5s,模拟加锁了进行操作的过程消耗的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.MyUnLock();//解锁
        }, "AA").start();

        TimeUnit.SECONDS.sleep(2);//睡2S保证上面的操作执行完毕
        new Thread(() -> {
            spinLock.myLock();//用自己的方法加锁
            spinLock.MyUnLock();//解锁
        }, "BB").start();
    }
}

运行结果

读写锁

独占锁(写锁): 指该锁一次只能被一个线程所持有。对 Reentrantlock和 Synchronized而言都是独占锁。
共享锁(读锁): 指该锁可被多个线程所持有。
对 ReentrantReadWritelock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的。
读写,写读,写写的过程是互斥的。

代码演示

//如果不加读写锁会发生  还没有写入成功就进行读取的效果。。。
class MyCache{//资源类

    //    volatile的作用就是当一个线程更新某个volatile声明的变量时,会通知其他的cpu使缓存失效,从而其他cpu想要做更新操作时,需要从内存重新读取数据。
    private volatile Map<String,Object> map=new HashMap<>();


    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();

    public void  put(String key,Object value) throws InterruptedException {
        readWriteLock.writeLock().lock();//写锁
        try{
            System.out.println(Thread.currentThread().getName()+"\t 开始写入"+key);
            TimeUnit.MICROSECONDS.sleep(3);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+"\t 写入完成");

        }catch (Exception e){

        }finally {
            readWriteLock.writeLock().unlock();
        }
    }
    public void get(String key) throws InterruptedException {

        readWriteLock.readLock() .lock();//读锁
        try{
            System.out.println(Thread.currentThread().getName()+"\t 开始读取");
            TimeUnit.MICROSECONDS.sleep(3);
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName()+"\t 读取完成"+result);

        }catch (Exception e){

        }finally {
            readWriteLock.readLock().unlock();
        }


    }
}


public class ReadWriteLockDemo {
    /**
     * 读写锁,只能一个人写,但是可以同时读。
     */

    public static void main(String[] args) {
        MyCache myCache=new MyCache();

        for (int i=0;i<5;i++){
            final  int tempInt=i;
            new Thread(()->{
                try {
                    myCache.put(tempInt+"",tempInt+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }


        for (int i=0;i<5;i++){
            final  int tempInt=i;
            new Thread(()->{
                try {
                    myCache.get(tempInt+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

运行结果

从结果可以看出来,在进行写的时候是不允许“加塞”的。只有开始写入–>写入完成,才可以允许别的操作。但是读是没有限制的。

由countDownLatch引出Enum的用法

现在要用countDownLatch实现灭掉六国才能统一天下的逻辑,首先不适用enum类。

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t 国,被灭了");
            countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();
        System.out.println("秦国灭掉了六国,一统天下!");
    }
}

在这里插入图片描述
如果我们要一号代表燕国,二号代表赵国…这样就要写很多if判断if(i==1) return燕国…这样十分的不方便。

现在引入Enum
CountryEnum

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum CountryEnum {
    ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FVIE(5, "魏"), SIX(6, "韩");
    private Integer countryCode;
    private String countryName;


    public static CountryEnum foreach_country(int countryCode) {
        CountryEnum[] values = CountryEnum.values();
        for (CountryEnum elment : values) {
            if (countryCode == elment.getCountryCode()) {//如果在枚举中存在这个国家编码
                return elment;//返回这个国家
            }
        }
        return null;
    }
}
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t 国,被灭了");
            countDownLatch.countDown();
            }, CountryEnum.foreach_country(i).getCountryName()).start();
        }

        countDownLatch.await();
        System.out.println("秦国灭掉了六国,一统天下!");
    }
}

在这里插入图片描述

这样就可以根据传过来的id得到国家的名字了。

阻塞队列知道吗?

基础知识

阻塞队列的基础知识

用阻塞队列实现生产者消费者模型3.0版本

前面两个版本的架构

前面两个版本的实现


新版本实现:

class MyResource {
    private volatile boolean FLAG = true;//默认开启,进行消费和生产
    private AtomicInteger atomicInteger = new AtomicInteger();//多线程中不用i++等
    BlockingQueue<String> blockingQueue = null;//我们这里不直接指定是什么类型的阻塞队列,到时候传什么过来我们new出来什么

    public MyResource(BlockingQueue<String> blockingQueue) {//构造方法,用于生成特定的Queue
        this.blockingQueue = blockingQueue;//通过反射拿到具体是构造出来了一个什么类型的queue
        System.out.println(blockingQueue.getClass().getName());
    }

    public void MyProd() throws InterruptedException {//生产者
        String data = null;
        boolean retValue;
        while (FLAG) {
            data = atomicInteger.incrementAndGet() + "";//++i。  因为offer() 要传入String类型的,所以要拼接字符串把int转化为String
            retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);//把生产的东西加入到阻塞队列中去
            if (retValue) {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "失败");
            }
            Thread.sleep(1000);
        }
        //如果Flag为false才会来到这块儿
        System.out.println(Thread.currentThread().getName() + "生产动作结束");

    }

    public void MyConsumer() throws InterruptedException {//消费者
        String result = null;
        while (FLAG) {
            result=blockingQueue.poll(4, TimeUnit.SECONDS);//4秒钟没有拿到东西就退出
            if (result == null || result.equalsIgnoreCase("")) {//代表没有取到东西
                FLAG = false;//把falg置为false,来跳出循环,结束消费
                System.out.println(Thread.currentThread().getName() + "\t 超过4s没有拿到东西,消费者退出");
                System.out.println();
                System.out.println();
                System.out.println();
                return;//退出判断。 结束正在运行的函数
            }
            System.out.println(Thread.currentThread().getName() + "\t 消费队列" + result + "成功");

        }

    }


    public void MyStop() {//自己的终止方法
        this.FLAG = false;
    }
}

public class ProdConsumer_BlockingQueue_Demo {

    public static void main(String[] args) {
        MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
            try {
                myResource.MyProd();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Prod").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
            System.out.println();
            try {
                myResource.MyConsumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Consumer").start();


        //暂停一会儿线程,让他们上面可以 “玩” 5s
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println();
            System.out.println();

            System.out.println("5s时间到,大老板main线程叫停,所有活动结束");
            myResource.MyStop();//用来改变标识位进而结束活动

    }
}

运行结果

线程池用过吗?ThreadPoolExecutor他谈你的理解?

对线程池的理解

线程池用过吗?生产上你如何设置合理参数

CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池

IO密集型

①由于I/O密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2

②I/O密集型,即该任务需要大量的I/O,即大量的阻塞。
在单线程上运行I/O密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在I/O密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
所以在I/O密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式: CPU核数/1-阻塞系数
阻塞系数在0.8~09之间
比如8核CPU:8/1-0.9=80个线程数

死锁编码及定位分析

产生死锁的主要原因

① 系统资源不足②进程运行推进的顺序不合适③资源分配不当
在这里插入图片描述

代码示例

public class deadLock {
    public static Object lockA = new Object();
    public static Object lockB = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t持有" + lockA + ",等待" + lockB);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("线程A完成");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t持有" + lockB + ",等待" + lockA);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA) {
                    System.out.println("线程B完成");
                }
            }
        }, "B").start();
    }
}

在这里插入图片描述

解决

①jps命令定位进程号
在这里插入图片描述
②jstack找到死锁查看
在这里插入图片描述


文章作者: fFee-ops
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 fFee-ops !
评论
  目录