Java多线程感悟一

简介:

写在前面

有时候在项目开发中,时不时的会遇到多线程方面的问题。可是要想驾驭它,就必须了解Java多线程方面的知识,认识它的内在。结合我自己的开发经验以及对Java多线程的一些学习过程,将写几篇关于Java多线程的博客,分享给大家,如有错误,请大家指正。


目录

  1. 线程和进程

  2. Thread和Runnable

  3. 线程的状态

  4. 生产者和消费者模型:wait/notify

  5. 后台线程

  6. interrupt

  7. 线程合并join

  8. UncaughtExceptionHandler


线程和进程

线程和进程的概念相信大家都知道,我来说下我的理解。操作系统可以将系统的一些资源,如CPU,内存等交给一段运行起来的程序,即进程。进程之间一般是相互独立的,比如Nginx的工作模型中有一个master进程,多个worker进程,如果有worker进程“挂”了,并不会影响其他worker进程处理请求。在进程内部可以有多个并发执行流,即线程。线程的存在必须依赖进程,同时一个进程内部的多个线程,它们共享进程的资源,相互之间可以影响。线程是轻量级的进程,它的创建、销毁、切换都比进程要小。



Thread和Runnable

说起Java的多线程,我们脑海里面可能有如下的想法:

要么extends Thread,然后重写run(),调用start()

或者implements Runnable , 实现run(),将Runnable实例传递给Thread构造方法,调用start()


呵呵,其实,以前,我也是这么想的。


那么,在Java线程中,Thread和Runnable到底是什么,各自扮演着什么角色?


看一下Thread的类声明吧:

1
public  class  Thread  implements  Runnable


从上面的来看,好像它们是“一家人”。


我们可以扫一扫Thread的源码,发现:

1
2
3
4
5
/* What will be run. */
private  Runnable target;
public  Thread(Runnable target) {
init( null , target,  "Thread-"  + nextThreadNum(),  0 );
}

其实,Runnable接口扮演的是一个任务的角色,通过提供run()来明确这个任务要做什么。对于Thread而言,默认情况下,它调用的就是你传递给它的Runnable的run()方法。Thread类中的target就是用来接收你传递进来的任务的,这样任务是任务,线程是线程,线程可以执行任务,就这么简单!



线程的状态

线程的状态很重要!

就我自己的感觉而言,我们写多线程方面的代码,就是在进行线程的状态转换从而完成业务上的要求,如果我们不清楚线程的状态,那么我们将无从下手,更加看不懂别人的代码!


根据我的理解,画了张图,一起来看下吧。


wKioL1TDQY-hNnPdAACMYASm-2g174.jpg


线程有5个基本状态:新建、就绪、运行、阻塞、死亡。


当我们新建了一个Thread类的对象时,在JVM中只是存在了一个Thread对象而已,此时OS中并没有真正存在一个线程,只有一个操作线程的外壳,此时为新建状态。


如果我们调用了start()方法,那么将从新建状态转为就绪状态。start()方法的调用结束使得OS完成了内核调用,线程已经存在。需要注意的是,start()调用,run()方法一般并不会同步调用,因为run()的调用是CPU的选择,什么时候调用,我们无法准确干预。


当start()调用结束,系统相关资源准备完毕,那么就会进入运行状态,调用run()。如果在run过程中,失去了CPU时间片,将回到就绪状态。比如yield()方法,这个方法会让出CPU的使用权,希望给其他人用一用,当然这只是它的想法,也许yield()调用进入就绪状态后,马上CPU又调度他执行。


运行状态中,如果发生了sleep/等待锁/IO阻塞/wait等,将进入阻塞状态。如果在阻塞状态中,睡好了/锁来了/IO完成了/notify来了,那么就完成了阻塞状态的解除,从而进入到就绪状态,重新等待CPU的调度。


正常情况下,当run()结束或者发生了未捕获的异常,就会进入死亡状态。



生产者和消费者模型:wait/notify

前面说了那么多理论,下面我们就来点代码练练手吧!


业务场景:

有一个篮子,容量是10个,生产者生产馒头放入其中,消费者从篮子里面拿馒头吃。如果生产者发现篮子里面的馒头已经满了,就不再生产,消费者可以消费。如果消费者发现篮子里面的馒头空了,就停止消费,生产者可以生产。


馒头Model:

1
2
class  ManTou{
}


篮子容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class  LanZhi{
private  int  size;
private  int  index =  0 ;
private  ManTou[] foods;
public  LanZhi( int  size){
this .size = size;
this .foods =  new  ManTou[size];
}
public  synchronized  void  produce(ManTou m){
if (index == size){
//不应该在生产馒头
try  {
this .wait();
catch  (InterruptedException e) {
e.printStackTrace();
}
}
//馒头放入篮子
foods[index] = m;
++index;
System.out.println( "生产一个馒头,现在馒头总共有:"  + index);
//发出通知:可以吃馒头了
this .notify();
}
public  synchronized  ManTou eat(){
ManTou m =  null ;
if (index ==  0 ){
//没得吃了
try  {
this .wait();
catch  (InterruptedException e) {
e.printStackTrace();
}
}
m = foods[--index];
System.out.println( "吃掉一个馒头,现在馒头总共有:"  + index);
//发出通知:可以放入馒头了
this .notify();
return  m;
}
}



生产任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class  ProduceRunnable  implements  Runnable{
private  LanZhi l;
public  ProduceRunnable(LanZhi l){
this .l = l;
}
@Override
public  void  run() {
while ( true ){
l.produce( new  ManTou());
try  {
//假设生产馒头耗时0.3S
Thread.sleep( 300 );
catch  (InterruptedException e) {
e.printStackTrace();
}
}
}
}



消费任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class  ConsumeRunnable  implements  Runnable{
private  LanZhi l;
public  ConsumeRunnable(LanZhi l){
this .l = l;
}
@Override
public  void  run() {
while ( true ){
l.eat();
try  {
//假设吃馒头耗时1S
Thread.sleep( 1000 );
catch  (InterruptedException e) {
e.printStackTrace();
}
}
}
}



主线程调用:

1
2
3
4
5
public  static  void  main(String[] args) {
LanZhi l =  new  LanZhi( 10 );
new  Thread( new  ProduceRunnable(l)).start();
new  Thread( new  ConsumeRunnable(l)).start();
}




说明:

wait/notify这两个方法都是在object中定义的方法,都应该在临界区域内发生调用(也就是synchronized区域),否则会发生异常。一个线程好不容易拿到了“门票”,为什么要调用wait呢?因为有些时候,我们拿到了锁,准备进行一些操作的时候,发现并不满足业务上的一些要求,就需要wati了。就好比,我们进入了卫生间,关上了门,蹲在了马桶上,此时发现,没有带纸,没有办法,只好退出卫生间!



后台线程

线程分为前台线程和后台线程。

首先需要注意的是,JVM认为一旦所有的前台线程运行完毕,只剩下后台线程的话,那么就意味着程序要结束了。


举个例子:

1
2
3
4
5
6
7
8
class  Daemon  extends  Thread{
@Override
public  void  run() {
for ( int  i =  0  ; i <  10  ; i++){
System.out.println( "i : "  + i);
}
}
}



主线程调用:

1
2
3
4
5
6
public  static  void  main(String[] args) {
Daemon d =  new  Daemon();
d.setDaemon( true );
d.start();
System.out.println( "hello" );
}
1
2
3
4
5
6
7
8
9
10
运行结果:
hello
i :  0
i :  1
i :  2
i :  3
i :  4
i :  5
i :  6
i :  7



hello的输出,意味着前台线程已经运行完毕了,那么JVM将会结束整个进程,并且不保证执行完后台线程。那么后台线程可以用来干嘛?比如Java的垃圾回收线程就是一个后台线程,而且它的调度优先级比较低,它不会随便去和其他线程争夺使用CPU。


interrupt

interrupt,即中断的意思,那中断意味着什么?


我们先看看下面的:

1
2
3
public  final  native  void  wait( long  timeout)  throws  InterruptedException;
public  static  native  void  sleep( long  millis)  throws  InterruptedException;
public  final  synchronized  void  join( long  millis)   throws  InterruptedException


我们知道有些方法会抛出中断异常,那什么时候抛呢?


再去扫一眼Thread源码,发现:

1
2
3
4
5
6
7
  public  static  boolean  interrupted() {
return  currentThread().isInterrupted( true );
  }
  public  boolean  isInterrupted() {
return  isInterrupted( false );
  }
  private  native  boolean  isInterrupted( boolean  ClearInterrupted);

在默认情况下,显然,线程的中断标志是false。当我们调用了interrupted()后,实际上是将线程中断标志设置为true。


看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
class  Thread1  extends  Thread{
@Override
public  void  run() {
try  {
Thread.sleep( 10000 );
//其他一些业务操作
System.out.println( "sleep over..." );
catch  (InterruptedException e) {
System.out.println( "出现中断异常" );
}
}
}

主线程调用:

1
2
3
Thread1 t1 =  new  Thread1();
t1.start();
t1.interrupt();


运行结果:

出现中断异常



说明interrupt()的调用,只是做了一个标记并产生异常抛出而已,并没有真正的唤醒线程。



线程合并join

线程合并?为什么多个线程要合并?什么时候会出现需要合并呢?怎么合并?


假设有这么一个业务排序场景:


我们有N个这样的块,每个块中存放数字,而且块间有序、块内是无序的,如下:

第一块数字范围:[0,1000]

第二块数字范围:[1001,2000]

第三块数字范围:[2001,3000]

...


分析:

不应该对所有块的所有数字来一次全排序,很显然的是,我们想利用块间有序这个条件,能不能对每一块进行排序。要知道,第一块排序和第二块排序,他们是相互独立的,没有锁的限制,完全可以并行进行。那我们的初步想法是,启动N个线程对N个块进行排序,当N个线程都结束,就完成了排序了。但是要知道,我们无法保证每个排序线程的完成顺序,但是业务上,我们确实需要当排序完毕后进行XXX操作。

当然,我们可以写一个循环进行遍历,来获取N个排序线程的状态,从而决定排序完毕后的操作什么时候进行。其实,Java已经提供了join(),来完成这方面的需要。


实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class  SortRunnable  implements  Runnable{
int [] array;
public  SortRunnable( int [] array){
this .array = array;
}
@Override
public  void  run() {
for ( int  i =  0  ; i < array.length ; i++){
for ( int  j = i+ 1  ; j < array.length ; j++){
if (array[i] > array[j]){
try  {
Thread.sleep( 1000 );
catch  (InterruptedException e) {
e.printStackTrace();
}
int  tmp = array[i] ^ array[j];
array[i] = tmp ^ array[i];
array[j] = tmp ^ array[j];
}
}
}
}
}

主线程调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int [] array1 = { 1 , 9 , 8 , 7 };
int [] array2 = { 10 , 30 , 25 };
Thread t1 =  new  Thread( new  SortRunnable(array1));
Thread t2 =  new  Thread( new  SortRunnable(array2));
t1.start();
t2.start();
t1.join();
t2.join();
for ( int  tmp : array1){
System.out.print(tmp +  " , " );
}
System.out.println();
for ( int  tmp : array2){
System.out.print(tmp +  " , " );
}



UncaughtExceptionHandler

直接看一个例子,大家就能明白!

1
2
3
4
5
6
class  MyException  implements  UncaughtExceptionHandler{
@Override
public  void  uncaughtException(Thread t, Throwable e) {
System.out.println( "捕捉到线程中未处理的异常" );
}
}


主线程调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread t =  new  Thread(){
@Override
public  void  run() {
//业务操作
//...
System.out.println( "start..." );
Integer.parseInt( "a" );
//业务操作
//...
System.out.println( "end..." );
}
};
t.setUncaughtExceptionHandler( new  MyException());
t.start();

运行结果:

start...

捕捉到线程中未处理的异常



说明:

如果在run()中出现了我们没有处理的异常,那么我们还有一次“后悔药”可以吃~



本文转自zfz_linux_boy 51CTO博客,原文链接:http://blog.51cto.com/zhangfengzhe/1607712,如需转载请自行联系原作者


相关文章
|
9天前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
122 60
【Java并发】【线程池】带你从0-1入门线程池
|
5天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
51 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
87 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
53 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
2月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
121 17
|
3月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
Java 多线程 面试题
Java 多线程 相关基础面试题
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等