Java 并发编程复习(一)



周末抽空看了 Java并发编程的艺术,暑假从图书馆借的,趁带复习并总结下并发编程相关知识点,很多都是大佬们总结好的,做了知识的搬运工,附加自己的理解。

1.synchronized是什么,实现原理怎样的?

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
java代码中使用synchronized是使用在代码块和方法中:
在这里插入图片描述
synchronized可以用在方法上也可以使用在代码块中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种。需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。(可参考cyc博客)

实现原理:

  • 修饰代码块:

    public class SynchronizedDemo {
      public static void main(String[] args) {
          synchronized (SynchronizedDemo.class) {
          }
          method();
      }
    
      private static void method() {
      }
    }
    
可以通过 java -p 查看编译后的class文件
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190817154617332.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNjk5MzM5,size_16,color_FFFFFF,t_70)
synchronized 同步语句块的实现使用的是 **monitorenter 和 monitorexit 指令**,其中 monitorenter 指令指向同步代码块的**开始位置**,monitorexit 指令则指明同步代码块的**结束位置**。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的。**每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一**。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

- 修饰方法:
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 **ACC_SYNCHRONIZED** 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

#### 2.有哪几种锁?锁的优化?
这里的锁优化主要是指 JVM 对 synchronized 的优化。

synchronized 在同步的时候是获取对象的monitor,即获取到对象的锁,用的锁是存在 Java 对象头里的。

锁主要存在四种状态,依次是:**无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态**,他们会随着竞争的激烈而逐渐升级。注意**锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。**

引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了**没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。**但是不同是:**轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。**
- 偏向锁:
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,**那么持有偏向锁的线程就不需要进行同步,连 CAS 操作也不需要。**
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向后恢复到未锁定状态或者轻量级锁状态。

- 轻量级锁:
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段。轻量级锁是相对于传统的重量级锁而言,它**使用 CAS 操作来避免重量级锁使用互斥量的开销**,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,**不需要申请互斥量。**另外,**轻量级锁的加锁和解锁都用到了CAS操作。**
如果有两条以上的线程争用同一个锁,轻量级锁比传统的重量级锁更慢,轻量级锁就不再有效,要膨胀为重量级锁。

- 三种锁比较:来自《Java 并发编程的艺术》
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190817163501345.png)

- 自旋锁:
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。**自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。**
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

- 锁消除:
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。

- 锁粗化:
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。如果虚拟机探测到这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。即原本加锁多次的,加锁一次就可以了。


#### 3. volatile 关键字,实现原理
作用:
- 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
- volatile 关键字也可以防止指令重排,如单例模式中的双重校验锁实现。

我们知道,变量可以保存在本地缓存,也可以保存在内存中,如果一个线程在内存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,就会造成数据的不一致。
**实现原理:**
生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出**Lock前缀的指令**,该指令有两个作用:
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

synchronized 关键字和 volatile 关键字的区别:
- volatile关键字是线程同步的轻量级实现,所以volatile性能比synchronized关键字要好。
- volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块;
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞;
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证;
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

#### 4.synchronized和ReentrantLock 的区别
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

区别:
- synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
- ReentrantLock提供了一种能够**中断等待锁**的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReentrantLock支持两种锁:公平锁和非公平锁。**默认非公平**。公平性是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO,可以通过 ReentrantLock类的ReentrantLock(boolean fair),而**synchronized 中的锁是非公平的。**
- 一个 ReentrantLock 可以同时绑定多个 Condition 对象。
- JDK1.6后 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 的性能大致相同。

选择:
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

#### 5. ThreadLocal是什么,实现原理:
通常解决线程安全的问题我们会利用synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。
**ThreadLocal 实现了每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼让多个线程间达到隔离的状态**,这样就不会出现线程安全的问题。这就是一种“空间换时间”的方案,每个线程都会都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。

实现原理:
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。

当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
ThreadLocal.et();
~~~ java
   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal.get();

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocal 内存泄露问题:
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

使用场景:
ThreadLocal 不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个thread实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此threadLocal只适用于 共享对象会造成线程安全 的业务场景。比如通过threadLocal管理Session就是一个典型的案例,不同的请求线程(用户)拥有自己的session,若将session共享出去被多线程访问,必然会带来线程安全问题。

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

6. 什么是线程池,有什么好处

使用线程池管理线程主要有如下好处:

  1. 降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;
  2. 提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。

Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    在这里插入图片描述

线程池的工作原理:
在这里插入图片描述
线程池的创建、关闭、如何合理配置线程池参数可以参考下面这篇文章。
线程池详解

如何设计一个线程池,需要考虑什么?

一个线程池包括以下四个基本组成部分:
• 线程管理器 (Thread Pool ) :用于创建并管理线程池,包括创建线程,销毁线程池,添加新任务;
• 工作线程 (PoolWorker) :线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
• 任务接口 (Task) :每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
• 任务队列 (TaskQueue) :用于存放没有处理的任务。提供一种缓冲机制; ○ 所包含的方法
• private ThreadPool() 创建线程池
• public static ThreadPool getThreadPool() 获得一个默认线程个数的线程池
• public void execute(Runnable task) 执行任务 , 其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
• public void execute(Runnable[] task) 批量执行任务 , 其实只是把任务加入任务队列,什么时候执行有线程池管理器 决定
• public void destroy() 销毁线程池 , 该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
• public int getWorkThreadNumber() 返回工作线程的个数
• public int getFinishedTasknumber() 返回已完成任务的个数 , 这里的已完成是只出了任务队列的任务个数,可能该任务并没有实际执行完成
• public void addThread() 在保证线程池中所有线程正在执行,并且要执行线程的个数大于某一值时。增加线程池中线程的个数
• public void reduceThread() 在保证线程池中有很大一部分线程处于空闲状态,并且空闲状态的线程在小于某一s值时,减少线程池中线程的个数

参考文献:


   转载规则


《Java 并发编程复习(一)》 ForeverSen 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
剑指Offer(10-19Java语言描述) 剑指Offer(10-19Java语言描述)
 继续刷剑指,提高效率,第二波。。 亮剑经典语录 二:逢敌必亮剑,倒在对手的剑下不丢脸,丢脸的是不敢亮剑。 10. 斐波那契数列题目描述:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)
2019-08-19
下一篇 
剑指Offer(1-9Java语言描述) 剑指Offer(1-9Java语言描述)
很快秋招就来了,今天开始刷剑指,应该不会太慢吧,之前刷了一百道leetcode,按照tag刷的,想着这个月前把剑指刷一遍,第一波,记录下: 顺便附带亮剑经典语录 一:碰到我们独立团,就是碰到了一群野狼,在咱们眼里,任何对手,都是我们嘴里的
2019-08-02
  目录