Java多线程常见概念

参考资料:https://redspider.gitbook.io/concurrent/

 

进程和线程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。

  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。

  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

 

 线程组(ThreadGroup)

每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。

 

Start和run的区别

Start()会创建一个新的子线程并启动

Run()只是Thread的一个普通方法的调用

 

 

Thread和Runnable关系

Thread是实现了Runnable接口的类,使得run支持多线程

因类的单一继承原则,推荐多使用Runnable接口

 

 1     public static void main(String[] args) {
 2        
 3         new Thread(new Runnable() {
 4             public void run() {
 5                 System.out.println("Runnable running..");
 6             }
 7         }) {
 8             public void run() {
 9                 System.out.println("Thread running..");
10             };
11         }.start();
12     }

输出结果为

Thread running..

继承Thread类,那么在调用start的方法时会去调用Thread的子类的方法

 

如何给run()方法传参

构造函数传参

成员变量传参

回调函数传参

 

 

如何实现处理线程的返回值

主线程等待法(新建一个属性来存返回值,当这个属性还没值的时候,就等待,直到它有值)

使用Thread类的join()阻塞当前线程以等待子线程处理完毕

通过Callable接口实现,通过FutureTask 或线程池获取(推荐)

 

 

Sleep和wait区别

Sleep是Thread类的方法,wait是Object类的方法

Sleep方法可以在任何地方使用

Wait方法只能在synchronized方法或synchronized块中使用

Thread.sleep只会让出CPU,不会导致锁行为的改变

Object.wait不仅让出CPU,还会释放已经占有的同步资源锁

 

 1 public class ThreadTest {
 2     public static void main(String[] args) {
 3         final Object lock = new Object();
 4         new Thread(new Runnable() {
 5             @Override
 6             public void run() {
 7                 System.out.println("A is waiting to get lock");
 8                 synchronized (lock) {
 9                     try {
10                         System.out.println("A get lock");
11                         Thread.sleep(20);
12                         System.out.println("A get do wait method");
13                         Thread.sleep(1000);//只会让出CPU,不会导致锁行为的改变
14                         System.out.println("A is done");
15                     } catch (InterruptedException e) {
16                         e.printStackTrace();
17                     }
18                 }
19             }
20         }).start();;
21         try {
22             Thread.sleep(10);// 让A先执行
23         } catch (InterruptedException e1) {
24             e1.printStackTrace();
25         }
26         new Thread(new Runnable() {
27             @Override
28             public void run() {
29                 System.out.println("B is waiting to get lock");
30                 synchronized (lock) {
31                     try {
32                         System.out.println("B get lock");
33                         System.out.println("B is sleeping 10 ms");
34                         lock.wait(10);//不仅让出CPU,还会释放已经占有的同步资源锁
35                         System.out.println("B is done");
36                     } catch (InterruptedException e) {
37                         e.printStackTrace();
38                     }
39                 }
40             }
41         }).start();;
42     }
43 }
举例

 

 

等待池

假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁

 

 

Notify和notifyAll区别

notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

 

 

yield

当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示

 

 

Interrupt

如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

如果线程处于正常活动状态,那么会将该线程的中断标志设为true,被设置中断标志的线程将继续正常运行,不收影响。

 

 

进阶

原子性、可见性、有序性都应该怎么保证呢?

原子性:JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized。
可见性:Java是利用volatile关键字来保证可见性的,除此之外,final和synchronized也能保证可见性。
有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。

公平和非公平锁

公平锁指:在竞争环境下,先到临界区的线程比后到的线程一定更快地获取得到锁
非公平:先到临界区的线程未必比后到的线程更快地获取得到锁

synchronized底层实现原理

Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。

synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。

执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。

Java偏向锁

开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。

其申请流程为:

首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4;
判断是否需要重偏向。如果不用的话,直接获得偏向锁;
利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。

synchronized 和 volatile 的区别

synchronized 关键字和 volatile 关键字是两个互补的存在

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

并发容器

ConcurrentHashMap : 线程安全的 HashMap
CopyOnWriteArrayList : 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。
ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
ConcurrentSkipListMap : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。

posted @ 2020-05-08 16:34  feifei97  阅读(560)  评论(0编辑  收藏  举报