Java并发编程之美 01

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();
    }

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();
    }

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 参数设置该值),很有可能在后面几次尝试中其他线程已经释 放了锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值