初识Java并发编程(一)

初识Java并发编程(一)

​ ​ ​ ​ ​ ​ 笔记是看某教学视频总结和记录的,有一些地方有待完善。总体来说看的学习视频开头部分讲的还是有点简略的,不过好的地方是有实际操作,我觉得可以当作入门视频来看。之前学并发看的对某一个点进行深入剖析的视频和一些书籍,再重新过一遍基础,对掌握整个体系还是挺有帮助的,有些地方老师讲的还是挺不错的。

1.进程与线程

1.1进程与线程
进程
  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载到内存。指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被执行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程。
  • 进程就可以看作一个实例,大部分程序可以同时运行多个实例进程(比如说我可以开启好几个360浏览器窗口),但有的程序只能开启一个实例进程(比如网易云音乐不能开2个窗口)。
线程
  • 一个进程之内可以分为1~多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位,在Windows里面进程不活动,只是线程的容器
对比
  • 进程基本上相互独立,而线程存在于进程内,是进程的子集

  • 进程拥有共享的资源,如内存空间等,供其内部的线程贡献

  • 进程间的通信比较负载

    • 同一台计算机的进程通信是IPC
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,比如HTTP
  • 线程通信比较简单,因为共享进程内的内存,一个例子是多个线程可以访问同一个共享变量(这个在《Java并发编程的艺术》中可以看到作者经常提及)

  • 线程更轻量,线程上下文切换成本一般比进程上下文切换低。(上下文切换这个需要重点理解,后面会讲到)

1.2并行与并发

​ ​ ​ ​ ​ ​ 单核CPU下,线程实际上是串行运行的。操作系统中有一个组件叫做任务调度器,将CPU时间片(Windows下时间片最小约为15毫秒)分配给不同的线程使用,只是由于CPU在线程间(时间片很短)的切换非常快,所以我们会感觉到是同步运行的。

​ ​ ​ ​ ​ ​ 一般将线程轮流使用CPU的做法称为并发,concurrent。
在这里插入图片描述
​ 多核CPU下,每个核都可以调度运行线程,这时候线程是并行的。
在这里插入图片描述
​ Rob Pike对并发和并行的描述是这样的:

并发(concurrent)是同一时间应对多件事情的能力

并行(parallel)是同一时间动手做多件事情的能力

2.Java线程

2.1创建和运行线程
方法一:直接使用thread
方法二:使用Runnable配合Thread
方法三:FutureTask配合Thread

​ ​ ​ ​ ​ ​ ​ 以上3种方法在之前的Java基础学习中都详细讲到过,不了解请点我

Thread和Runnable之间的关系

  • Thread类是Runnable接口的子类
public class Thread implements Runnable
  • 查找源码,看看run()方法会有如下收获:
public void run() {
        if (target != null) {
            target.run();
        }
}

​ ​ ​ ​ ​ ​ ​ 这个target是个啥呢?再来锁定一下,private Runnable target;,这样一来就明白了:Thread实现Runnable接口,但是并没有完全实现run() 方法,此方法是Runnable子类完成的,所以如果继承Thread就必须覆写run方法。

  • 方法一和方法二相比,以实现方法二为主。使用方法二的好处:
    • 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
    • Runable 适合多个相同的程序代码的线程去处理同一个资源
    • runnable 可以避免Java 中单继承的限制
    • runnable 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
    • PS:关于资源共享方面,我建议还是多看看dlao博客,我搜了几篇,对于这方面说法很多,还需要在实践下。
2.2查看线程
Windows
  • 任务管理器可以查看进程和线程数,也可以杀死进程

  • tasklist查看进程

    • 查看本机所有所有进程
      • tasklist
    • 根据pid查询指定进程
      • tasklist |findstr pid,这个命令出来的结果可能不止一个,可以理解为模糊查询吧
    • 查看远程所有进程
      • Tasklist /s 218.22.123.26 /u admin /p 111111
      • 查看远程所有进程的时候,需要远程机器的RPC服务的支持
  • taskkill杀死进程

    
    taskkill [/s system [/u username [/p [password]]]]
             { [/fi filter] [/pid processid | /im imagename] } [/t] [/f]
    
    描述:
        使用该工具按照进程 id (pid) 或映像名称终止任务。
    
    参数列表:
        /s    system           指定要连接的远程系统。
    
        /u    [domain\]user    指定应该在哪个用户上下文执行这个命令。
    
        /p    [password]       为提供的用户上下文指定密码。如果忽略,提示
                               输入。
    
        /fi   filter           应用筛选器以选择一组任务。
                               允许使用 "*"。例如,映像名称 eq acme*
    
        /pid  processid        指定要终止的进程的 pid。
                               使用 tasklist 取得 pid。
    
        /im   imagename        指定要终止的进程的映像名称。通配符 '*'可用来
                               指定所有任务或映像名称。
    
        /t                     终止指定的进程和由它启用的子进程。
    
        /f                     指定强制终止进程。
    
        /?                     显示帮助消息。
    
    筛选器:
        筛选器名      有效运算符                有效值
        -----------   ---------------           -------------------------
        status        eq, ne                    running |
                                                not responding | unknown
        imagename     eq, ne                    映像名称
        pid           eq, ne, gt, lt, ge, le    pid 值
        session       eq, ne, gt, lt, ge, le    会话编号。
        cputime       eq, ne, gt, lt, ge, le    cpu 时间,格式为
                                                hh:mm:ss。
                                                hh - 时,
                                                mm - 分,ss - 秒
        memusage      eq, ne, gt, lt, ge, le    内存使用量,单位为 kb
        username      eq, ne                    用户名,格式为 [domain\]user
        modules       eq, ne                    dll 名称
        services      eq, ne                    服务名称
        windowtitle   eq, ne                    窗口标题
    
        说明
        ----
        1) 只有在应用筛选器的情况下,/im 切换才能使用通配符 '*'。
        2) 远程进程总是要强行 (/f) 终止。
        3) 当指定远程机器时,不支持 "windowtitle" 和 "status" 筛选器。
    
    例如:
        taskkill /im notepad.exe
        taskkill /pid 1230 /pid 1241 /pid 1253 /t
        taskkill /f /im cmd.exe /t 
        taskkill /f /fi "pid ge 1000" /fi "windowtitle ne untitle*"
        taskkill /f /fi "username eq nt authority\system" /im notepad.exe
        taskkill /s system /u 域\用户名 /fi "用户名 ne nt*" /im *
        taskkill /s system /u username /p password /fi "imagename eq note*"
    

Linux
Java
  • jps命令查看所有Java线程
  • jstack <PID>查看某个Java进程的所有线程状态
  • jconsole查看某个Java进程中线程的运行情况(图形界面)
2.3 线程运行的原理
栈与栈帧

​ ​ ​ ​ ​ ​ ​ Java Virtual Machine Stacks(Java虚拟机栈)

​ ​ ​ ​ ​ ​ ​ JVM由堆、栈、方法区所组成,其中每个线程启动之后,虚拟机就会为线程分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

​ ​ ​ ​ ​ ​ ​ 举个例子,请看下面的代码,在IDEA编译器中将第4行打上断点,进入debug模式,然后观察左侧Frame框的变化。

public class Test05 {

	public static void main(String[] args) {
		method1(10);
	}
	
	private static void method1(int x){
		int y=x+1;
		Object m=method2();
		System.out.println(m);
	}
	
	private static Object method2(){
		Object n=new Object();
		return n;
	}
	
}

​ ​ ​ ​ ​ ​ ​ 通过debug调试应该就可以明白什么叫做每个栈由多个栈帧组成,此外,在进行单点调试的过程当中,也能更好的理解它的确是后进先出的特点。
在这里插入图片描述

图解流程

1.类加载,将字节码加载到方法区,JVM启动一个main的主线程,并且给他分配一块内存。
在这里插入图片描述
2.CPU给主线程分配了时间片,主线程的代码就开始执行了。主线程里面的主方法就是一个栈帧,所以也会给它分配内存。
在这里插入图片描述

3.执行method1(),给method1()分配栈帧内存。
在这里插入图片描述

4.执行语句,当执行到Object m=method2()时,又会创建一个栈帧,然后执行method2方法中的语句。
在这里插入图片描述

5.new一个对象,赋给n
在这里插入图片描述

6.return n。
在这里插入图片描述

  1. 执行完之后,就会逐渐释放栈帧内存(不放图了)。

​ ​ ​ ​ ​ ​ ​ 多线程的情况下是什么样子呢,可以自行运行查看。代码如下,在执行method1()方法的地方都打上断点,右键断点在Suspend处将All改成Thread

public class Test05 {

	public static void main(String[] args) {
		Thread t1=new Thread(){
			@Override
			public void run(){
				method1(20);
			}
		};
		t1.setName("t1");
		t1.start();
		method1(10);
	}

	private static void method1(int x){
		int y=x+1;
		Object m=method2();
		System.out.println(m);
	}

	private static Object method2(){
		Object n=new Object();
		return n;
	}

}

在这里插入图片描述

线程上下文切换(Thread Context Switch)

​ ​ ​ ​ ​ ​ ​ 因为下列原因会导致CPU停止执行当前的线程,转而去执行另外一个线程的代码。

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程本身调用了sleep、yield、wait、join、park、synchronized、lock等方法

​ ​ ​ ​ ​ ​ ​ 当上下文切换发生的时候,需要有操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用就是记住下一条JVM指令执行的地址,是线程私有的。

  • 要记录的是什么状态?状态包括程序计数器、虚拟机栈中每个栈帧的信息,比如说局部变量、操作数栈、返回地址等。
  • Context Switch频繁发生会影响性能(这一点很重要,不过暂时先不展开讲)
2.4 start方法和run方法

​ ​ ​ ​ ​ ​ ​ 直接调用run()方法并不会启动一个线程,只有调用start()方法才会启动线程。直接调用run()方法,通过日志打印可以发现,该方法实际上是在主线程中调用的,而不是在新创建的Thread线程中。

​ ​ ​ ​ ​ ​ ​ start()方法只能够被调用1次,调用过了之后再调用就会报错。

2.5 yield与sleep
yield
  • 调用yield会让当前线程从Running进入Runnable状态,案后调度执行其它同优先级的线程,如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
  • 具体的实现依赖于操作系统的任务调度器
sleep
  • 调用sleep方法会让当前线程从Running进入Timed Waiting状态(因为使用sleep方法的时候传递了一个休眠多少毫秒的参数,所以是Timed Waiting)
  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行
  • 建议用TimeUnit(J.U.C包下)的sleep代替Thread的Sleep来获得更好的可读性
线程优先级
  1. 线程优先级会提示(hint)调度器优先调度该线程,但他仅仅是一个提示,调度器可以忽略它(如果你看过《并发编程的艺术》,也许会注意到有一个demo就忽略了人为设置的优先级)
  2. 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲的时候,优先级几乎没作用
应用:防止CPU占用100%

​ ​ ​ ​ ​ ​ ​ 在单核的情况下,编译运行一个while(true)的代码,可以看到CPU占用率逼近100%,while(true)空转浪费CPU资源,那么怎么办呢?可以使用yield或者sleep方法让出CPU的使用权给其他程序,再次编译可以发现,CPU占用率会下降很多很多。

​ ​ ​ ​ ​ ​ ​ 不过也可以使用其他方法,sleep方法适用于无锁同步的场景,也可以使用wait()或者条件变量来达到类似的效果,不过有的方法可能需要加锁并且需要相应的唤醒操作,一般适用于进行同步的场景。

2.7 join方法详解
为什么需要join()?
public class Test05 {
	static  int r=0;

	public static void main(String[] args) throws InterruptedException {
		test1();
	}

	private static void test1() throws InterruptedException{
		System.out.println("开始");
		Thread  t1=new Thread(()->{
			System.out.println("线程开始");
			try {
				sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("线程结束");
			r=10;
		},"t1");

		t1.start();
		System.out.println("结果r:"+r);
		System.out.println("结果");
	}


}

​ ​ ​ ​ ​ ​ ​ 看看上面的程序,打印输出的结果r是10还是0呢?结果是0,因为主线程main和线程t1是并行执行的,t1线程需要休眠一段时间之后才能让r=10,而主线程一开始就打印r的结果所以只能打印出r-=0。

​ ​ ​ ​ ​ ​ ​ 那么怎么解决呢?在t1.start()方法后调用join()方法。

​ ​ ​ ​ ​ ​ ​ 视频上关于join()我觉得还是讲的不够的,此外,又看了2篇博客join()方法的使用

2.8 interrupt方法

​ ​ ​ ​ ​ ​ ​ interrupted()方法是判断当前线程是否被打断,会清除打断标记,而isInterrupted()判断是否被打断,不会清楚打断标记,interrupt()方法打断线程,如果被打断的线程正在sleep、wait或者join,会导致被打断的线程抛出异常,并清除打断标记,如果打断正在运行的线程,则会设置打断标记,park的线程被打断,也会设置打断标记。

打断sleep、wait、join的线程

​ ​ ​ ​ ​ ​ ​ 使用interrupt方法后会清空打断状态。

打断正常运行的线程
public class Test05 {

	public static void main(String[] args) throws InterruptedException{
		Thread t1=new Thread(()->{
			while(true){
				boolean interrupted = Thread.currentThread().isInterrupted();;
				if (interrupted){
					System.out.println("线程t1被打断了,退出循环");
					break;
				}

			}
		});
		t1.start();
		Thread.sleep(1000);
		System.out.println("interrupt");
		t1.interrupt();
	}
}

​ ​ ​ ​ ​ ​ ​ 如果上面线程t1中,我们不加入判断的话,那么就算是调用了interrupt()方法,t1线程也不会被中断的。

两阶段终止模式

​ ​ ​ ​ ​ ​ ​ 在一个线程T1中如何“优雅”终止线程T2?这里的优雅指的是给T2一个料理后事的机会。

​ ​ ​ ​ ​ ​ ​ 几个错误思路:

  • 使用线程对象的stop()方法停止线程
    • 该方法会真正杀死线程,如果这时候线程锁住了共享资源,那么当它被杀死之后就再也没机会释放锁了,其他线程将会永远无法获取锁
  • 使用System.exit(int)方法停止线程
    • 目的仅仅是停止一个线程,但是使用这个方法会让这个程序都停止

​ 使用基于interrupt方法的两阶段终止模式是可行的,先看下流程图。
在这里插入图片描述
代码实现:

public class Test05 {

	public static void main(String[] args)throws InterruptedException {
		TwoPhaseTermination tpt=new TwoPhaseTermination();
		tpt.start();

		Thread.sleep(3500);
		tpt.stop();
	}
}

class TwoPhaseTermination{
	private Thread monitor;

	//启动监控线程
	public void start(){
		monitor=new Thread(()->{
			while(true){
				Thread current =Thread.currentThread();
				if(current.isInterrupted()){
					System.out.println("料理后事");
					break;
				}
				try{
					Thread.sleep(1000);//情况1
					System.out.println("执行监控记录");//情况2
				}catch (InterruptedException e){
					e.printStackTrace();
					//重新设置打断标记
					current.interrupt();//不能省略!
				}
			}
		});

		monitor.start();
	}

	//停止监控线程
	public void stop(){
		monitor.interrupt();
	}
}

​ ​ ​ ​ ​ ​ ​ 运行结果:

执行监控记录
执行监控记录
执行监控记录
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at exam.offer.day05.TwoPhaseTermination.lambda$start$0(Test05.java:30)
	at java.lang.Thread.run(Thread.java:748)
料理后事
打断park线程

​ ​ ​ ​ ​ ​ ​ 这个park()方法或许你和我第一次听说,没事以后应该还会见。该方法是LockSupport类(J.U.C包)中的一个静态方法。

​ ​ ​ ​ ​ ​ ​ 打断标记为真的时候,会让park方法失效。

public class Test05 {

	public static void main(String[] args)throws InterruptedException {
		Thread t1=new Thread(()->{
			System.out.println("park");
			LockSupport.park();
			System.out.println("unpark");
			System.out.println("打断标志:"+Thread.currentThread().isInterrupted());
			//如果后面还想让他执行LockSupport.park()方法,就要让打断标志为false,
			//可以将上面这行代码改为
			//System.out.println("打断标志:"+Thread.interrupted());
			
			//System.out.println("park");
			//LockSupport.park();
			//System.out.println("unpark");
		});

		t1.start();
		sleep(1);
		t1.interrupt();
	}
}
2.9 不推荐的方法

​ ​ ​ ​ ​ ​ ​ 已经过时了,容易破坏同步代码块,造成线程死锁。比如stop()方法【两阶段终止模式代替】、suspend()方法【wait()方法代替】和resume()方法【notify()方法代替】。

2.10 主线程与守护线程

​ ​ ​ ​ ​ ​ ​ 默认情况下,Java进程需要等待所有线程都运行结束,才会结束,有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即时守护线程的代码没执行完,也会强制结束。

​ ​ ​ ​ ​ ​ ​ 比如说下面这段代码,其中就将新创建的线程设置为守护线程。

public class Test05 {
	public static void main(String[] args) throws InterruptedException{
		Thread thread = new Thread(()->{
			while(true){
				if(Thread.currentThread().isInterrupted()){
					break;
				}
			}
			System.out.println("thread结束");
		});
		//设为守护线程
		thread.setDaemon(true);
		thread.start();
		Thread.sleep(1000);
		System.out.println("主线程结束");
	}
}

​ ​ ​ ​ ​ ​ ​ 垃圾回收器线程就是一种守护线程;Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令之后,不会等待它们处理完当前的请求。

2.11五种状态(从操作系统层面)

​ ​ ​ ​ ​ ​ ​ 从操作系统层面描述线程可以分为5种。
在这里插入图片描述

  • 初始状态:仅仅是在语言层面创建了线程对象,还没有和操作系统线程关联
  • 可运行状态(就绪状态):该线程已经被创建(与操作系统线程关联),可以由CPU调度执行
  • 运行状态
  • 阻塞状态
    • 如果调用了阻塞API,如BIO读写文件,这时候该线程实际并不会用到CPU,会导致线程上下文切换,进入阻塞状态
    • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换到可运行状态
    • 与可运行状态的区别是,对阻塞状态的线程来说只要他们一直不唤醒,调度器就一直不会考虑调度他们
  • 终止状态:表示线程执行完毕,生命周期已经结束,不会在转换为其它状态
2.12 六种状态(从Java API层面)

​ 从Java API层面描述,根据Thread.State枚举,分为6种状态。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值