JavaEE - 多线程(进阶)

多线程(进阶)

常见的锁策略

锁策略,和普通程序猿基本没啥关系.和"实现锁"的人才有关系的~
这里所提到的锁策略,和Java本身没关系.适用于所有和"锁”相关的情况~~

1.悲观锁vs乐观锁

悲观锁:预期锁冲突的概率很高.
乐观锁:预期锁冲突的概率很低.

悲观锁,做的工作更多,付出的成本更多,更低效.
乐观锁,做的工作更少,付出的成本更低,更高效.

2读写锁vs普通的互斥锁

对于普通的互斥锁只有两个操作,加锁和解锁~
只要两个线程针对同一个对象加锁,就会产生互斥~~

对于读写锁来说,分成了三个操作.
加读锁 -> 如果代码只是进行读操作,就加读锁
加写锁-> 如果代码中进行了修改操作,就加写锁
解锁

读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.

读写锁最主要用在 "频繁读,不频繁写"的场景中.
多线程同时读同—个变量不会有线程安全问题!!
而且在很多场景中,都是读操作多,写操作少~(数据库索引)

3.重量级锁vs轻量级锁

(和上面的悲观乐观有一定重叠)(处理锁冲突的结果)
重量级锁,就是做了更多的事情,开销更大.
轻量级锁,做的事情更少,开销更小.

也可以认为,通常情况下,悲观锁─般都是重量级锁,乐观锁─般都是轻量级锁.(不绝对)

在使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的 mutex接口),此时一般认为这是重量级锁.(操作系统的锁会在内核中做很多的事情,比如让线程阻塞等待…)
如果锁是纯用户态实现的,此时一般认为这是轻量级锁(用户态的代码更可控,也更高效)

4.挂起等待锁vs自旋锁

挂起等待锁,往往就是通过内核的一些机制来实现的.往往较重.[重量级锁的一种典型实现]
自旋锁,往往就是通过用户态代码来实现的.往往教轻.[轻量级锁的一种典型实现]

上述的这些锁策略,彼此之间并不是完全独立的.

5.公平锁vs非公平锁

公平锁:多个线程在等待一把锁的时候~~,谁是先来的,谁就能先获取到这个锁(遵守先来后到)

非公平锁:多个线程在等待—把锁的时候,不遵守先来后到~~(每个等待的线程获取到锁的概率都是均等的)

读写锁
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

一个线程对于数据的访问,主要存在两种操作:读数据和写数据.

  • 两个线程都只是读一个数据,此时并没有线程安全问题.直接并发的读取即可.
  • 两个线程都要写一个数据,有线程安全问题.
  • 一个线程读另外一个线程写,也有线程安全问题.

读写锁就是把读操作和写操作区分对待. Java标准库提供了ReentrantReadWriteLock类,实现了读写锁.

  • ReentrantReadWriteLock.ReadLock类表示一个读锁.这个对象提供了 lock / unlock方法进行加锁解锁.
  • ReentrantReadWriteLock.WriteLock类表示一个写锁.这个对象也提供了 lock /
    unlock方法进行加锁解锁.

其中,

  • 读加锁和读加锁之间,不互斥.
  • 写加锁和写加锁之间,互斥.
  • 读加锁和写加锁之间,互斥.

注意,只要是涉及到 “互斥”,就会产生线程的挂起等待.一旦线程挂起,再次被唤醒就不知道隔了多久了.
因此尽可能减少 "互斥"的机会,就是提高效率的重要途径.

读写锁特别适合于 "频繁读,不频繁写"的场景中. (这样的场景其实也是非常广泛存在的).
比如比特的教务系统.
每节课老师都要使用教务系统点名,点名就需要查看班级的同学列表(读操作).这个操作可能要每周执行好几次.而什么时候修改同学列表呢(写操作)?就新同学加入的时候.可能一个月都不必改一次.

再比如,同学们使用教务系统查看作业(读操作),一个班级的同学很多,读操作一天就要进行几十次上百次.但是这一节课的作业,老师只是布置了一次(写操作)

Synchronized不是读写锁.

理解用户态 vs内核态
想象去银行办业务.
在窗口外,自己做,这是用户态.用户态的时间成本是比较可控的.
在窗口内,工作人员做,这是内核态.内核态的时间成本是不太可控的.
如果办业务的时候反复和工作人员沟通,还需要重新排队,这时效率是很低的.

相关面试题

1)介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 "频繁读,不频繁写"的场景中.

2) synchronized是可重入锁么?
是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数).如果发现当前加锁的线程就是持有锁的线程,则直接计数自增.
在这里插入图片描述

CAS(略,详情见讲义)

Synchronized原理

基本特点
结合上面的锁策略,我们就可以总结出, Synchronized具有以下特性(只考虑 JDK 1.8):

  1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.(根据锁竞争的激烈程度,自适应)
  2. 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁.(如果锁冲突比较严重,就会变成重量级锁)(根据锁竞争的激烈程度,自适应)
  3. 实现轻量级锁的时候大概率用到的自旋锁策略,重量级锁的部分基于挂起等待锁来实现
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁,只是一个普通的互斥锁

加锁工作过程
JVM将 synchronized锁分为无锁、偏向锁、轻量级锁、重量级锁状态。会根据情况,进行依次升级。
在这里插入图片描述

在这里插入图片描述
1)偏向锁
第一个尝试加锁的线程,优先进入偏向锁状态.
偏向锁不是真的 “加锁”,只是给对象头中做一个 “偏向锁的标记”,记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁,那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别当前申请锁的线程是不是之前记录的线程),那就取消原来的偏向锁状态,进入一般的轻量级锁状态.

偏向锁本质上相当于 “延迟加锁” .能不加锁就不加锁,尽量来避免不必要的加锁开销.
但是该做的标记还是得做的,否则无法区分何时需要真正加锁.

举个栗子理解偏向锁

假设男主是一个锁,女主是一个线程.如果只有这一个线程来使用这个锁,那么男主女主即使不领证结婚(避免了高成本操作),也可以一直幸福的生活下去.但是女配出现了,也尝试竞争男主,此时不管领证结婚这个操作成本多高,女主也势必要把这个动作完成了,让女配死心.

2)轻量级锁
随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS来实现.

  • 通过 CAS检查并更新一块内存 (比如 null =>该线程引用)
  • 如果更新成功,则认为加锁成功
  • 如果更新失败,则认为锁被占用,继续自旋式的等待(并不放弃 CPU).

自旋操作是一直让 CPU空转,比较浪费 CPU资源.
因此此处的自旋不会一直持续进行,而是达到一定的时间/重试次数,就不再自旋了.
也就是所谓的 “自适应”

3)重量级锁
如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .

  • 执行加锁操作,先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用,则加锁成功,并切换回用户态.
  • 如果该锁被占用,则加锁失败.此时线程进入锁的等待队列,挂起.等待被操作系统唤醒.
  • 经历了一系列的沧海桑田,这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒这个线程,尝试重新获取锁.

相关面试题
1)什么是偏向锁?
偏向锁不是真的加锁,而只是在锁的对象头中记录一个标记(记录该锁所属的线程).如果没有其他线程参与竞争锁,那么就不会真正执行加锁操作,从而降低程序开销.一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态.
2) synchronized实现原理是什么?
参考上面的 synchronized 原理章节全部内容.

其他优化操作:
1.锁膨胀/锁升级
2.锁粗化
3.锁消除
(详见讲义)

Callable接口

Callable的用法
Callable是一个 interface .相当于把线程封装了一个 “返回值”.方便程序猿借助多线程的方式计算结果.

**代码示例:**创建线程计算 1 + 2 + 3 + … + 1000,不使用 Callable版本

  • 创建一个类 Result ,包含一个 sum表示最终结果, lock表示线程同步使用的锁对象.
  • main方法中先创建 Result实例,然后创建一个线程 t.在线程内部计算 1 + 2 + 3 + … + 1000.
  • 主线程同时使用 wait等待线程 t计算结束. (注意,如果执行到 wait之前,线程 t已经计算完了,就不必等待了).
  • 当线程 t计算完毕后,通过 notify唤醒主线程,主线程再打印结果.
static class Result {
    
    
   public int sum = 0;
   public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    
    
   Result result = new Result();
   Thread t = new Thread() {
    
    
       @Override
       public void run() {
    
    
           int sum = 0;
           for (int i = 1; i <= 1000; i++) {
    
    
               sum += i;
          }
           synchronized (result.lock) {
    
    
               result.sum = sum;
               result.lock.notify();
          }
      }
  };
   t.start();
   synchronized (result.lock) {
    
    
       while (result.sum == 0) {
    
    
           result.lock.wait();
      }
       System.out.println(result.sum);
  }
}

可以看到,上述代码需要一个辅助类 Result,还需要使用一系列的加锁和 wait notify操作,代码复杂,容易出错.

代码示例: 创建线程计算 1 + 2 + 3 + … + 1000,使用 Callable版本

  • 创建一个匿名内部类,实现 Callable接口. Callable带有泛型参数.泛型参数表示返回值的类型.
  • 重写 Callable的 call方法,完成累加的过程.直接通过返回值返回计算结果.
  • 把 callable实例使用 FutureTask包装一下.
  • 创建线程,线程的构造方法传入 FutureTask .此时新线程就会执行 FutureTask内部的
    Callable的call方法,完成计算.计算结果就放到了 FutureTask对象中.
  • 在主线程中调用 futureTask.get()能够阻塞等待新线程计算完毕.并获取到 FutureTask中的结果.
Callable<Integer> callable = new Callable<Integer>() {
    
    
   @Override
   public Integer call() throws Exception {
    
    
       int sum = 0;
       for (int i = 1; i <= 1000; i++) {
    
    
           sum += i;
      }
       return sum;
  }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);

可以看到,使用 Callable和 FutureTask之后,代码简化了很多,也不必手动写线程同步代码了.

理解 Callable
Callable和 Runnable相对,都是描述一个 “任务”. Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务.Callable通常需要搭配 FutureTask来使用. FutureTask用来保存 Callable的返回结果.因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定.FutureTask就可以负责这个等待结果出来的工作.

理解 FutureTask
想象去吃麻辣烫.当餐点好后,后厨就开始做了.同时前台会给你一张 “小票” .这个小票就是FutureTask.后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.

相关面试题
介绍下 Callable是什么

Callable是一个 interface .相当于把线程封装了一个 “返回值”.方便程序猿借助多线程的方式计算结果.
Callable和 Runnable相对,都是描述一个 “任务”. Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务.
Callable通常需要搭配 FutureTask来使用. FutureTask用来保存 Callable的返回结果.因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定.
FutureTask就可以负责这个等待结果出来的工作.

JUC(java.util.concurrent)的常见类(略,详情见讲义)
信号量 Semaphore、原子类、CountDownLatch(略,详情见讲义)

JUC(java.util.concurrent)的常见类

在这里插入图片描述

ReentrantLock
可重入互斥锁.和 synchronized定位类似,都是用来实现互斥效果,保证线程安全.ReentrantLock也是可重入锁. "Reentrant"这个单词的原意就是 “可重入”

ReentrantLock的用法:

  • lock():加锁,如果获取不到锁就死等.
  • trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁.
  • unlock():解锁
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();  
try {
    
        
	// working    
} finally {
    
        
	lock.unlock()    
}  

ReentrantLock和 synchronized的区别

  • synchronized是一个关键字,是 JVM内部实现的(大概率是基于 C++实现). ReentrantLock是标准库的一个类,在JVM外实现的(基于 Java实现).
  • synchronized使用时不需要手动释放锁.ReentrantLock使用时需要手动释放.使用起来更灵活,但是也容易遗漏unlock.
  • synchronized在申请锁失败时,会死等. ReentrantLock可以通过 trylock的方式等待一段时间就放弃.
  • synchronized是非公平锁, ReentrantLock默认是非公平锁.可以通过构造方法传入一个 true开启公平锁模式
  • 更强大的唤醒机制. synchronized是通过 Object的 wait / notify实现等待-唤醒.每次唤醒的是一个随机等待的线程.ReentrantLock搭配 Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候,使用 synchronized,效率更高,自动释放更方便.
  • 锁竞争激烈的时候,使用 ReentrantLock,搭配 trylock更灵活控制加锁的行为,而不是死等.
  • 如果需要使用公平锁,使用 ReentrantLock.

相关面试题
1)线程同步的方式有哪些?
synchronized, ReentrantLock, Semaphore等都可以用于线程同步.

2)为什么有了 synchronized还需要 juc下的 lock?
以 juc的 ReentrantLock为例,

  • synchronized使用时不需要手动释放锁. ReentrantLock使用时需要手动释放.使用起来更灵活,
  • synchronized在申请锁失败时,会死等. ReentrantLock可以通过 trylock的方式等待一段时间就放弃.
  • synchronized是非公平锁, ReentrantLock默认是非公平锁.可以通过构造方法传入一个true开启公平锁模式.
  • synchronized是通过 Object的 wait / notify实现等待-唤醒.每次唤醒的是一个随机等待的线程.ReentrantLock搭配 Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.

原子类 (略)
信号量 Semaphore(略)

信号量听说过么?之前都用在过哪些场景下?
信号量,用来表示 “可用资源的个数”.本质上就是一个计数器.
使用信号量可以实现 “共享锁”,比如某个资源允许 3个线程同时使用,那么就可以使用 P操作作为加锁, V操作作为解锁,前三个线程的 P操作都能顺利返回,后续线程再进行 P操作就会阻塞等待,直到前面的线程执行了 V操作.

CountDownLatch(略)

线程安全的集合类

原来的集合类,大部分都不是线程安全的.
Vector, Stack, HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的.

多线程环境使用 ArrayList
1)自己使用同步机制 (synchronized或者 ReentrantLock)
前面做过很多相关的讨论了.此处不再展开.

  1. Collections.synchronizedList(new ArrayList);
    synchronizedList是标准库提供的一个基于 synchronized进行线程同步的 List.
    synchronizedList的关键操作上都带有 synchronized

3)使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。

  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
  • 添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下,性能很高,不需要加锁竞争.
缺点:
1.占用内存较多.
2.新写的数据不能被第一时间读取到.

多线程环境使用队列

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列
  2. LinkedBlockingQueue
    基于链表实现的阻塞队列
  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
  4. TransferQueue
    最多只包含一个元素的阻塞队列

多线程环境使用哈希表
HashMap本身不是线程安全的.
在多线程环境下使用哈希表可以使用:
Hashtable(不推荐)
ConcurrentHashMap(推荐)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1) Hashtable
只是简单的把关键方法加上了 synchronized关键字.

在这里插入图片描述
这相当于直接针对 Hashtable对象本身加锁.

  • 如果多线程访问同一个 Hashtable就会直接造成锁冲突.
  • size属性也是通过 synchronized来控制同步,也是比较慢的.
  • 一旦触发扩容,就由该线程完成整个扩容过程.这个过程会涉及到大量的元素拷贝,效率会非常低.

在这里插入图片描述

在这里插入图片描述
2) ConcurrentHashMap
相比于 Hashtable做出了一系列的改进和优化.以 Java1.8为例

  • 读操作没有加锁(但是使用了 volatile保证从内存读取结果),只对写操作进行加锁.加锁的方式仍然是是用synchronized,但是不是锁整个对象,而是 “锁桶” (用每个链表的头结点作为锁对象),大大降低了锁冲突的概率.
  • 充分利用 CAS特性.比如 size属性通过 CAS来更新.避免出现重量级锁的情况.
  • 优化了扩容方式:化整为零
  1. 发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去.
  2. 扩容期间,新老数组同时存在.
  3. 后续每个来操作 ConcurrentHashMap的线程,都会参与搬家的过程.每个操作负责搬运一小部分元素.
  4. 搬完最后一个元素再把老数组删掉.
  5. 这个期间,插入只往新数组加.
  6. 这个期间,查找需要同时查新数组和老数组

在这里插入图片描述
参考资料:https://blog.csdn.net/u010723709/article/details/48007881

相关面试题
1) ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁.目的是为了进一步降低锁冲突的概率.为了保证读到刚修改的数据,搭配了volatile关键字.

2)介绍下 ConcurrentHashMap的锁分段技术?
这个是 Java1.7中采取的技术. Java1.8中已经不再使用了.简单的说就是把若干个哈希桶分成一个"段" (Segment),针对每个段分别加锁.目的也是为了降低锁竞争的概率.当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争.

3) ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).将原来数组 +链表的实现方式改进成数组 +链表 /红黑树的方式.当链表较长的时候(大于等于8个元素)就转换成红黑树.

4) Hashtable和HashMap、ConcurrentHashMap之间的区别?
HashMap:线程不安全. key允许为 null
Hashtable:线程安全.使用 synchronized锁 Hashtable对象,效率较低. key不允许为 null.
ConcurrentHashMap:线程安全.使用 synchronized锁每个链表头结点,锁冲突概率低,充分利用CAS机制.优化了扩容方式. key不允许为 null

死锁

死锁是什么
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
三种情况:
1.一个线程加两次锁
2.两个线程两把锁,互相加锁
3.多个线程 哲学家就餐

举个栗子理解死锁

滑稽老哥和女神一起去饺子馆吃饺子.吃饺子需要酱油和醋.
滑稽老哥抄起了酱油瓶,女神抄起了醋瓶.
滑稽:你先把醋瓶给我,我用完了就把酱油瓶给你.
女神:你先把酱油瓶给我,我用完了就把醋瓶给你.
如果这俩人彼此之间互不相让,就构成了死锁.
酱油和醋相当于是两把锁,这两个人就是两个线程.

为了进一步阐述死锁的形成,很多资料上也会谈论到 “哲学家就餐问题”.

  • 有个桌子,围着一圈哲♂家,桌子中间放着一盘意大利面.每个哲学家两两之间,放着一根筷子.
    在这里插入图片描述
  • 每个哲♂家只做两件事:思考人生或者吃面条.思考人生的时候就会放下筷子.吃面条就会拿起左右两边的筷子(先拿起左边,再拿起右边).

在这里插入图片描述

  • 如果哲♂家发现筷子拿不起来了(被别人占用了),就会阻塞等待.

在这里插入图片描述

  • [关键点在这]假设同一时刻,五个哲♂家同时拿起左手边的筷子,然后再尝试拿右手的筷子,就会发现右手的筷子都被占用了.由于哲♂家们互不相让,这个时候就形成了死锁

在这里插入图片描述

死锁是一种严重的 BUG!!导致一个程序的线程 “卡死”,无法正常工作!

如何避免死锁
死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
其中最容易破坏的就是 “循环等待”.

  • 破坏循环等待

最常用的一种死锁阻止技术就是锁排序.假设有 N个线程尝试获取 M把锁,就可以针对 M把锁进行编号(1, 2, 3…M).
N个线程尝试获取锁的时候,都按照固定的按编号由小到大顺序来获取锁.这样就可以避免环路等待.

可能产生环路等待的代码:
两个线程对于加锁的顺序没有约定,就容易产生环路等待.

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    
    
   @Override
   public void run() {
    
    
       synchronized (lock1) {
    
    
           synchronized (lock2) {
    
    
               // do something...
          }
      }
  }
};
t1.start();
Thread t2 = new Thread() {
    
    
   @Override
   public void run() {
    
    
       synchronized (lock2) {
    
    
           synchronized (lock1) {
    
    
               // do something...
          }
      }
  }
};
t2.start();

不会产生环路等待的代码:
约定好先获取 lock1,再获取 lock2 ,就不会环路等待.

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
    
    
   @Override
   public void run() {
    
    
       synchronized (lock1) {
    
    
           synchronized (lock2) {
    
    
               // do something...
          }
      }
  }
};
t1.start();
Thread t2 = new Thread() {
    
    
   @Override
   public void run() {
    
    
       synchronized (lock1) {
    
    
           synchronized (lock2) {
    
    
               // do something...
          }
      }
  }
};
t2.start();

相关面试题
谈谈死锁是什么,如何避免死锁,避免算法?实际解决过没有?
参考整个 "死锁"章节

其他常见面试题

1)谈谈 volatile关键字的用法?
volatile能够保证内存可见性.强制从主内存中读取数据.此时如果有其他线程修改被 volatile修饰的变量,可以第一时间读取到最新的值.

2) Java多线程是如何实现数据共享的?
JVM把内存分成了这几个区域:
方法区,堆区,栈区,程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中,就可以让多个线程都能访问到.

3) Java创建线程池的接口是什么?参数 LinkedBlockingQueue的作用是什么?
创建线程池主要有两种方式:

  • 通过 Executors工厂类创建.创建方式比较简单,但是定制能力有限.
  • 通过 ThreadPoolExecutor创建.创建方式比较复杂,但是定制能力强.

LinkedBlockingQueue表示线程池的任务队列.用户通过 submit / execute向这个任务队列中添加任务,再由线程池中的工作线程来执行任务.

4) Java线程共有几种状态?状态之间怎么切换的?

  • NEW:安排了工作,还未开始行动.新创建的线程,还没有调用 start方法时处在这个状态.
  • RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.调用 start方法之后,并正在CPU上运行/在即将准备运行的状态.
  • BLOCKED:使用 synchronized的时候,如果锁被其他线程占用,就会阻塞等待,从而进入该状态.
  • WAITING:调用 wait方法会进入该状态.
  • TIMED_WAITING:调用 sleep方法或者 wait(超时时间)会进入该状态.
  • TERMINATED:工作完成了.当线程 run方法执行完毕后,会处于这个状态.

5)在多线程下,如果对一个数进行叠加,该怎么做?

  • 使用 synchronized / ReentrantLock加锁
  • 使用 AtomInteger原子操作

6) Servlet是否是线程安全的?
Servlet本身是工作在多线程环境下.
如果在 Servlet中创建了某个成员变量,此时如果有多个请求到达服务器,服务器就会多线程进行操作,是可能出现线程不安全的情况的.

7) Thread和Runnable的区别和联系?
Thread类描述了一个线程.
Runnable描述了一个任务.
在创建线程的时候需要指定线程完成的任务,可以直接重写 Thread的 run方法,也可以使用Runnable来描述这个任务.

8)多次start一个线程会怎么样
第一次调用 start可以成功调用.
后续再调用 start会抛出 java.lang.IllegalThreadStateException异常

9)有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?

synchronized加在非静态方法上,相当于针对当前对象加锁.
如果这两个方法属于同一个实例:
线程1能够获取到锁,并执行方法.线程2会阻塞等待,直到线程1执行完毕,释放锁,线程2获取到锁之后才能执行方法内容.
如果这两个方法属于不同实例:
两者能并发执行,互不干扰.

10)进程和线程的区别?
进程是包含线程的.每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间.同一个进程的线程之间共享同一个内存空间.
进程是系统分配资源的最小单位,线程是系统调度的最小单位。

11)引起线程阻塞有哪些原因?阻塞线程怎么被唤醒?

12)Java 中有没有什么方法能够唤醒阻塞Io?

猜你喜欢

转载自blog.csdn.net/qq_43398758/article/details/123797225
今日推荐