Java并发编程之美 01
原文链接:https://blog.csdn.net/qq_39355828/article/details/120633032 (opens new window)
# Java并发编程之美
# 01.多线程实现的方式
# 01.继承Thread,重写run方法
public static class Mythread extends Thread
{
@Override
public void run()
{
System.out.println("hello,Thread");
}
}
public static void main(String[] args) {
Mythread mythread=new Mythread();
mythread.start();
}
2
3
4
5
6
7
8
9
10
11
12
13
# 02.实现Runnable接口的run方法
public static class Mythread implements Runnable
{
@Override
public void run()
{
System.out.println("hello,Thread");
}
}
public static void main(String[] args) {
Mythread mythread=new Mythread();
new Thread(mythread).start();
}
2
3
4
5
6
7
8
9
10
11
12
13
# 02.进程通知等待
# 01.wait()函数
当一个线程调用一个共享变量的 wait() 方法时,该调用线程会被阻塞挂起,直到发生 下面几件事情之一才返回:(1)其他线程调用了该共享对象的 notify() 或者 notifyAll() 方法; (2)其他线程调用了该线程的 interrupt() 方法
另外需要注意的是, 一个线程可以从挂起状态变为可以运行状态(也就是被唤醒), 即使该线程没有被其他线程调用 notify()、notifyAll() 方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
# 02.notify() 函数
一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列 方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线 程是随机的。
# 03.notifyAll() 方法
会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
# 04.join 方法
等待线程执行终止. join 方法是 Thread 类直接提供的。join 是无参且返回值为 void 的方法。
# 05.sleep 方法
当一个执行中的线程调用了 Thread 的 sleep 方 法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但 是该线程所拥有的监视器资源,比如锁还是持有不让出的。
# 06.yield 方法
Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示 线程调度器当前线程请求让出自己的 CPU 使用,
# 07.死锁
互斥条件 :指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资 源的线程释放该资源。
请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求, 而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件 :指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
环路等待条件 :指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合 {T0,T1,T2,…,T n } 中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占 用的资源,……T n 正在等待已被 T0 占用的资源。
死锁的避免:
破坏请求并持有和环路等待条件
资源申请的有序性
# 03.多线程并发
多线程并发导致的问题,从根本上说是主内存和工作内存的矛盾,线程工作的时候会把把工作内存复制到自己工作内存进行,从而刷新回会主内存时候的不一致性。
# 01.synchronized 的内存语义
进入 synchronized 块的内存语义是把在 synchronized 块内使用到的变量从线程的工作内存 中清除,这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是 直接从主内存中获取。退出 synchronized 块的内存语义是把在 synchronized 块内对共享变 量的修改刷新到主内存。
# 02.volatile 关键字
可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者 其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获 取最新值,而不是使用当前线程的工作内存中的值。volatile 的内存语义和 synchronized 有 相似之处,具体来说就是,当线程写入了 volatile 变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存),读取 volatile 变量值时就相当于进入同 步块(先清空本地内存变量值,再从主内存获取最新值)。
# 03.那么一般在什么时候才使用 volatile 关键字呢?
(1)写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而 volatile 不保证原子性。
(2)读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 volatile 的。
# 04Java 中的原子性操作
(1)synchronized
使用 synchronized 关键字的确可以实现线程安全性, 即内存可见性和原子性, 但是 synchronized 是独占锁,没有获取内部锁的线程会被阻塞掉,而这里的 getCount 方法只是 读操作,多个线程同时调用不会存在线程安全问题。但是加了关键字 synchronized 后,同 一时间就只能有一个线程可以调用,这显然大大降低了并发性。
(2)CAS
在 Java 中, 锁在并发处理中占据了一席之地, 但是使用锁有一个不好的地方, 就是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题, 这在一定程度 上弥补了锁带来的开销问题, 但是 volatile 只能保证共享变量的可见性, 不能解决读改—写等的原子性问题。CAS 即 Compare and Swap, 其是 JDK 提供的非阻塞原子性操 作,它通过硬件保证了比较—更新操作的原子性。
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)
就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
# 05伪共享
把多个变量存放到一个 Cache 行中。当多个线程同时修改一个缓存行里面的多个变量时, 由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降
伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存 行中不同的变量。那么为何多个变量会被放入一个缓存行呢?其实是因为缓存与内存交换 数据的单位就是缓存行,当 CPU 要访问的变量没有在缓存中找到时,根据程序运行的局部性原理,会把该变量所在内存中大小为缓存行的内存放入缓存行。
如何避免伪共享
JDK 8 提供了一个 sun.misc.Contended 注解,用来解决伪共享问题。将上面代码修改 为如下。
# 06.锁的概述
# 01.乐观锁与悲观锁
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以 在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。
乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记 录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。
# 02.公平锁与非公平锁
公平锁表示线程获取锁 的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。
# 03.独占锁与共享锁
独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock 就是以独占方式实现 的。共享锁则可以同时由多个线程持有,例如 ReadWriteLock 读写锁,它允许一个资源可 以被多线程同时进行读操作。
# 04.可重入锁
当一个线 程再次获取它自己已经获取的锁时是否会被阻塞呢 ? 如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严 格来说是有限次数)地进入被该锁锁住的代码。
# 05.自旋锁
当一个线程在获取锁(比 如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换 到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度 上会影响并发性能。自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有, 它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可 以使用 -XX:PreBlockSpinsh 参数设置该值),很有可能在后面几次尝试中其他线程已经释 放了锁