博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java 并发基础学习
阅读量:6496 次
发布时间:2019-06-24

本文共 5792 字,大约阅读时间需要 19 分钟。

hot3.png

三个基础概念

  1. 原子性。一个操作或者一系列骚操作,要么全部执行要么全部不执行。数据库中的“事物”就是个典型的院子操作。
  2. 可见性。当一个线程修改了共享属性的值,其它线程能立刻看到共享属性值的更改。举个例子:由于JMM(Java Memory Model)分为主存和工作内存,共享属性的修改过程为从主存中读取并复制到工作内存中,在工作内存中修改完成之后,再刷新主存中的值。如果线程A在工作内存中修改完成但还没有刷新主存中的值,线程B看到的值还是旧值。这样可见性就没法保证。
  3. 有序性。程序的运行顺序似乎和我们编写逻辑的顺序是一致的,但计算机在实际执行中却并不一定。为了提高性能,编译器和处理器都会对代码进行重新排序。但是有个前提,重新排序的结果要和单线程执行程序顺序一致。
int a = 0;      // 语句Aint b = 1;      // 语句Bint c = a + b;  // 语句C

由于语句A和语句B没有数据依赖。重排序后,语句A和语句B,在计算机中的执行顺序可能是AB也可能是BA,但AB都与C有数据依赖,所以AB都在C前面执行。

Java中控制并发的几种方式

  1. volatile
  2. synchronized
  3. CAS/AQS
  4. concurrent并发包

volatile

volatile用来保证可见性和有序性,不保证原子性。

volatile保证的可见性

volatile修饰的属性保证每次读取都能读到最新的值,可是并不会更新已经读了的值,它也无法更新已经读了的值。线程A在工作内存中修改共享属性值会立即刷新到主存,线程B/C/D每次通过读写栅栏来达到类似于直接从主存中读取属性值,注意,是类似,网上有些说volatile修饰的变量读写直接在主存中操作,这种说法是不对的,只是表现出类似的行为。读写栅栏是一条CPU指令,插入一个读写栅栏, 相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行(有序性)。读写栅栏另一个作用是强制更新一次不同CPU的缓存。例如,一个写栅栏会 把这个栅栏前写入的数据刷新到缓存,以此保证可见性。

valatile保证的有序性

当对volatile修饰的属性进行读/写操作时,其前面的代码必须已经执行完成且结果对后续的操作可见。在重排序时,以volatile修饰属性的读/写操作代码行为分界线,读/写操作前面的代码不许排序到后面,后面同理不许排序到前面。由此保证有序性.

{   // 线程A    bean = new Bean();     // 语句A    inited = true;         // 语句B}{   // 线程B    if(inited){            // 语句C        bean.getAge();     // 语句D    }}

在线程A中语句AB没有任何数据依赖,所以可能会被重排序成先执行语句B,后执行语句A。假设线程A先执行完语句B之后(这时还没有执行语句A)被挂起,CPU转而执行线程B,由于bean对象没有初始化,所以在执行到语句D就会出错。如果inited属性用volatile修饰,就不会发生这种错误的重排序。

volatile不保证原子性

由于volatile保证可见性和有序性,被volatile修饰的共享属性一般并发读/写没有问题,可以看做是一种轻量级的synchronized的实现。但有些情况比较特殊,比如i++自增。举个栗子。

volatile int a = 0; // 语句Aa++;                // 语句B

a++。其实包含了两步操作。读取a, 执行a+1并将a+1结果赋值给a。假设线程A执行完第一步之后被挂起。线程B执行了a++。那么主存中a的值为1。但是线程A的工作内存中还是0,由于线程A在之前就已经读取了a的值,执行a++之后再次将a的值刷新到主存,也就是说,a++执行了两次,但两次都是从0变为1。所以a的值最终为1。这里有个槽点,之前说volatile修饰的属性,每次读取都是最新的值,这里线程B执行a++之后,线程A里怎么还是0?应该是1啊!我觉得这是volatile一个比较鸡肋的地方,volatile修饰的属性,如果在修改之前已经读取了值,那么修改之后,无法改变已经复制到工作内存的值。体会一下~

synchronized

synchronized保证原子性、可见性和有序性。用来修饰方法或者代码块。下面是synchronized的一些规则。

  1. 根据锁对象的不同,一把锁同时最多只能被一个线程持有。
  2. 如果目标锁已经被当前线程持有,其它线程只能阻塞等待其它线程释放目标锁。
  3. 如果当前线程已经持有了目标锁,其他线程仍然可以调用目标类中没有被synchronized修饰的方法。

以上规则对下文的Lock同样适用。

锁对象举例

synchronized修饰方法或者synchronized(this)

public class Test {    static SyncTest test1 = new SyncTest();    static SyncTest test2 = new SyncTest();    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                test1.syncTwo();            }        }).start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        test1.syncOne();    }}public class SyncTest {    public synchronized void syncOne(){        System.out.println("ThreadId : " + Thread.currentThread().getId());        System.out.println("one");    }    public void syncTwo(){        synchronized (this) {            int a =0;            while(true){                a++;                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println("ThreadId : " + Thread.currentThread().getId());                if(a == 5) break;            }            System.out.println("two");        }    }}控制台输出:ThreadId : 10ThreadId : 10ThreadId : 10ThreadId : 10ThreadId : 10twoThreadId : 1one

为了先调用test1.syncOne(),这里先将主线程暂停1s。从控制台输出可以发现,子线程确实被阻塞了,从而说明synchronized修饰方法或者synchronized(this),获得的都是实例对象的锁。

如果将test1.syncOne()换成test2.syncOne();那么主线程就不会阻塞了。控制台输出为:

ThreadId : 1oneThreadId : 10ThreadId : 10ThreadId : 10ThreadId : 10ThreadId : 10two

synchronized(xxx.class)/synchronized static

public class SyncTest {    public synchronized static void syncOne() {        System.out.println("ThreadId : " + Thread.currentThread().getId());        System.out.println("one");    }    public void syncTwo() {        synchronized (SyncTest.class) {            int a = 0;            while (true) {                a++;                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println("ThreadId : " + Thread.currentThread().getId());                if (a == 5)                    break;            }            System.out.println("two");        }    }}

如果将SyncTest中的synchronized(this)换成synchronized(SyncTest.class)/synchronized static。那么无论调用test1.syncOne()还是test2.syncOne(),主线程都会阻塞。synchronized(SyncTest.class)/synchronized static这种写法,保证对TestSync类的访问,同一时刻只能有一个线程持有锁。

synchronized实现的是阻塞型并发,synchronized修饰的范围越大,瓶颈越高。为了解决这种问题,由此又有减小锁范围、减小锁粒度和锁分段之说。鉴于篇幅,详细还请自行查看。

CAS

CAS(compare and swap),即比较并交换。synchronized锁住的代码块,同一时刻只能由一个线程访问。属于悲观锁。相对于这种需要挂起线程的悲观锁,还一种由CAS实现的乐观锁。CAS包含三个部分:

  1. 内存地址A
  2. 预期旧值B
  3. 预期新值C 

在进行CAS操作时,首先比较A和B,如果相等,则更新A中的值为C并返回true。否则,返回false。通常CAS伴随着死循环,以不断尝试更新的方式实现并发。伪代码如下:

public boolean compareAndSwap(long memoryA, int oldB, int newC){    if(memoryA.get() == oldB){        memoryA.set(newC);        return true;    }    return false;}

相对于synchronized省去了挂起线程、恢复线程的开销,但是如果迟迟得不到更新,死循环对CPU资源也是一种浪费。

使用CAS有个“先检查后执行”的操作,而这种操作在Java中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。CAS在Java中的体现为Unsafe类,Unsafe类会通过C++直接获取到属性的内存地址,接下来CAS由C++的Atomic::cmpxchg方法实现。这个方法会在CPU指令中添加lock指令,而带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存。具体可参考。我觉得CAS相对于synchronize,本质上也是一种阻塞的实现。只是阻塞的粒度(CPU指令级别)更小。

AQS

AQS(AbstractQueuedSynchronizer)中维护着一个volatile修饰的属性“state”和一个双向链表,通过使用Unsafe中CAS对“state”属性的一些列骚操作(实际就是把state当做标志位)实现独占锁和共享锁,独占锁和共享锁又分为公平锁和非公平锁。

  1. 独占锁:同一时刻只有一个线程持有同一锁,其余线程在链表中排队。
  2. 共享锁:同一时刻可以多个线程持有同一锁。
  3. 公平锁:锁被线程持有后,其余线程排队执行。锁按照FIFO放入链表。
  4. 非公平锁:锁被线程持有后,其余线程排队执行。锁按照FIFO放入链表。但是在刚释放锁的之后,如果有新线程竞争锁,那么新线程将和链表中下个即将被唤醒的线程竞争锁。

关于AQS,我找到两篇比较好的文章,这里就不赘述了。想深入了解的可以看下源码。

concurrent

JDK在java/util/concurrent提供了很多常用的并发类及并发容器类。并发类基本是通过lock(CAS/AQS)实现,并发容器基本是通过synchronize和lock(CAS/AQS)实现的。这是一篇基础,有机会再慢慢补齐这些。作者水平有限,如有错误,接受有偿指正~

转载于:https://my.oschina.net/JiangTun/blog/1612158

你可能感兴趣的文章
多线程同步(循环50 基础加深版)
查看>>
Black and White
查看>>
静态变量和实例变量的区别
查看>>
晨跑【最小费用最大流】
查看>>
景点中心 C组模拟赛
查看>>
iOS国际化(多语言设置)
查看>>
bzoj 2733 平衡树启发式合并
查看>>
sublime简书安装配置
查看>>
爱上MVC~Web.Config的Debug和Release版本介绍
查看>>
条款03 尽可能使用const
查看>>
【转】那些年我们一起清除过的浮动
查看>>
python__高级 : 动态添加 对象属性, 类属性, 对象实例方法, 类静态方法, 类方法...
查看>>
【每天一道算法题】时间复杂度为O(n)的排序
查看>>
NLog的介绍使用
查看>>
Haproxy+Rabbitmq中的问题
查看>>
字符串变量小议
查看>>
232. Implement Queue using Stacks
查看>>
Poj(1469),二分图最大匹配
查看>>
和菜鸟一起学linux之V4L2摄像头应用流程【转】
查看>>
spin_lock、spin_lock_irq、spin_lock_irqsave区别【转】
查看>>