多线程总结

多线程面试题

问题

在这里插入图片描述

锁的概念

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁

偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

  1. 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。

  2. 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争

  3. 重量级锁:有实际竞争,且锁竞争时间长

1、公平锁/非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比 先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,(参数为true时表示公平锁,不传或者false都是为非公平锁。)默认是非公平锁非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁,它无法保证等待的线程获取锁的顺序。。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

2、可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

对于Java ReentrantLock而言,是一个可重入锁,其名字是Re entrant Lock重新进入锁。

对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

3、独享锁/共享锁 (互斥锁/读写锁)

独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock其读锁是共享锁,其写锁是独享锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是ReentrantLock

读写锁在Java中的具体实现就是ReadWriteLock

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

4、乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

对于同一个数据的并发操作,悲观锁采取加锁的形式悲观的认为不加锁的并发操作 一定会出问题

乐观锁在更新数据的时候,主要就是两个步骤:冲突检测和数据更新。乐观的认为,不加锁的并发操作是没有事情的。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被 告知这次竞争中失败,并可以再次尝试。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常 多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是利用各种锁。

乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新

CAS包含三个参数 CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。仅当要更新的变量值等于预期的值时,才会将要更新的变量值的值设置成新值,否则什么都不做。

5、偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。 重量级锁会让其他申请的线程进入阻塞,性能降低

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

可中断锁,顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

乐观锁和悲观锁讲一下,哪些地方用到。

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度

悲观锁对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程,传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,数据库的for update SQL语句。Java中synchronized 和ReentrantLock 等独占锁就是悲观锁思想的实现。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

**乐观锁适用于写比较少的情况下(多读场景),一般多写的场景下用悲观锁就比较合适,**一 般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能。

自旋锁,偏向锁,轻量级锁,重量级锁

Synchronized的原理及自旋锁,偏向锁,轻量级锁,重量级锁的区别

乐观锁、悲观锁、AQS、sync和Lock

CAS

在这里插入图片描述
CAS在多线程种,不需要锁的情况下,保证线程一致性的去修改某一个值
jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

存在的问题:要是结果一直就一直循环了,CUP开销是个问题,还有ABA问题和只能保证一个共享变量原子操作的问题

  1. 循环时间长开销大的问题:是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。
  2. 只能保证一个共享变量的原子操作:CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。
  3. ABA问题:加版本号可以解决
    比较并交换这条指令的原子性:使用 lock cmpxchg 指令
    在这里插入图片描述

乐观锁在项目开发中的实践,有么?

有的就比如我们在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全,但是看场景使用,并不是适用所有场景,他的优点缺点都很明显。

CAS算法在JDK种的应运用

AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了类sun.misc.Unsafe库里面的 CAS算法,用CPU指令来实现无锁自增。所以,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。

CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

 AtomicInteger atomicInteger = new AtomicInteger;
        atomicInteger.incrementAndGet();
 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
  public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
  public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);



// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

那开发过程中ABA你们是怎么保证的?

加标志位,例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。

举个栗子:现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。

update table set value = newValue where value = #{oldValue}
//oldValue就是我们执行前查询出来的值 
update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} 
// 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样

Atomic

原子类:一个操作是不可中断的,一个操作一旦开始,就不会被其他线程干扰。

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

synchronized实现过程

Synchronized
JDK 1.0-1.2 重量级锁

在JDK1.5之前都是使用synchronized关键字保证同步的,它可以把任意一个非NULL的对象当作锁。

当我们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作;

在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,如果以上两种都失败,则启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

synchronized可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
synchronized有三种应用方式:

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

synchronized可以保证变量的原子性,可见性和顺序性。
所以可以保证方法或者代码块在运行时只有一个方法可以进入临界区获取资源,同时还可以保证内存变量的内存可见性。并且synchronized是一个可重入锁。
实现过程

  1. java代码:synchronzed
  2. monitorenter moniterexit
  3. 执行过程中自动升级
  4. lock comxchg

CAS是乐观锁的实现,synchronized就是悲观锁了。

synchronized是如何保证同一时刻只有一个线程可以进入临界区呢?

synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

同步方法和同步代码块底层都是通过monitor来实现同步的。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。

我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。

synchronized
synchronized 实现原理

还有其他的同步手段么?

锁消除 eliminate

在这里插入图片描述

锁粗化 lock coarsening

在这里插入图片描述

锁优化

减少锁的时间:不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;
减少锁的粒度:它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;
ConcurrentHashMap:java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组。

Segment< K,V >[] segments

Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。
**锁粗化:**大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;
在以下场景下需要粗话锁的粒度:
假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;
使用读写锁: ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;
消除缓存行的伪共享:除了我们在代码中使用的同步锁和jvm自己内置的同步锁外,还有一种隐藏的锁就是缓存行,它也被称为性能杀手。
在多核cup的处理器中,每个cup都有自己独占的一级缓存、二级缓存,甚至还有一个共享的三级缓存,为了提高性能,cpu读取数据是以缓存行为最小单元读取的;32位的cpu缓存行为32字节,64位cup的缓存行为64字节,这就导致了一些问题。
例如,多个不需要同步的变量因为存储在连续的32字节或64字节里面,当需要其中的一个变量时,就将它们作为一个缓存行一起加载到某个cup-1私有的缓存中(虽然只需要一个变量,但是cpu读取会以缓存行为最小单位,将其相邻的变量一起读入),被读入cpu缓存的变量相当于是对主内存变量的一个拷贝,也相当于变相的将在同一个缓存行中的几个变量加了一把锁,这个缓存行中任何一个变量发生了变化,当cup-2需要读取这个缓存行时,就需要先将cup-1中被改变了的整个缓存行更新回主存(即使其它变量没有更改),然后cup-2才能够读取,而cup-2可能需要更改这个缓存行的变量与cpu-1已经更改的缓存行中的变量是不一样的,所以这相当于给几个毫不相关的变量加了一把同步锁;
为了防止伪共享,不同jdk版本实现方式是不一样的:

  1. 在jdk1.7之前会 将需要独占缓存行的变量前后添加一组long类型的变量,依靠这些无意义的数组的填充做到一个变量自己独占一个缓存行;
  2. 在jdk1.7因为jvm会将这些没有用到的变量优化掉,所以采用继承一个声明了好多long变量的类的方式来实现;
  3. 在jdk1.8中通过添加sun.misc.Contended注解来解决这个问题,若要使该注解有效必须在jvm中添加以下参数:
    -XX:-RestrictContended

sun.misc.Contended注解会在变量前面添加128字节的padding将当前变量与其他变量进行隔离;
关于什么是缓存行,jdk是如何避免缓存行的,网上有非常多的解释,在这里就不再深入讲解了;

锁升级过程

new-偏向锁-轻量级锁(无锁,自旋锁,自适应自旋)-重量级锁
在这里插入图片描述

在这里插入图片描述

在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。

volatile

一个volatile跟面试官扯了半个小时

保证线程之间的可见性

可见性就是一个线程对共享变量做了修改之后,其他的线程立即能够看到修改后的值。Java内存模型将工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。
Java提供了volatile关键字来保证可见性。当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中,非volatile变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。而声明变量是volatile的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会跳过CPU cache这一步去内存中读取新值volatile只确保了可见性,并不能确保原子性。

保证线程之间的有序性

为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,编译器和处理器常常会对指令做重排序
有序性:即程序执行的顺序按照代码的先后顺序执行。
CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。通过volatile关键字来保证一定的“有序性”,
volatile关键字本身就包含了禁止指令重排序的语义。另外可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile
volatile如何解决指令重排序
1、volatile i
2、ACC_VOLATILE
3、JVM的内存屏障:屏障两边的指令不可以重排!保障有序!
4、hotspoot实现

synchronized 和Lock区别

在这里插入图片描述

  • lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  • 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  • 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  • 是否知道获取锁
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  • Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  • 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  • synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
...
condition.await();
...
condition.signal();
condition.signalAll();

synchronized、lock性能区别

  • synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

  • Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
    现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

synchronized、lock用途

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。

  • 1.某个线程在等待一个锁的控制权的这段时间需要中断
  • 2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
    3.具有公平锁功能,每个到来的线程都将排队等候

下面细细道来……

先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。

用两个线程,交替输出字母和数字

在这里插入图片描述

 public static void main(String[] args) {
        final Object o=new Object();
        
        new Thread(()->{
            synchronized (o) {
                for (int i = 1; i <= 26; i++) {
                    System.err.print(i + ":\t");
                    o.notifyAll();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //再调用一次唤醒
                o.notify();
            }
        });

        new Thread(()->{
            synchronized (o) {
                for (int i = 'A'; i <= 'Z'; i++) {
                    System.err.println((char) i);
                    o.notifyAll();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //再调用一次唤醒
                o.notify();
            }
        });



    }

synchronized和Lock探索

synchronized 和Lock区别

一个synchronized跟面试官扯了半个小时

系统底层如何实现数据一致性

系统底层如何保证有序性

CPU,内存,缓存,缓存行

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

时间局部性原理,空间局部性原理

每次从内存中取数据的时候,按块读取,程序局部性原理,可以提高效率,充分发挥总线CPU针脚等一次性读取更多数据的能力

缓存行:缓存行越大,局部性空间效率越高,但读取时间慢;缓存行越小,局部性空间效率越低,但读取时间快。 目前多用64字节,

1字节=8位, long类型占8字节

在这里插入图片描述

缓存一致性协议

cpu为了保持缓存行之间数据一致性的协议
在这里插入图片描述
在这里插入图片描述

cpu乱序执行的概念

在这里插入图片描述

在多线程情况下如何保证线程安全

如何保证线程安全

在这里插入图片描述

  1. 互斥同步

互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。

在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。

此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。

  1. 非阻塞同步

非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。

CAS缺点:

ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3A。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 无需同步方案

要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。

  • 可重入代码
    可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
    可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。
    (synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)
  • 线程本地存储
    如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
    符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。

死锁的例子

/** 
* 一个简单的死锁类 
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒 
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒 
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; 
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定; 
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 
*/  
public class DeadLock implements Runnable {  
    public int flag = 1;  
    //静态对象是类的所有对象共享的  
    private static Object o1 = new Object(), o2 = new Object();  
    @Override  
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (o1) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o2) {  
                    System.out.println("1");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (o2) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o1) {  
                    System.out.println("0");  
                }  
            }  
        }  
    }  
  
    public static void main(String[] args) {  
          
        DeadLock td1 = new DeadLock();  
        DeadLock td2 = new DeadLock();  
        td1.flag = 1;  
        td2.flag = 0;  
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。  
        //td2的run()可能在td1的run()之前运行  
        new Thread(td1).start();  
        new Thread(td2).start();  
  
    }  
}  


public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

线程死锁,解除线程死锁有哪几种方式?

死锁的四个必要条件以及处理策略

多个线程同时被阻塞,它们中的一个或者全部都在等待某 个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

产生死锁的原因:

  • 竞争资源
  • 请求和释放资源的顺序不当,也同样会产生进程死锁

产生死锁的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。
  • 请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其它进程占有,此时请求进程阻塞,但又对自己获得的其它资源保持不放。
  • 不剥夺条件:指进程已获得资源,在使用完之前,不能被剥夺,只能在使用完时由自己释放。
  • 循环等待条件:指在发生死锁时,必然存在一个进程—资源的环形链

解决死锁的策略

1、死锁预防:破坏导致死锁必要条件中的任意一个就可以预防死锁。

  • 破坏保持和等待条件:一次性申请所有资源,之后不再申请资源,如果不满足资源条件则得不到资源分配。(就是系统中不允许进程在获得某种资源的情况下,申请其他资源。即阻止进程在持有资源的同时申请其他资源。)
  • 破坏不可剥夺条件:就是允许对资源实行抢夺。当一个进程获得某个不可剥夺的资源时,提出新的资源申请,若不满足,则释放所有资源;(如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。)
  • 破坏循环等待条件: 按某一顺序申请资源,释放资源则反序释放。(是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。)

2、死锁避免: 进程在每次申请资源时判断这些操作是否安全。

  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁;
  • 尽量减少同步的代码块

3、死锁检测: 判断系统是否属于死锁的状态,如果是,则执行死锁解除策略。

4、死锁解除: 将某进程所占资源进行强制回收,然后分配给其他进程。(与死锁检测结合使用的)

挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。

在这里插入图片描述
2、使用jstack 进程进程号 找到死锁信息
在这里插入图片描述

线程和进程

系统中运行的一个程序,每个进程都拥有独立的地址空间,一个进程无法访问另一个进程资源,如果想让一个进程访问另一个进程的资源,需要使用进程间通信。

  • 匿名管道:半双工,只能在父子进程间通信
  • 有名管道: 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道可以在不同进程间通信

一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

根本区别

  • 进程是操作系统资源分配的基本单位,
  • 线程是处理器任务调度和执行的基本单位
  • 进程是程序的一次执行,线程是进程中执行的一段程序片段;
  • 进程间是独立的,不能共享内存空间和上下文,而线程可以;

线程和进程

方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。

程序如何开始运行:CPU读指令–PC(存储指令地址)–读数据(Register)–计算(ALU)–回写–下一条
线程切换:Context Switch Cpu保存现场 执行新线程,恢复现场,继续执行原现场这样的一个过程

上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。

  • 直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
  • 间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小

在这里插入图片描述
线程调度

  • 抢占式调度:指的是每条线程执行的时间、线程的切换都由系统控制,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

  • 协同式调度:指某一线程执行完后主动通知系统切换到另一线程上执行,线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃
    线程让出cpu的情况:1、当前运行线程主动放弃CPU,例如调用yield()方法2、当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上3、当前运行线程结束,即运行完run()方法里面的任务
    在这里插入图片描述
    线程的创建

  • 继承 Thread 类,重写 run 方法

  • 实现 Runnable 接口,实现 run 方法

  • 实现 Callable 接口,实现 call 方法

class ThreadTest {
	public static void main(String[] args) throws Exception{
		//1、
		MyThread thread = new MyThread();
		thread.start();
		//2、
		MyRunnable runnable = new MyRunnable();
		new Thread(runnable).start();
		//3、
		MyCallable callable = new MyCallable();
		// 定义返回结果
		FutureTask<String> result = new FutureTask(callable);
		// 执行程序
		new Thread(result).start();
		// 输出返回结果
		System.out.println(result.get());
	}
}
class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("Thread");
	}
}
class MyRunnable implements Runnable {
	@Override
	public void run() {
		System.out.println("Runnable");
	}
}
class MyCallable implements Callable {
	@Override
	public String call() {
		System.out.println("Callable");
		return "Success";
	}
}

yield 方法是让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态。

JVM(Java 虚拟机)中的垃圾回收线程属于守护线程

线程的切换
进程和线程的深入理解

wait() sleep()

  • 存在类的不同:sleep() 来自 Thread,wait() 来自 Object。
  • 释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

interrupt,interrupted与isInterrupted方法的区别? 如何停止一个正在运行的线程

1、interrupt()方法的作用实际上是:在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞状态。

2、interrupted()调用的是currentThread().isInterrupted(true)方法,即说明是返回当前 线程的是否已经中断的状态值,而且有清理中断状态的机制。测试当前线程是否已经中断,线程的中断状态由该方法清除。即如果连续两次调用该方法, 则第二次调用将返回false(在第一次调用已清除flag后以及第二次调用检查中断状态之前, 当前线程再次中断的情况除外)所以,interrupted()方法具有清除状态flag的功能

3、isInterrupted()调用的是isInterrupted(false)方法,意思是返回线程是否已经中断的状态,它没有清理中断状态的机制。

interrupt() 方法用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中 断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态位并做处理。

interrupted() 检测当前线程是否已经中断,是则返回true,否则false,并清除中断状态。换言之,如果该方法被连续调用两次,第二次必将返回false,除非在第一次与第二次的瞬间线程再次被中断。如果中断调用时线程已经不处于活动状态,则返回false。

isInterrupted() 检测当前线程是否已经中断,是则返回true,否则false。中断状态不受该方法的影响。如果中断调用时线程已经不处于活动状态,则返回false。

在java中有以下3种方法可以终止正在运行的线程:

使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都 是过期作废的方法

使用interrupt()方法中断线程

讲一下volatile关键字的作用

1、保证了不同线程对该变量操作的内存可见性
2、禁止指令重排序

当写一个volatile变量时,JMM将本地内存更改的变量写回到主内存中。

当取一个volatile变量时,JMM将使线程对应的本地内存失效,然后线程将从主内存读取共 享变量。

volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在 JVM 底层是基于内存屏障实现的。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的本地内存中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过本地内存这一步,所以就不会有可见性问题

对 volatile 变量进行写操作时,会在写操作后加一条 store 屏障指令,将工作内存中的共享变量刷新回主内存;

对 volatile 变量进行读操作时,会在读操作前加一条 load屏障指令,从主内存中读取共享变量;

volatile可以通过内存屏障防止指令重排序问题。硬件层面的内存屏障分为读屏障和写屏障。

对于读屏障来说,在指令前插入读屏障,可以让高速缓存中的数据失效,强制重新从主内存 加载数据。

对于写屏障来说,在指令后插入写屏障,能让写入缓存中的最新数据更新写入主内存,让其 他线程可见。

synchronized 作用,讲一讲底层实现

synchronized关键字解决的是多个线程之间访问资源的同步性,调用操作系统内核态做同步,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

synchronized关键字最主要的三种使用方式:

修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例。因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized 关键字底层原理属于 JVM 层面。

① synchronized 同步语句块的情况:synchronized 同步语句块的实现使用的是monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

② synchronized 修饰方法的的情况:JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized关键字和 volatile关键字的区别

volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代 码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提 升,实际开发中使用 synchronized 关键字的场景还是更多一些。

多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞,volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字 两者都能保证。

volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性

ReetrantLock

ReetrantLock实现方式

锁的获取过程:

  1. 通过cas操作来修改state状态,表示争抢锁的操作,如果能够获取到锁,设置当前获得锁状态的线程。compareAndSetState(0, 1)
  2. 如果没有获取到锁,尝试去获取锁。acquire(1)
  • 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false。如果是同 一个线程来获得锁,则直接增加重入次数,并返回true。
  • 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node,添加到AQS队列尾部
  • acquireQueued,将Node作为参数,通过自旋去尝试获取锁。(如果前驱为head才有资格进行锁的抢夺。)
  • 如果获取锁失败,则挂起线程

锁的释放过程:

1、释放锁

2、如果锁能够被其他线程获取,唤醒后继节点中的线程。一般情况下只要唤醒后继结点的 线程就行了,但是后继结点可能已经取消等待,所以从队列尾部往前回溯,找到离头结 点最近的正常结点,并唤醒其线程。

在获得同步锁时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在 队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状 态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

ReetrantLock 和 synchronized的区别

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代
    码块等;

ReetrantLock 和 synchronized的区别

读写锁

读写锁是用AQS来实现的,ReadWriteLock有公平和非公平的实现,都是继承sync类。读写锁是一个共享锁的状态。

主要基于int类型的state关键字,状态为0表示锁空闲,

公平跟非公平的区别

公平锁:当前线程发现已经有线程再排队获取锁了,那么它必须排队,除了一种情况,线程已经占有锁,此次就是重入,不用排队

非公平锁:不需要排队就可以尝试获取锁。但是有一种情况需要排队,当前全局处于读锁状态,且等待队列中第一个线程想获取写锁,后面再有读锁的时候,这个情况就当前线程就要去排队了,如果读锁都可以去抢的话,写锁就没机会了。

在这里插入图片描述

AQS

AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。
AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。
在这里插入图片描述
在这里插入图片描述

AQS是抽象的,ReentrantLock ,Semaphore,CountDownLatch 全部都是实现这个AQS的一个子类,重写一些方法,AQS定义的一些方法默认是没有实现的,要求子类去实现的,所以就是典型的模板方式。

AQS是一个用来构建锁和同步器的框架。AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请 求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

状态信息通过protected类型的getState,setState,compareAndSetState进行操作。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

//返回同步状态的当前值
protected final int getState() {
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

AQS定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁

  • 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  • 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

Share(共享):多个线程可同时执行。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线 程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state 会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

ReentrantLock 就是基于 AQS 实现的,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。

和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。

ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。


Semaphore(信号量)-允许多个线程同时访问

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源

Semaphore 与 CountDownLatch 一样,也是共享锁的一种实现。它默认构造 AQS 的 state 为 permits当执行任务的线程数量超出 permits,那么多余的线程将会被放入阻塞队列 Park,并自旋判断 state 是否大于 0。只有当 state 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 release() 方法,release() 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 如此,每次只有最多不超过 permits 数量的线程能自旋成功,便限制了执行任务线程的数量。

Semaphore 有两种模式,公平模式和非公平模式。

CountDownLatch (倒计时器)

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state = 0,如果 state = 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。

CountDownLatch 的两种典型用法

1、 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown(),当计数器的值变为 0 时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

CountDownLatch 的不足:CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。

CyclicBarrier(循环栅栏)

CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的.

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

public CyclicBarrier(int parties) {
    this(parties, null);
}

//用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。

CyclicBarrier 的应用场景

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。

比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。

CyclicBarrier 和 CountDownLatch 的区别

  • CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。
  • 对于 CountDownLatch 来说,重点是“一个线程()等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
  • CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

线程池作用?Java 线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?线程池的工作机制?

线程池

线程池

1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池通过 ThreadPoolExecutor 的方式进程创建

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

//线程池使用
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));

threadPoolExecutor.execute(new Runnable() {
	@Override
	public void run() {
		// 执行线程池
		System.out.println("Hello, Java.");
	}
});

3 个最重要的参数:

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。

maximumPoolSize : 线程池中最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞(放入任务队列)。

workQueue : 线程池中的任务队列,当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor 其他常见参数:

  • keepAliveTime :当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,非核心线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
  • unit:keepAliveTime参数的时间单位。
  • threadFactory:executor 创建新线程的时候会用到。
  • handler:饱和策略。

ThreadPoolExecutor饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时, ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务。也就是直接在调用execute 方法的线程中运行( run )被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选 择这个策略。
  • ThreadPoolExecutor.DiscardPolicy不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求

线程池拒绝策略分别使用在什么场景?

1、AbortPolicy中止策略:丢弃任务并抛出RejectedExecutionException异常。使用场景:这个就没有特殊的场景了,但是有一点要正确处理抛出的异常。当自己自定 义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前 的执行流程。

2、DiscardPolicy丢弃策略:ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出 异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。使用场景:如果你提交的任务无关紧要,你就可以使用它 。

3、DiscardOldestPolicy弃老策略丢弃队列最前面的任务,然后重新提交被拒绝的任务。使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,能想到的场景就是, 发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。

4、CallerRunsPolicy调用者运行策略:由调用线程处理该任务。使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用。

线程池的工作过程

1、提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达 到线程数,则创建核心线程处理任务;否则,就执行下一步;

2、接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,执 行下一步;

3、接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则 创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出RejectedExecutionException异常。

execute() 和 submit() 都是用来执行线程池的,区别在于 submit() 方法可以接收线程池执行的返回值。

线程池关闭,可以使用 shutdown() 或 shutdownNow() 方法

  • shutdown():不会立即终止线程池,而是要等所有任务队列中的任务都执行完后才会终止。执行完 shutdown 方法之后,线程池就不会再接受新任务了。
  • shutdownNow():执行该方法,线程池的状态立刻变成 STOP 状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,执行此方法会返回未执行的任务。

Executors可以创建以下六种线程池

1、newFixedThreadPool:创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。

使用了 LinkedBlockingQueue 作为任务队列

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 3; i++) {
            fixedThreadPool.execute(() -> {
                System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTim
                       eFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

2、newSingleThreadExecutor:创建一个单线程线程池。

单线程的线程池存在的意义:单线程线程池提供了队列功能,如果有多个任务会排队执行,可以保证任务执行的顺序性。单线程线程池也可以重复利用已有线程,减低系统创建和销毁线程的性能开销。

3、newCachedThreadPool:短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果 60 秒没被使用,则会被移除缓存。

没有核心线程,直接向 SynchronousQueue 中提交任务,如果有空闲线程,就去取出任务执行。如果没有空闲线程,就新建一个。执行完任务的线 程有 60 秒生存时间,如果在这个时间内可以接到新任务,才可以存活下去。

4、 newScheduledThreadPool:创建一个数量固定的线程池,支持执行定时性或周期性任务

5、SingleThreadScheduledExecutor():此线程池就是单线程的newScheduledThreadPool。

6、WorkStealingPool(n):Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序

不建议使用Executors创建线程

在这里插入图片描述

阻塞队列有几种

用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下 阻塞队列:

  1. ArrayBlockingQueue(有界队列):基于数组结构的有界阻塞队列,按FIFO排序任务;
  2. LinkedBlockingQuene(有/无界队列(基于链表的,传参就是有界,不传就是无界): 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
  3. SynchronousQuene(同步移交队列(需要一个线程调用put方法插入值,另一个线程调 用take方法删除值)):一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
  4. PriorityBlockingQuene具有优先级的无界阻塞队列

ThreadLocal 是什么,应用场景是什么,原理是怎样的?

用于解决多线程间的数据隔离问题,也就是说ThreadLocal 会为每一个线程创建一个单独的变量副本。把共享数据的可见范围限制在同一个线程之内,因此 ThreadLocal 是线程安全的,每个线程都有属于自己的变量。

ThreadLocal 的子类 InheritableThreadLocal 就可以实现线程间信息共享

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使用 get()和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在ThreadLocal上, ThreadLocal 可以理解为只是ThreadLocalMap 的封装,传递了变量ThreadLocalMap 值。我们可以把ThrealLocal理解为ThreadLocal 类实现的定制化的 HashMap 。类中可以通过Thread.currentThread() 获取到当前线程对象后,直接通过getMap(Thread t) 可以访问到该线程的ThreadLocalMap 对象。每个 Thread 中都具备一个 ThreadLocalMap ,而 ThreadLocalMap 可以存储以ThreadLocal 为 key ,Object对象为 value 的键值对。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {}   

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread 内部都是使用仅有那个ThreadLocalMap 存放数据的, ThreadLocalMap 的 key 就是 ThreadLocal 对象,value 就是ThreadLocal 对象调用set 方法设置的值。

ThreadLocal 最典型的使用场景有两个

  • ThreadLocal 可以用来管理 Session,因为每个人的信息都是不一样的,所以就很适合用 ThreadLocal 来管理;
  • 数据库连接,为每一个线程分配一个独立的资源,也适合用 ThreadLocal来实现。

每次获得connection都需要浪费cpu资源和内存资源,是很浪费资源的。所以诞生了数据库连接池。数据库连接池实现原理如下:

pool.getConnection(),都是先从threadlocal里面拿的,如果threadlocal里面有,则用,保证线程里的多个dao操作,用的是同一个connection,以保证事务。如果新线程,则将新的connection放在threadlocal里,再get给到线程。

将connection放进threadlocal里的,以保证每个线程从连接池中获得的都是线程自己的connection。

ThreadLocal threadLocal = new ThreadLocal();
// 存值
threadLocal.set(Arrays.asList("老王", "Java 面试题"));
// 取值
List list = (List) threadLocal.get();
System.out.println(list.size());
System.out.println(threadLocal.get());
//删除值
threadLocal.remove();
System.out.println(threadLocal.get());

ThreadLocal类为什么要加上private static修饰?

首先,private修饰与ThreadLocal本身没有关系,private更多是在安全方面进行考虑。static 修饰这个变量,这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此 类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。(设置为static可以避免每个线程从任务队列中获取task后重复创建ThreadLocal所关联的对象)可以解决内存泄露问题(看下一问)。

ThreadLocal有什么缺陷?如果线程池的线程使用ThreadLocal会有什么问题?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用(弱引用,生命周期只能存活到下次GC前 ),而 value 是强引用。所以,如果没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露

ThreadLocalMap实现中已经考虑了这种情况,在调用 set() 、 get() 、 remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用remove() 方法

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。Entry继承自WeakReference( 弱引用,生命周期只能存活到下次GC前 ),但只有Key是弱引用类型的, Value并非弱引用。由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了 一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收。当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。( ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在 )。

为了防止此类情况的出现,我们有两种手段:

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove()方法清除线程共享变量;

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots 分析后就变成不可达了,下次GC的时候就可以被回收

2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

ThreadLocal 和 Synchonized 有什么区别?

Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种 “以时间换空间” 的方式;而 ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 “以空间换时间” 的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

德玛西亚!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值