为什么重新new两个线程线程号相同_多线程知识点小结

224d732d9036d22251edc5e63dff0c7e.png

多线程基础知识

213e5fc5e172b42eadaf2bf07f77b97d.png

1. volatile关键字

JDK提供的一种轻量级的同步机制。保证了可见性,有序性,不保证原子性

1.1 什么是可见性

JMM模型,当线程操作主内存中的变量的时候,首先复制一份变量到线程的工作内存中,然后更新结束以后,就将这个变量的值更新到主内存中。

而当多个线程进行操作的时候,可能会出现不同线程之间读取的值不能实时更新。所以需要提供一种机制,当线程对变量进行更新的时候,去通知其他线程该变量的值已经被更新,从主内存中重新获取该变量的值。

volatile关键字就提供了这种机制,当变量被这个关键字修饰以后,变量的值被修改,其他线程就会重新读取这个变量的值。

代码演示可见性:

public class VolatileDemo {
    private volatile int num = 1 ;

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(()->{try {
            TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
            volatileDemo.num = 10 ;
        }).start();
        while(volatileDemo.num == 1){

        }
        System.out.println("值已经被修改为:"+volatileDemo.num);
    }
}

num被修改为10以后主线程跳出循序,打印输出。实现可见性。

1.2 什么是原子性

先来思考一下数据库事务中的原子性。一次事务中的SQL只能全部成功或者全部失败。

Java中的原子性,在一个线程中,原子操作不能被阻塞或者中断。

不保证原子性演示

public class VolatileDemo {
    private volatile int num =0 ;

    public void addPlus(){
        num++;
    }
    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        for(int i = 0 ; i < 20 ; i++){
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    volatileDemo.addPlus();
                }
            },"线程为:"+i).start();
        }
        //后台默认存在两个线程,一个main,一个GC
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"t finallyt"+volatileDemo.num);
    }
    
   //输出 main  finally 19689

为什么会出现这种情况:

1197af0fa2186fd57d549b6566528467.png

假设现在线程一在执行num++,当他执行完++操作以后,需要写入主内存的时候,线程被挂起,线程二执行。

线程二执行完++操作以后写回主内存,正要通知其他线程值要修改的时候,线程一执行,将这个值又修改为了1,导致一次加失效。

num++底层实现

通过反编译获取

  2: getfield      #2                  // Field num:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field num:I

首先获取字段,然后拿到一个常量值1,对字段加运算,写会主内存

在这个步骤中就会出现上面的情况。

解决不保证原子性

通过添加锁

通过原子类实现

/**
 * 原子类实现原子性
 */
class MyData{
    //默认值为0
    volatile AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlus(){
        //先++
        atomicInteger.incrementAndGet();
        //先获取
//        atomicInteger.getAndIncrement();
    }
}
public class atomicDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        for(int i = 0 ; i < 20 ; i++){
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    myData.addPlus();
                }
            },"线程为:"+i).start();
        }
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"t value:t"+myData.atomicInteger.get());
    }
}

1.3 什么是有序性

同样,首先回忆一下MySql体系架构中存在一个优化器,它会优化开发者自己的SQL语句。即MySql引擎不会按照开发者自己书写的逻辑进行执行,而是有一个优化的过程。

类似,Java也有这样的机制,当多个线程同时执行的时候,cpu不一定按照开发者自己书写的顺序执行任务。这个时候代码的执行顺序就会被打乱。此时代码有序性不能保证。

但是在Java中指令重排中有一种机制指令重排中不能存在依赖关系

代码解读:

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

代码中的执行顺序可能会出现:

  • 1234
  • 2134
  • 1324
  • 1243或者3421这些情况由于指令重排需要遵循规则数据之间不存在依赖关系所以这些情况不会出现。

代码解读

创建一个对象需要三步
    首先在堆中分配一块内存区域            //1
    在这个内存区域中创建对象              //2
    将这个内存区域的地址赋值给这个对象的变量 //3

这个时候如果不添加volatile关键字,就会出现执行顺序为132的情况(2,3之间没有依赖关系),所以会出现线程读取的对象地址为null的情况。

这两案例就是由于指令重排而引起的错误

1.4 volatile怎么实现的有序性和可见性

底层是内存屏障

内存屏障的作用有两个:第一、保证特定操作的执行顺序;第二、保证某些变量的内存可见性

9ee078c5a72bf4bc027a38c99b5332a3.png

1.5 DCL

DCL(Double Check Lock)双端检查加锁。

看一下单例模式的实现:

class MyInstance {
    private static volatile MyInstance myInstance = null ;
    private MyInstance(){};
    public static MyInstance getInstance(){
        if(myInstance ==null){
            synchronized (MyInstance.class){
                if(myInstance ==null){
                    myInstance = new MyInstance();
                }
            }
        }
        return myInstance ;
    }
}

两个问题:第一、为什么使用volatile关键字;第二、为什么使用双重检查加锁

第一个问题:使用volatile关键字,保证多个线程对这个变量可见,防止多次创建同一个变量。同时禁止了指令重排。

第二个问题:使用双端检查加锁,防止一个线程执行到判断是否为null的时候时候线程被挂起,另一个线程执行创建对象,这个线程唤醒,再次创建对象。所以采用对new实例加锁,加锁以后再次进行一次判断。不过这样容易发生指令重排的现象,所以通过对该变量添加volatile关键字实现禁止指令重排。

2. CAS

什么是CAS(compareAndSwap)。比较替换,当对一个元素进行赋值的时候首先查看该元素是不是自己期望的值,如果是则进行修改,如果不是则返回false

edee001e1e0ac5c6ba1615cfdf11085d.png

getAndAddInt源码(自旋+CAS)

//o1是对象,v1是字段的内存偏移量,v2是需要增加的值
public int getAndAddInt(Object o1 , long v1,int v2){
    int v3 ;
    do{
        //获取指定内存中的值
  v3 = this.getIntVolatile(o1,v1);
        //查看当前值是否还为获取的值(放置其他线程修改)
    }while(this.compareAndSwapInt(o1,v1,v3,v3+v2))
    return v3 ;
}

2.1 原子类

原子类就是通过CAS+自旋实现.通过直接和内存中的值进行比较,实现原子操作.2.1

2.1 AtomicInteger

底层实现:Unsafe类中的CAS操作和自旋

构造方法:无参,默认为0;有参,传入一个指定值

2.1 AtomicReference

原子引用,用来对自定义类进行原子操作

AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(new User("1",1));
        System.out.println(atomicReference.get());

2.2 CAS缺点

  • 循环时间长
  • ABA问题

3. ABA

8965fb99e1ba351f77a269c21910b20b.png

即一个线程对主内存中的值进行了一系列中间操作,但修改开始和修改结果相同,导致其他线程认为该值没有发生变化。

3.1 解决ABA

通过添加一个版本号实现,类似于数据库中的乐观锁的实现

在java中提供了一个类AtomicStampedReference

User user01 = new User("1",1);
        User user02 = new User("2",2);
        //初始化值和初始化一个版本号
        AtomicStampedReference<User> atomicStampedReference = new AtomicStampedReference<>(user01,100);
        boolean b = atomicStampedReference.compareAndSet(user02, user01, 100, 101);
        boolean b1 = atomicStampedReference.compareAndSet(user01, user02, 101, 101);
        boolean b2 = atomicStampedReference.compareAndSet(user01, user02, 100, 101);
        System.out.println(b2);

比较两次,一次为值,一次为版本号,这个时候ABA问题就被解决了

4. JUC三大线程类

4.1 CountDownLatch

所有线程执行完毕以后,主线程才开始执行。

CountDownLatch countDownLatch = new CountDownLatch(3);
        for(int i = 0 ; i < 3 ; i++){
            new Thread(()->{
                System.out.println("输出");
                countDownLatch.countDown();
            },"线程为:"+i).start();

        }
        countDownLatch.await();
        System.out.println("主线程执行");

输出
输出
输出
主线程执行

原理实现:在创建对象的时候,传入一个需要等待的线程的数量。当线程完成任务以后通过countDown()方法减一,当这个值减为0的时候,通过await()方法等待的线程被唤醒,执行。

4.2 CyclicBarrier

当指定数量的线程就绪以后,开始执行代码

CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for(int i = 0 ; i < 5 ; i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"t进入");
                try{
                    TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"t执行完成");
            },"线程为:"+i).start();
        }

/**


线程为:1 进入
线程为:0 进入
线程为:2 进入
线程为:3 进入
线程为:4 进入
线程为:0 执行完成
线程为:2 执行完成
线程为:4 执行完成

此时线程处于阻塞状态,等待另一个线程的进入,才可以执行任务

*/

实现原理:构造方法传入一个线程数量代表需要达到这个数量以后才可以执行之后的代码,如果线程数量不够,则线程阻塞等待

4.3 Semaphore

对线程进行限流

Semaphore semaphore = new Semaphore(3);
        for(int i = 0 ; i < 5 ; i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"t进入执行");
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"t离开");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            },"线程为:"+i).start();
        }
//输出
线程为:0 进入执行
线程为:2 进入执行
线程为:0 离开
线程为:1 进入执行
线程为:1 离开
线程为:2 离开
线程为:3 进入执行
线程为:3 离开
线程为:4 进入执行
线程为:4 离开

5. Callable

创建线程任务的另一种方式

5.1 与Runnable的区别

RunnableCallable返回值无有抛出异常无有执行方法runcal

5.2 执行

  • 我们发现在Thread类中没有直接传入这个接口的构造方法

702f453e4adf239c0b2ffe5b6b2d1372.png
  • 通过传入Runnbale的实现类实现传入Callable接口

970bc8f5fe0a779521601f879b1edfc2.png
Thread thread = new Thread(new FutureTask<Integer>(()->{return 1;}));

6. Java故障排除

  • jps -l 打印正在执行的java程序
  • jstack ID号,查询具体出错的行数

7. 线程状态

  • NEW开始创建线程
  • RUNNABLE线程进入就绪状态
  • BLOCKED线程进入阻塞状态
  • WAITING线程等待
  • TIMED_WAITING线程超时等待
  • TERMINATED线程销毁

往期推荐

Lock和Synchronized​mp.weixin.qq.com
f6acb2d6d4edaeb56417b74c71addb81.png
线程安全集合类​mp.weixin.qq.com
cb68e419b9b6ca7622a3fb299342381a.png
SQL优化看这一篇就够了​mp.weixin.qq.com
bc05cc1bce2cb330a673885c715b32ee.png
SpringBoot错误信息处理机制及原理​mp.weixin.qq.com
4f0106edab386501ded30f4bddcc98d2.png
你了解线程池吗?​mp.weixin.qq.com
db7de773c19a35a0e0348f4df0d8bd1a.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值