Java并发-1

进程

进程是操作系统进行资源资源分配的单位,进程中包含若干线程

线程

线程是CPU进行调度和执行的基本单位

方法区 堆 虚拟机栈 本地方法栈 程序计数器

每个线程拥有自己的栈和PC

多个线程共享方法去和堆

并行和并发

并行

单位时间内,任务同时执行

并发

在一定时间内,任务都执行了

并行是一起执行。并发是宏观上一起执行的,实际上是CPU时间片的轮询

创建线程的方式

继承Thread

继承thread

重写run方法

没有返回值

实现Runnable

重写run方法

没有返回值

相对于继承thread。更新灵活,避免了单继承的局限性

实现Callable

实现callable接口

重写call方法,此方法带有返回值,且有抛出异常

thread构造器无法直接传入,需要通过Runnable的子类,futureTask进行传入

获取数据时,使用futureTask.get()方法获取。此方法是阻塞的

总结

继承thread,每启动一个线程,都是一个新的资源类,不能使用内部非静态变量作为锁。实现Runnable,避免了单继承,多个线程可以共享一个资源类,在使用时,只能使用主线程被final修饰的变量。

Thread线程只能调用一次start()。多次会抛出异常

run()方法的调用是在start0()里面调用的是,是本地native c++方法

线程的生命周期

新建

初始化状态,线程被构建,但是还没有调用start()方法

运行

运行状态,运行状态包含运行态和就绪态。

阻塞

阻塞状态,表示线程阻塞于锁

等待

等待状态,,表示线程进入了等待状态,进入改状态的线程需要等待其他线程通知或中断

超时等待

超时等待,她可以在指定时间后,自行返回

销毁

终止状态,表示当前线程已经执行完毕

线程创建之后处于新建态,调用start()之后处于ready(可运行态),线程获取到了cpu时间片后处于running(运行态)。

调用sleep方法后,线程会进入阻塞态,sleep结束后,恢复到ready,等待cpu时间片的调用

一个java程序启动,至少包含2个线程 ,一个main线程,一个gc线程

守护线程

//设置为守护线程
setDaemon(true);
如果用户线程结束了,守护线程也会结束

join方法

join方法底层是wait()方法,调用join方法后,调用者就会被阻塞。直到线程运行完后,调用者所在的线程才会继续执行

yield方法

使当前线程由运行态--进入到就绪态 (Ready)。交互cpu时间片

并发编程中的三个问题

可见性

可加性:是指一个线程对共享变量进行修改,其他线程可立即得到修改后的值

案例:

假设新开一个线程,线程里面操作资源类的内容变更,主线程采用while循环判断内容是否更改(采用while可以避免虚假唤醒)。

未加volatile关键字修饰变量。则会一直循环,因为其他线程无法感知到内容实际已修改,即未从工作内存同步到主存中。

使用volatile关键字后,如果变量被线程修改,其他线程可感知到,则案例死循环将终止。

volatile可以保证内存可见性,不能保证原子性。可以防止指令重排

原子性

原子性:在一次或多次操作中,要么操作全部成功,要么全部失败。不会存在中间状态。

解决原子性方案:synchronized 、 cas原子类工具、lock锁机制

synchronized 、AtomicInteger、【lock.lock、lock.unlock】

有序性

有序性:指的是程序中代码的执行顺序,Java在编译和运行时会对代码进行优化(指令重排)来加快程序的速度,但是这样会导致程序的执行顺序和我们编写的代码顺序不同。在并发下,就会产生问题。

初始化一个对象
instance=new Demo();  是被分为三部分
memory=allocate(); 分配内存空间   步骤1
instance(memory) 初始化对象       步骤2
instance=memory; 设置instance指向刚刚分配的内存空间地址。此时instance!=null   步骤3

步骤2和3不存在依赖关系,是否发送重排,单线程下,结果都一样。但是如果步骤3在步骤2前,这个时候instance不为null,但实际初始化工作还没有完成。就会返回一个null的getInstance。这时候数据就出现了问题。

解决方案,加volatile,防止指令重排

CAS

  • CAS:cas是比较并交换compareAndSet,是一条CPU并发原语,功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子性的。

  • 采用Cas+volatile可以保证内存可见性、防止指令重排、以及原子性。解决了并发问题。

  • cas底层原理:

  • 调用Unsafe类中的Cas方法,jvm帮我们是先出cas汇编指令,它是依赖于硬件的功能,通过她实现原子操作,Cas是CPU的一条原子指令。

  • cas的思想就是乐观锁的思想.。java中,采用while循环进行自旋,尝试获取锁或者满足期望值。

CAS三大问题

  • 如果cas长时间不成功,会给cpu带来开销,java中,是采用while进行一直循环的
  • 只能保证一个共享变量的原子性
  • 存在ABA问题

ABA问题

ABA问题指的就是,初始化值为A,如果线程1修改A为B。然后线程2修改A值为C。但是线程1修改成功后,又将B修改为了A。此时,线程2去修改,发现原来的值还是A,她就认为数据没有被修改过,然后就将A修改为了C。但是实际情况,A是被修改过后的,并不是原来默认的A了。

解决方案

增加版本号。通过版本号机制,每次修改,都将版本号+1,修改时,判断版本号是否和自己获取前的一致,一致则修改,否则修改失败。

java juc下的AtomicStampedReference类就是ABA的一种解决方案。

只能保证一个共享变量的原子性

java中采用AtomicReference、AtomicStampedReference 原子引用来保证引用对象之间的原子性

Synchronized优化

synchronized可以同时保证可见性,有序性,原子性

锁消除

锁消除是JIT编译器对synchronized做的优化,在编译的时候,jit通过逃逸分析技术,来分析synchronized锁对象,判断她是不是只可能被一个线程来加锁,没有其他线程来竞争加锁,这个时候编译就不加入monitorenter和monitorexit的指令。仅仅一个线程争用锁的时候,就可以消除这个锁了,提示了代码的执行效率,因为只有一个线程来加锁,就不会涉及到锁的竞争

锁粗化

synchronized(this) { 

}
synchronized(this) {

}
synchronized(this) {

}
----------------------------------------------------
   最终会优化成一个
   synchronized(this) {

}

锁粗化的意思是,如果JIT编译器发现了代码中连续多次加锁释放锁的代码,会结合成一个锁,这就是锁粗化,避免多次加锁释放锁的开销

偏向锁(我偏心)

monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销较大。因此,如果发现大概率只有一个线程会主要竞争一个锁,那么就会给这个锁一个偏好,后面她加锁和释放锁,都基于偏好来执行,不需要通过cas,性能会有提升。但是如果有偏好之外的线程来竞争锁,就会回收之前的偏好,但是其他线程来竞争锁的记录交小。

轻量级锁

如果偏向锁没有成功,说明锁的竞争很激烈。那么这个时候就会尝试采用轻量级锁来加锁,就是将对象头的MarkWord里面有一个轻量级锁的指针,尝试指向持有锁的线程,然后判断一下是不是自己加的锁,如果是自己加的锁,那就指向代码。如果不是自己加的锁,那就是加锁失败,说明以及有其他人加锁了,这个时候就会升级为重量级锁

适应性锁

JIT编译器的优化,如果每个线程持有锁的时间非常短,那么一个线程获取不到锁,就会暂停,发生上下文切换,让其他线程来执行,但其他线程很快就释放了锁,然后唤醒暂停的线程,加入锁的竞争,这样线程会频繁的上下文切换,导致开销过大。针对这种情况,可以采用忙等策略,线程没有获取到锁,就进入while循环不停等待,不会暂停发生上下文切换,等到机会获取到锁继续执行就好了。

posted @ 2022-03-06 00:09  暮雪超霸  阅读(33)  评论(0编辑  收藏  举报