Java并发编程总结(一)

什么是线程?

线程是进程的实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行
活动,是系统进行资源分配和调度的基本单位。线程则是进行的这一个执行
路径,一个进程中至少有一个线程。进程中的多个线程共享进程的资源。

创建线程

java中创建线程有三种方式:
1、实现Runnable接口的run方法 (无返回值)
2、继承Thread类并重写run方法(无返回值)
3、使用FutureTask方式(有返回值)

wait()函数

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起
直到发生如下事情就会返回
1、其他线程调用了该共享对象的notify或者notifyAll方法
2、其他线程调用了该线程的interrupt方法,抛出InterruptedException的异常返回
需要注意的地方
    如果调用wait方法的线程没有事先获取该对象的监视器锁,则
    调用wait方法时调用会抛出IllegalMonitorStateException异常

wait(long timeout)函数

在wait方法多了一个超时时间,和wait不同之处在于如果一个线程调用共享对象的该方法挂起后,
没有在指定的的timeout ms时间内被其他线程调用该共享变量的notify或者notifyAll方法唤醒
那么该函数还是会因为超时而返回。

wait(long timeout,int nano)函数

在其内部调用wait(long timeout)函数,只有当nano>0 时才使参数timaout递增1

notify()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程
一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
被唤醒的线程不能马上从wait方法返回并继续执行,他必须在获取了共享对象的监视器锁后才可以返回。
因为该线程还需要和其他线程一起竞争该锁。只有该线程竞争到了共享变量的监视器锁才可以继续执行。

notifyAll函数

唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
需要注意的点:
    在共享变量上调用notifyAll方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量
    等待集合里面的线程。如果调用notifyAll方法后一个线程调用了该共享变量的wait方法而被放入
    阻塞集合,该线程不会被唤醒。

如何才能获取一个共享变量的监视器锁呢?

1、执行synchronized同步代码块时,使用该共享变量作为参数。
    synchronized(共享变量){
    
    }
2、使用该共享变量的方法,并且该方法使用了synchronized修饰。
    synchronized void add(int a,int b){
    
    }

虚假唤醒

一个线程可以从挂起状态变为可以运行状态,即使该线程没有被其他线程
调用notify(),notifyAll()方法进行通知或者被中断,或者等待超时
这就是所谓的虚假唤醒

虚假唤醒的检查

synchronized(obj){
    while(条件不满足){
        obj.wait();
    }
}

volatile变量特性

1、可见性:对一个valatile变量的读,总是能看到任意线程对这个volatile
变量最后的写入
2.原子性:对任意单个volatile变量的读/写具有原子性,但是类型于a++这种
复合操作不具有原子性。
当前线程调用共享对象的wait方法时,当前线程只会释放当前方向对象的锁,当前线程持有的其他共享对象的监听器并不会被释放

join方法

等待线程执行终止
使用场景:需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕
再汇总处理。Thread提供了join方法就可以达到这个目的。
join是无参且返回值为void的方法。
ThreadJoin2Test:
线程A调用线程B的join方法后会阻塞,当其他线程调用了线程A的interrupedException异常而返回。

sleep方法

当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这个期间
不参与CPU的调度,但是该线程所拥有的监视器资源还是持有不让出的。
指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU的资源后就可以
继续运行了。
如果在sleep期间,调用了中断的方法,将抛出InterrupedException异常。

yield方法

让出CPU执行权,线程处于就绪状态

线程中断

线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据
中断状态自行处理
interrupt函数:中断线程,当线程A运行时,线程B可以调用线程A的interrupt函数方法来设置线程A的中断
标志为true,设置标记仅仅为设置标记,线程A实际并没有中断,他会继续往下执行。如果线程A应为调用了wait、join
或者sleep方法的时候,线程B调用线程A的interrupt函数将会抛出异常。
boolean isInterrupted()方法:检查当前线程是否被中断。
boolean interrupted()方法,检查当前线程是否被中断。该方法如果发现线程被中断,则会清除中断标志,并且
该方法是static方法,可以通过Thread类直接调用

线程切换

在多线程编程中,线程个数一般是大于CPU的个数,CPU资源分配采用了时间轮转的策略,也就是给每个线程分配一个
时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用
,这就是线程上线切换
    线程上下文切换时机:
        1.当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。

线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力的作用下,这些线程
会一直相互等待而无法继续运行下去。
产生死锁的四个条件:
    1、互斥条件:值线程对已经获取到的资源进行排它性使用,即该资源同时只能有一个线程占用。如果此时还有
    其他线程请求获取该资源,则请求则只能等待,直到占有资源的线程释放该资源。
    2、请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已经被其他线程
    占有,所有当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
    3、不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才有自己
    释放该资源。
    4、环路等待条件:指在发生死锁时,必然存在一个线程-资源的环型链,及线程集合中等待各自线程的占用的资源

如何避免线程死锁

只需要破坏掉至少一个构造死锁的必要条件即可,但是只有请求并持有和环路等待条件时可以被破坏的。造成死锁的原因
其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。

守护进程与用户进程

Java中的线程分为两类,分别为守护进程和用户线程。
   用户线程:在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程
   其实在JVM内部同时还启动了好多守护进程,比如垃圾回收。
   区别:是当最后一个非守护进程结束时,JVM会正常退出,而不管当前是否有守护线程。也就是守护经常是否结束
   并不影响JVM的退出。

ThreadLocal

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程
安全,一般使用者在访问共享变量时需要进行适当的同步。
    同步的措施一般是加锁。
    ThreadLocal提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程
    都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了
    线程安全问题。

ThreadLocal不支持继承性

同一个ThreadLocal变量在父线程中被设置后,在子线程中是获取不到的。

InheritableThreadLocal继承了ThreadLocal,实现了一个特性
让子线程能访问在父类进程中设置的本地变量

并发和并行

并发:指同意时间段内多个任务同时都在执行,并且没有执行结束
并行:指在单位时间内多个任务同时在执行。并发是强调在一个时间段内同时执行
而一个时间段有多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定
同时执行。

线程安全

线程安全是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

synchronized关键字

synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当做一个同步锁来使用,Java内置的
的使用者看不到的锁叫内部锁。
线程执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。

valatile关键字

该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为valatile时,线程在写入变量时不会把值
缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不
是使用当前线程的工作内存中的值。
    volatile的操作并不能保证原子性。
    使用场景:1、写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取-计算-写入三步操作,这三步不是
    原子性的,而volatile并不保证原子性。
    2.读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的

unSafe类

Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,使用JNI的方式访问本地的C++实现库。
Unsafe类的初始化运行的时候会检查是使用哪一种类加载器:
@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
//判断是不是bootstrap类加载器加载的localClass,如果使用AppClassLoader加载的,会抛出异常
//为什么会抛出异常?
//unsafe类是在rt.jar包中,这个包中的里面的类是使用bootstrap类加载器加载的,而我们启动main函数
//是使用AppClassLoader加载的,如果不做这个限制的话,就可以随意使用unsafe了,unsafe是可以直接
//操作内存的,这是不安全的。
public static boolean isSystemDomainLoader(ClassLoader var0) {
    return var0 == null;
}

Java指令重排序

java内存模型允许编译器和处理器对指令重排序已提高运行性能,并且只会对不存在数据依赖性的指令重排序
在单线程下重排序可以保证最终执行的结果和程序顺序执行的结果一致。但是在多线程下就会存在问题。
reordering:
重排序在多线程下会导致非预期的程序执行结果,而使用volatile修饰ready就可以避免重排序问题和内存可见
性问题。
写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile
变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。

Java中的伪共享

为了解决计算机系统中主存与CPU之间运行速度差问题,会在CPU和主内存之间添加一级或者多级高速缓冲存储器,
这个Cache一般是被集成到CPU内部,所以叫CPU Cache。
在Cache内部是换行存储的,其中每一行称为Cache行。Cache行是Cache与主内存进行数据交换的单位,Cache
行的大小一般为2的幂次数字节。
当CPU访问某个变量时,首先会去看CPU Cache内是否有这个变量,如果有则直接从中获取,否则就去主内存中
里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。由于存放到Cache行的
是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中,当多个线程同时修改一个缓存行里面
的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这
就是伪共享。
在单线程下访问时将数组元素放入一个或者多个缓存行对代码执行是有利的,因为数据都在缓存中,代码执行会更
快。
在单个线程下顺序修改一个缓存行中的多个变量,会充分利用程序运行的局部性原则。从而加速了程序运行。而再
多线程下并发修改一个缓存行中的多个变量时就会竞争缓存行,从而降低程序运行性能。

如何避免伪共享?

在java 8之前一般是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的
缓存行,这样就避免 将多个变量存放在同一个缓存行中。
java 8中提供了一个Contended注解,用来解决伪共享问题。
需要注意的是在默认情况下,@Contended注解只用于java核心类,如果用户路径下的类需要使用这个注解,则
需要添加JVM参数-XX:-RestrictContended,填充的宽度默认为128,自定义的话则可以设置
-XX:ContendedPaddingWidth参数。

锁的概念 乐观锁和悲观锁

乐观锁:他认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在进行数据提交更新时,
才会对数据冲突与否进行校验。根据update返回的行数让用户决定如何去做。
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者业务状态来实现,乐观锁知道提交时
才锁定,所以不会产生任何死锁。
悲观锁:悲观锁指对数据被外界修改持保守态度。认为数据很容易就会被其他线程修改,所以在数据被处理前先对
数据进行加锁,并在整个数据处理过程中,是数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即
在数据库中,在对数据记录操作前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程
则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。

公平锁和非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。
公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁
非公平锁:则是在运行时闯入。
ReentrantLock提供了公平和非公平锁的实现。

独占锁和共享锁

独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。
共享锁则可以同时由多个线程持有。
独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这个限制了并发性,因为读操作并不会影响数据的一致性
而独占锁只允许同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取操作。
共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

可重入锁

当一个线程要获取一个呗其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁
时是否会被阻塞呢?如果不阻塞,那么我们所该锁是可重入的。也就是只要该线程获取看该锁,那么可以无限次地进
入被该锁锁住的代码
实际上,synchronized内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标识,用来标识该锁目前被
哪个线程占用,然后关联一个计数器,一开始计数器值为0,说明该锁没有任何线程占用,当一个线程获取了该锁时
计数器的值会变成1,这是其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。当获取了该锁的线程
再次获取锁时,会将计数器加1,当释放锁后计数器值-1,当计数器值为0时,锁里面的线程标识被重置为null,
这时候被阻塞的线程会被唤醒来竞争获取该锁。

自旋锁

由于java中的线程是域操作系统中的线程一一对应,所以当一个线程在获取锁失败后,或被切换到内核状态而被挂
起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大
的,在一定程度影响并发性能,自旋锁则是当前线程在获取锁时,如果发现锁已经被其他线程占有,它不会马上阻塞
自己,在不放弃CPU使用权的情况下,多次尝试,默认为10次,可以使用--XX:PreBlockSpinsh参数设置该值
很有可能在几次尝试其他线程释放了锁,如果尝试指定的次数后仍然没有获取到锁,则当前线程才会被阻塞挂起,
由此来看,自旋锁是使用CPU的时间换取线程阻塞域调度的开销,可能CPU时间白白浪费了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章