键盘敲烂,月薪过万作业不做,等于没学
当前系列: J&C 修改讲义

复习:


理解单线程

在此之前,我们的代码都是运行在一个线程中,这被称之为“线程(Thread)编程”。

这个(我们默认使用的)线程又被称为主(primary)线程,或者启动线程。

更具体的说,代码运行在线程的(stack,复习)中:一个线程对应着一个栈,这个栈又被称之为线程栈。

单个线程中,Java和C#代码都是同步(依次)运行的。

PS:为什么强调Java和C#,因为JavaScript是单线程都可以异步的,^_^

sleep()

Thread类的静态方法,传入整型参数,单位毫秒

Thread.sleep(1000);

让当前执行线程睡眠1000毫秒,一般0用于演示/调试。

梗://项目经理要求加的,方便以后优化加钱

线程信息

使用Thread.currentThread()可以获取一个线程对象:
Thread current = Thread.currentThread();

然后,可以通过thread对象的属性拿到线程的一些基本信息,比如Id、Name等。

System.out.println(current.getId());     //线程Id
System.out.println(current.getName());    //名字
System.out.println(current.getPriority());    //优先级
System.out.println(current.getState());    //线程状态

注意

  1. 线程对象(比如current)是JVM或CLR管理的Java/C#对象(又被称之为工作线程),不是操作系统中的线程(又被称之为核心线程)。工作线程是通过调用核心线程实现多线程的。所以我们获取的是JVM或CLR为线程分配的ID和Name
  2. 在出现资源争夺的时候,优先级更高的线程更有可能优先执行。
演示说明线程的状态:……


创建新线程

Java和C#允许开发人员创建新的线程:(PS:JavaScript不允许

Thread thread = new Thread();
这种线程又被称之为用户线程子线程

子线程还可以被分为:

  • 前台(foreground)线程:会独立运行,会随着主线程结束而结束。即:如果main线程先于前台进程结束,前台进程仍然会继续执行;或者说,只要有一个前台线程未退出,进程就不会终止。
  • 后台(background)/守护(daemon)线程:和前台线程相反。

不管是主线程,还是我们开发人员新建线程,默认都是前台线程。后台线程需要显式设置。

演示:设置和输出是否为后台/守护线程等……

System.out.println(thread.isDaemon());  //false

thread.setDaemon(true);
System.out.println(thread.isDaemon());  //true

新建线程的时候,我们通常都要传递一个方法/函数,让该函数运行在新建的线程上。@想一想@:怎么传递一个函数呢?最简单的就是lambda表达式:

new Thread(()->{
	System.out.println("源栈欢迎您");
})

start()

实例方法,启动当前对象所指向的线程。

其实调用start()方法,只是将线程状态改为准备就绪(ready),然后就只能等着操作系统控制CPU执行该线程中的方法,并不一定就马上运行(run)。


多线程特殊性

异步并发

多线程默认(但是不是一定?)是异步并发的,不然干嘛要多线程呢?演示并解释以下代码运行结果:
static int i = 0;    //Java中的lambda只能访问静态字段:
for (i = 0; i < 10; i++) {  //注意:i是静态字段
	System.out.println("单线程:" + i);
	new Thread(()->System.out.println(i)).start();
}

线程安全

由于多个线程共享一些资源,就有可能A线程的运行,干扰了B线程,使得B线程出现非预期的结果,这就被称之为线程安全(unsafe)。

换言之,多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的。

我们有时候会看到一些方法的文档,上面就会写着:它是线程安全的。就是说:你放心用,哪怕是多线程调用,它都能保证结果运行正确,不需要我们自己采取锁/同步等方式确保线程安全(safe)。

for (int j = 0; j < 10; j++) { // 注意:i是静态字段
	i = 3;
	new Thread(() -> System.out.println((i / 3.0f))).start();
	i = 5;
	new Thread(() -> System.out.println((i / 5.0f))).start();
}

@想一想@:为啥不都做成线程安全的呢?

异常捕获

多线程的另一个大麻烦是能在A线程中直接捕获B线程中的异常。

try {
	throw new RuntimeException("抓我呀!");
	//new Thread(()->{ throw new RuntimeException("抓我呀!"); }).start();
} catch (Exception e) {
	System.out.println("抓到你了!");
}

你可以理解成try...catch是基于栈的,只能控制当前线程中的当前栈。

如果要跨线程捕获异常,需要一系列复杂的操作……大体逻辑:

  • 在创建B线程时,就配置好一个侦听/处理器(listener/handler)
  • 当B线程出现未处理异常时,侦听器会中止当前线程运行,并将异常报给A线程


线程调度控制

很多时候我们仍然需要对线程的运行进行控制,常用的手段:

join()

它可以使得线程之间的并行执行变为串行执行

Thread current = new Thread(()->System.out.println(i));			
current.start();			
current.join();

注意:一定要先start()再join()

理解:join,加入的意思,如果

  • 没有join,两个线程各运行各的;
  • 有了join,current线程就“加入”了调用线程(calling thread),所以不能各玩各的了。调用线程请礼貌一点,孔融让梨,让join线程先执行……

join()方法其实也可以传递一个参数给它:

current.join(10);

这样调用线程就只会等待current线程执行10毫秒。

另外,join(0)等价于join()

和I/O操作类似,线程运行时,锁定其资源(对象/类/方法,不能是变量),不让其他线程访问。

java中的关键字是synchronized,C#是lock。

为了演示,我们不能使用lambda,而是需要用声明一个类,然后将其方法传递给Thread:

class Student implements Runnable {
	int i;	//这是字段,不是方法变量
	@Override
	public void run() {		
		synchronized (this) {    //锁住了当前对象
			i = 0;
			while (i<5) {			
				System.out.println(Thread.currentThread().getId() + ":"+  i);
				i++;			
			}			
		}
	}
}
Student atai = new Student();

Thread t1 = new Thread(atai);
t1.start();

//注意:t1和t2都使用了同一个对象
Thread t2 = new Thread(atai);
t2.start();

演示:

  • i不是字段而是变量,变量无法被多个线程共用
  • 没有synchronized,t1和t2共同操作i,结果就乱了……

从运行的结果来看,好像是t1和t2同步(synchronized)运行,但实际上是因为t1运行时锁住了atai对象,t2只能等着t1运行结束……

线程同步:将操作共享数据的代码行作为一个整体,同一时间只允许一个线程执行,执行过程中其他线程不能参与执行。目的是为了防止多个线程访问一个数据对象时,对数据造成的破坏。

volatile

我们已经知道了多个线程可以共享一个同一个对象资源。

但是,基于某些原因(缓存/编译器优化等),线程并总是拿到最新的数据(如果没有锁):

/* volatile */static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
	Thread thread = new Thread(()->{
		int i = 0;
		while (!stop) {			
			//System.out.println(Thread.currentThread().getId() + ":" + i);
			i++;
		}
		System.out.println("线程终止, i=" + i);
	} );
	thread.start();	
	Thread.sleep(1);		
	stop = true;	//希望据此终止线程中的while循环	
	thread.join();
	System.out.println("全部结束");
}

演示技巧:

  • 在eclipse的debug perspective 下观察 Java Application的增加
  • 注释掉耗性能的System.out.println(Thread.currentThread().getId() + ":" + i);

volatile的作用:确保各个线程总是能拿到最新的变量值(可见性,以及该变量相关的操作总是按正确的顺序执行(有序性)

如何确保?这是JVM的事。注意:Java语法是规范,只要求结果,不管其实现……


C#语法对比

方法首字母大写和属性转换:略

同义词/近义词:

Console.WriteLine(current.ManagedThreadId); 
Console.WriteLine(current.ThreadState); 

current.IsBackground = true;
Console.WriteLine(current.IsBackground);
传入Action实例方法:
class Student    //不需要实现什么Runnable接口
{
    public void show()   //任意方法名
    {
new Thread(new Student().show);
lock而不是synchronized:
lock (this)


线程池

理解(pool)的概念:(复习:字符串池)
  • 一个线程使用完成后并不销毁,而是放回池中
  • 所以池中可以存放多个线程
  • 下次使用时直接从池中取出未使用的线程
为什么要使用线程池?
  1. 线程的创建和销毁都是有性能开销的,需要JVM/CLR和操作系统进行交互(非常重要:天下没有免费的午餐!)
  2. 内置了一系列的线程调度(Scheduler)策略/机制,为开发人员提供一个更简洁的、开箱即用(out-of-box)的多线程开发应用方案。

包含的内容:

  • 池的容量:固定的还是可扩展的,最多能放多少线程
  • 线程池无空闲线程时:任务是排队等待,还是……?
  • 要不要设置线程最长运行时间?
  • 如何处理空闲线程:一直闲着不行啊,资源浪费(又涉及平均分配)
  • ……


作业

  1. 使用多线程发送邮件到newEmail.txt中的邮箱
  2. 使用两个线程模拟龟兔赛跑,乌龟速度慢(=1)但不休息,兔子速度快(=3)但会休息,全程距离为10。在控制台输出他们每一秒龟兔已完成路程,如:
    1:龟1,兔3
    2:龟2,兔6
    3:龟3,兔6
    4:龟4,兔6
    ……
    10:龟10,兔9
    说明:有坑,不能直接输出,考虑将数据先放入某种“容器”,最后再统一输出
  3. 使用多线程完成:【选
    1. 二分查找(天然的可以多线程并发操作)
    2. 找最大值(提示:分段查找后再比较)
    3. 快速排序
学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 J&C 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码