java多线程并发基础汇总一

一、java并发线程基础

1.1 什么是线程

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,正真要占用CPU运行的是线程,所以线程是CPU分配的基本单位

进程与线程之间的关系:
在这里插入图片描述

多个线程共享进程的堆和方法区资源,每个线程都有自己的程序及计数器和栈。

  • 程序计数器:CPU是会切换线程的,程序计数器就是用来记录线程当前要执行的指令地址,线程让出CPU等下次再执行的时候就可以从计数器的位置继续执行(如果是native方法,pc计数器记录的是undefined地址,只有执行的是java代码时pc计数器才记录下一条指令的地址)
  • 栈:线程自己的栈资源,用来存储该线程的局部变量,是线程私有的,栈还用来存放线程的调用栈帧
  • 堆:是进程中的最大一块内存,是被进程中的所有线程共享的,是进程创建时分配的,堆主要存放使用new操作创建的对象实例
  • 方法区:用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的
1.2 线程的创建方式
  1. 继承Thread类(该类还是实现了Runnable接口):好处,在run()方法没获取当前线程直接使用this就行,不好的是java不支持多继承,就不能继承其他的类了。
  2. 实现Runnable接口的run方法
  3. 使用FutureTask:上面两种任务没有返回值,这种方式通过futureTask.get()等待任务返回结果。

注意: 调用了start后并没有马上执行而是处于就绪状态,指的是该线程已经获取了除cpu之外的其他资源,等待CPU资源后才会真正处于运行状态,一旦run方法执行完毕,该线程处于终止状态

1.3 Object类中的方法:线程通知与等待
  1. wait()函数(会释放锁sleep不会释放):线程调用就被阻塞,遇到下面几种情况之一返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()。(2)其他线程调用了该线程的interrupt()方法,该线程抛出interruptException异常返回
    注意1: 如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateExeption异常
    获取监视器锁:synchronized
    注意2: 虚假唤醒,指该线程从挂起---->运行,即使是没有被其他线程调用notify()或者notifyAll()方法进行通知,或者被中断,或者等待超时等。一般用一个while循环来避免发生
  2. notify()函数:一个线程调用notify方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程,随机选一个,唤醒后的线程就去竞争监视器锁。
    注意3: 只有当前线程获取了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否者抛出IllegalMonitorStateExeption异常
  3. notifyAll()函数:唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程
1.4 Thread类中的方法
1.4.1 等待线程执行终止的join方法

Thread类中join()无参无返回值。等待某几件事完成后才能继续执行下去,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。此时可以使用join()方法。其他线程调用interrupt()方法中断线程,会抛出InterrupedException异常而返回

public class joinTest {
    public static void main(String[] args) throws InterruptedException{
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                	//当前线程睡眠
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("child one over");
            }
        });

        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("child two over");
            }
        });

        //启动子线程
        one.start();
        two.start();

        System.out.println("wait all children thread over");

        //等待子线程执行完毕,返回
        one.join();
        two.join();

        System.out.println("all children thread over");
    }
}

运行结果:
在这里插入图片描述
主线程启动两个子线程,分别调用它们的join()方法,这里主线程先调用one.join()被阻塞,等待one执行完毕返回,然后调用two.join()再次阻塞。这里只是演示一下,后面的一般用CountDownLatch

1.4.2 让线程阻塞的sleep()方法

Thread勒种有一个静态的sleep()方法,调用该方法的线程会暂时放出CPU的使用权,但是该线程不会释放锁 时间过后会正常返回,处于就绪状态。如果休眠期间其他线程调用interrupt()方法中断线程,会抛出InterrupedException异常而返回

1.4.3 让出CPU执行权的yield方法

Thread类中的静态yield方法,当一个线程调用yield方法时,就暗示现线程调度器请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。调用后就处于就绪状态,线程调度器会从就绪线程队列中获取一个优先级最高的线程。这个方法一般用的很少
sleep和yield的区别: 当线程调用sleep方法时的调用线程会被阻塞挂起指定时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下次调度还是可能调度当前线程

1.4.4 线程中断(重要)
  • void interrupt()方法:中断线程,A运行时,线程B可以调用线程A的interrupt()方法来设置A的中断标志为true并立即返回。这里只是设置标志,A并没有被中断。如果A因为调用了wait系列函数,join方法或者sleep方法被阻塞挂起,这时候若B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterrupedException异常
  • boolean isInterrupted()方法:检测当前线程是否与被中断,是返回true,否则返回false(不会清除中断标志)
  • boolean interrupted()方法:检测当前线程是否与被中断,是返回true,否则返回false(会清除中断标志),是静态方法,可以直接通过Thread类调用
    注意: interrupted()内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。

interrupted()和isInterrupted方法区别:

public class interruptTest {
    public static void main(String[] args) throws InterruptedException{
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {

                for (;;){

                }
            }
        });

        //启动线程
        one.start();

        //设置中断标志
        one.interrupt();

        //获取中断标志
        System.out.println("isInterrupted:"+one.isInterrupted());

        //获取中断标志并重置
        System.out.println("isInterrupted:"+one.interrupted());

        //获取中断标志并重置
        System.out.println("isInterrupted:"+Thread.interrupted());

        //获取中断标志
        System.out.println("isInterrupted:"+one.isInterrupted());

        one.join();

        System.out.println("main thread is over");
    }
}

运行结果:
在这里插入图片描述
分析: 第一行为true都没什么问题,但是会觉得后面三行应该为true,false,false。但是其实并不是这样的。上面说过interrupted()方法内部获取当前线程的中断状态,这里虽然调用了one的interrupted()方法,但是获取的是主线程的中断标志,因为主线程是当前线程。one.interrupted()和Thread.interrupted()方法的作用一样,目的都是获取当前线程的中断标志。

1.5 线程上下文切换和线程死锁
  • cpu采用时间片轮转算法给线程分配时间片。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是是上下文切换。
    线程切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程池被其他线程中断时。
  • 线程死锁指两个或两个以上的线程执行过程中,因争夺资源而造成的相互等待的状态

在这里插入图片描述

1.6 守护线程和用户线程

java线程分为守护线程(daemon)和用户线程(user)
区别:当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程。只要有一个用户线程没有结束,正常情况下JVM就不会退出。具体介绍等我学了JVM后写。

1.7 ThreadLocal

多线程访问同一个共享变量特别容易出现并发问题,特别是在多线程需要对同一个共享变量进行写入时。我们一般都需要同步措施,一般就是加锁。有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的都是自己线程的变量呢?ThreadLocal可以做这件事情。

ThreadLocal提供了线程本地变量,如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免线程安全问题。
简单说一下实现原理:
在这里插入图片描述
由该图可知,Thread类中有一个threadLocals和一个inheritableThreadLocals,都是Thread LocalMap类型的变量, 而ThreadLocalMap是一个定制化的Hashmap。默认况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的 threadLocals里面并存放起来,当调用线程调用它的get方法时,再从线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面(可能会导致内存溢出),所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除本地变量。另外,Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量。

1.8 inheritableThreadLocal

因为ThreadLocal不支持继承,同一个ThreadLocals变量在父线程中被设置后,在子线程中是获取不到的。inheritableThreadLocal就是让子线程可以访问在父线程中设置的本地变量。
inheritableThreadLocal继承 ThreadLocal,重写了三个方法:

  1. 重写了createMap方法,那么现在当第一次调用set方法时,创建的是 inheritableThreadLocals变量而不是 ThreadLocals变量
  2. 当调用get方法获取当前线程内部的map变量时,获取的是 inheritableThreadLocals变量而不再是 ThreadLocals

inheritableThreadLocal通过重写,让本地变量保存了具体线程的 inheritableThreadLocals变量里面,那么线程在通过 inheritableThreadLocal类实例的set和get方法设置变量时,就会创建当前线程的 inheritableThreadLocals变量。当父线程创建 子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals变量里面。

二、并发线程的其他基础

2.1 java中的线程安全问题和共享变量内存可见性问题
  • 线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者不可预见的结果的问题。
  • java内存模型规定,将所有的变量都存在主内存,当线程使用变量时,会把主内存里面的变量复制到自己的工作内存,线程读写变量操作的是自己工作内存中的变量。当一个线程操作共享变量时,首先从主内存复制共享变量到自己的工作内存,然后对工作内存里面的变量进行处理,处理完后将变量值更新到主内存。因为有缓存的原因,线程A第一次操作变量缓存命中后,线程B就算修改了主内存里面的值,A再次取的时候命中了缓存而不会去主内存找,就出现了内存不可见问题。
    在这里插入图片描述
2.2 synchronized关键字
  1. synchronized块是java提供的一种原子性内置锁(jvm级别的),java中的每个对象都可以把它当做一个同步锁来使用(这个锁在对象头里面的),叫监视器锁。java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态,这是很耗时的操作,而synchronized就会导致上下文切换。
  2. synchronized可以解决原子性和内存可见性:synchronized块的内存语义是把在synchronized快内使用到的变量从工作内存中清除,synchronized在使用改变量的时候直接从主内存获取。退出synchronized块的内存语义是把synchronized块内对共享变量的修改刷新到主内存。(这也是加锁和释放锁的语义)
2.3 volatile关键字
  1. 对于内存可见性,可以不用加锁,就是volatile关键字,该关键字确保一个变量的更新对其他线程马上可见。当一个变量被声明成volatile时,写线程在写入变量时不会把值缓存在寄存器或者其他地方,而是直接把值刷新回主内存。和synchronized有相似。
  2. volatile不保证原子性,保证可见性,还可以防止指令重排。
  3. 写volatile变量时,可以确保volatile写之前的操作不会被编译器重拍到写volatile之后不。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
2.4 java中的CAS操作
  1. 加锁会导致上下文切换和重新调用的开销。非阻塞的volatile关键字只保证可见性,不保证原子性。CAS(compare and swap)是JDK提供的非阻塞原子性操作,它通过硬件保证比较——更新操作的原子性。JDK里面的Unsafe类提供了一系列的compareAndSwap*方法。
  2. boolean compareAndSwapLong(Object obj,long valueOffset,long xepect,int upddate)。CAS里面有四个操作数,分别为:对象内存地址,对象中的变量的偏移量、变量预期值和新的值。操作语义为:如果对象obj中内存偏移量为valuueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性命令

CAS会导致ABA问题,这里不详细描述,JDK中的AtomicStampedReference类给每个变量的状态值都配了一个时间戳,代表版本号,从而避免ABA问题。

2.5 Unsafe类

JDK的rt.jar包中的Unasfe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,他们使用JNI的方式是访问本地C++库。(使用这个库时,如果没有在核心类里面必须得用反射获取,因为他判断了是不是用BootStrap类加载器加载的)
几个主要方法介绍:

  1. public native long objectFieldOffset(Field var1);返回指定的变量所在类中的内存偏移地址。该偏移量仅仅在该Unsafe函数中访问指定字段时使用
  2. public native long getLongvolatile(Object obj, long offset)方法:获取对象obj偏移量为offset的变量对应volatile语句的值
  3. void putLongvolatile(Object obj, long offset,long value)方法:设置obj对象中offset偏移的类型为弄的field的值为value,支持volatile语义
  4. park()和unpark()等等…
2.6 伪共享

计算机为了解决CPU和主内存之前的运行速度差异,会在之间加一级或者多级高速缓冲存储器。cache内部是按行存储的,其中每一行称为一个cache行,cache行是cache与主内存进行数据交换的单位。由于存放到cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。

  • 为什么会出现伪共享:伪共享的产生是因为多个变量被放入一个缓存行中,并且多个线程同时去蟹肉缓存行中不同的变量。当cpu要访问的变量没有在缓存行中找到,根据程序运行的局部性原理,会把该变量所在内存中大小为缓存行的内存放入缓存行。 但是在单线程下读取缓存行速度快,多线程就会出现问题。
  • 如何避免伪共享:JDK8之前采用字节填充的方式,创建一个变量使用填充字段填充改变量所在的缓存行。JDK8提供了一个sun.misc.Contended注解,默认情况下这个注解朱能用于核心类。可以修改默认的填充宽度和在用户路径下使用,具体不详述。
2.7 锁的概述

乐观锁与悲观锁

  • 乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
    java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败
  • 悲观锁:悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

公平锁与非公平锁:

  • 非公平锁:JVM 按随机、就近原则分配锁的机制则称为不公平锁, ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。 非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
  • 公平锁:公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁

独占锁与共享锁:

  • 独占锁:独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性
  • 共享锁:共享锁则允许多个线程同时获取锁,并发访问共享资源,如: ReadWriteLock。 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
    1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
    2. java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

可重入锁:
本文里面讲的是广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。 可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

自旋锁:
自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

原子类,AQS,线程池

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值