设置或获取线程的名称
public final String getName()
public final void setName(String name)
思考,如何获取main线程的名称呢?
static Thread currentThread() //返回对当前正在执行的线程对象的引用。
······················································································································································································································
线程调度
假设在单CPU的情况下,线程的两种主要调度模型
协同式线程调度(Cooperative Thread-Scheduling)
抢占式调度(Preemptive Thread-Scheduling)
Java使用哪种调度模型?抢占式调度
多线程的优先级
public final int getPriority() 返回线程的优先级。
public final void setPriority(int priority) 更改线程的优先级。
注意事项:
1. 线程的默认优先级的5
2. 多线程的优先级的取值范围1 <= priority <=10
3. 然而,我们在java语言中设置的线程优先级,它仅仅只能被看做是一种"建议"(对操作系统的建议),
实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级)
所以记住:千万不要试图,利用线程优先级,来间接控制线程调度的顺序,换句话说线程优先级不是一种稳定的调节手段
java官方: 线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点
注意事项:
1. 多个线程,交替执行
2. “同时执行”
a. 并发: 同一时间段内,同时执行(但是同一时间点,可能并没有同时执行)
b. 并行: 同一时间点, 同时执行
······················································································································································································································
- public static native void sleep(long millis)
- 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- millis - 以毫秒为单位的休眠时间。
- native表示这个方法底层实现不是用java语言实现的
// 让当前线程休眠3秒,因为sleep运行在子线程中,所以就是让子线程休眠3秒
Thread.sleep(3000);
//还有另外一种让当前线程休眠的方式 TimeUnit 时间单位
TimeUnit.SECONDS.sleep(3);
public final void join()
- (谁)等待(谁)该线程终止。
- 谁等待谁终止?
- 调用该方法的线程等待
- 调用的是哪个线程对象的join方法,就等待哪个线程的run方法执行完毕
本例中,main线程等待joinThread线程的run()执行完毕
public static void yield() (礼让方法)不重要(了解即可)
- 暂停当前正在执行的线程对象
- 并执行其他线程。(yield方法实现不了该功能)
因为执行其他线程是由操作系统决定的,当前线程yield()后,将处于阻塞状态,而CPU会空闲后会从当前阻塞队列中取一个线程出来执行,所以下一个执行的线程依然可能是本线程。
public final void setDaemon(boolean on)
- 1.将该线程标记为守护线程(参数为true)或用户线程(简单理解就是我们用户自己创建的线程,之前学的都是用户线程)(参数为false)。如果为 true,则将该线程标记为守护线程
- 2.该方法必须在启动线程前调用
- 3.我们创建的普通线程,都不是守护线程
守护线程的执行特征:
- 当正在运行的线程都是守护线程时,Java虚拟机退出。
- 守护线程:守护 非守护线程
比如: 垃圾回收其所在的线程,就是一个典型的守护线程
只要有普通的用户线程在执行,就有可能创建对象,有创建对象,对象就有可能变成垃圾,
你产生垃圾,我就默默的帮你回收垃圾。
······················································································································································································································
public void interrupt() 中断线程。
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,
或者该类(Thread)的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int)
方法过程中受阻,它还将收到一个 InterruptedException。
我的理解:为啥要将一个线程打断?可能是因为这个线程占用的时间太长了,一直在sleep,或者线程里面有死循环。(相当于一个应用程序卡了,我们将其结束进程),但你打断完,还要要求线程做一些工作(如回收资源等),就写在catch后
InterruptedException: 有啥意义
- 该异常,其实是针对,被异常终止的线程(被interrupt打断),给它提供了一个机会,能够在线程异常终止的情况下(线程被打断后),仍然能够正确执行一些资源处理的工作
如果线程并不处于休眠状态也可以用interrupt对其打断对吧?
- 中断一个没有处于阻塞状态的线程不会有任何效果
class InterruptedThread extends Thread {
@Override
public void run() {
System.out.println("begin");
//依然会输出0到99
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
- 但是,如果在该线程对象正在执行sleep, join, wait等方法正处于阻塞状态,然后在该对象上调用interrupt方法之后,就会抛出InterruptedException异常。
class InterruptedThread extends Thread {
@Override
public void run() {
System.out.println("begin");
//测试,在线程被中断之后,就不能再进入阻塞状态了
try {
Thread.sleep(3000);
System.out.println("休眠3秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
······················································································································································································································
线程的状态转化:
新建:线程处于刚刚创建的状态
就绪:有执行资格,等待cpu调度获得执行权
运行:取得执行权,正在cpu上执行
阻塞:无执行资格,无执行权
死亡:线程正常或异常终止(run()方法执行完毕),线程对象成为垃圾,等待垃圾回收器回收
······················································································································································································································
实现线程的第二种方式:
- 定义实现Runnable接口的子类
- 实现Runnable接口的run方法
- 创建该子类对象
- 在创建Thread对象的时候,将创建好的Runnable子类对象作为初始化参数,传递给Thread对象
- 启动Thread对象(启动线程)
注意事项:
-
我们Runnable接口子类的run()方法代码,会运行在子线程当中。
-
所以,在线程的第二种实现方式中,我们自己定义子类,实现Runnable接口的run方法,将要在子线程中执行的代码,放在run()方法中。
-
但是,Runnable子类对象,并不代表线程,它只代表,要在线程中执行的任务。
我们认为,从逻辑上说,第二种方法逻辑十分清晰:
- 线程就是一条执行路径,至于在线程这条执行路径上,究竟执行的是什么样的具体代码,应该和线程本身没有关系的
- 也就是说,线程,和在线程(执行路径)上执行的任务应该是没有什么直接关系的
- 线程实现的第二种方式,把线程(Thread对象代表线程) 和在 线程上执行的任务(Ruannable子类对象) 分开
Runnable的run方法运行在子线程thread中。
······················································································································································································································
再次粘贴出方式一的代码:
Thread实现方式一:
1.继承Thread
2.重写子类的run方法
3.创建该子类的对象
4.启动线程 start()
方式二一 VS 方式二:
1. 方式一实现步骤较方式二少
2. 方式一的实现方式,存在单重继承的局限性 方式一是继承父类,方式二是实现接口
3. 方式二将线程和任务解耦。将线程和线程执行的任务分开了
4. 方式二,便于多线程数据的共享
关于数据共享问题,看一个题目:
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
分析:
3个窗口售票,互不影响,同时进行。
3个窗口共同出售这100张电影票
package com.cskaoyan.thread.threadsafe;
/**
* @author shihao
* @create 2020-04-30 22:59
*
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
*
* 1. 3个窗口要用3个线程来模拟
* 2. 3个共同售卖100张票(数据共享)
*/
public class Demo01 {
public static void main(String[] args) {
//用线程的第一种方式实现,变量的共享
firstType();
//利用线程的第二种实现方式,方便的实现了,多线程的数据共享
//secondType();
}
private static void secondType() {
SalesTask salesTask = new SalesTask();
//三个线程(执行路径)调用一个实现Runnable接口的子类的对象(执行一个任务)
Thread window1 = new Thread(salesTask, "窗口1");
Thread window2 = new Thread(salesTask, "窗口2");
Thread window3 = new Thread(salesTask, "窗口3");
window1.start();
window2.start();
window3.start();
}
private static void firstType() {
WindowThread window1 = new WindowThread("窗口1");
WindowThread window2 = new WindowThread("窗口2");
WindowThread window3 = new WindowThread("窗口3");
window1.start();
window2.start();
window3.start();
}
}
//第一种实现方式
class WindowThread extends Thread {
//表示待售卖的100张票
//加上static修饰符后就实现了数据共享
static int tickets = 100;
public WindowThread(String name) {
super(name);
}
@Override
public void run() {
//有票,就卖票
while (tickets > 0) {
//模拟售票动作
System.out.println(getName() + "卖出第" + tickets-- + "张票");
}
}
}
//线程的第二种实现方式
class SalesTask implements Runnable {
//表示待售卖的100张票
int tickets = 100;
@Override
public void run() {
//有票,就卖票
while (tickets > 0) {
//模拟售票动作
System.out.println(Thread.currentThread().getName() + "卖出第" + tickets-- + "张票");
//无法再用this.getName了,因为方式一中的getName是Thread的getName()方法。
//Thread.currentThread(),当前运行的线程
}
}
}
关于第一种方式中的static:
回顾week02_day03_static中:
三、static关键字的特点:
-
被类的所有 对象所共享:(判定是否使用static的关键)
a. 当static修饰了成员变量,该成员变量的值,就不在存储与对象中了,而是单独存储了一份,被类的所有对象所共享。
b. 当static修饰成员方法的时候,该方法被当前类的所有对象共享当前类对象.方法(和普通成员方法从共享的角度,几乎没有太大区别) -
可以通过类名访问
a. 可以通过类名直接访问,static成员变量的值
如:Student.SchoolName
b. 直接通过类名直接调用,static成员方法
如:Student.getSchoolName(); -
随着类的加载而加载
a. static成员变量,随着类加载过程,其实就已经在方法区中,分配了内存
b. static成员方法, 一旦类加载完毕,我们就可以直接访问,static方法,而不必创建对象,然后在对象名. 访问方法 -
优先于对象而存在,不依赖于对象而存在
a. 成员变量的角度来理解,static修饰的成员变量,不在依赖于对象而存在,因为static修饰的成员变量的值,不在存储在,该类的每个对象中。作为对比,没有被static修饰的成员变量,都依赖于对象而存在,因为他们的值,都存储在对象中
b. 成员方法角度,被static修饰的成员方法,在没有对象存在的情况下,也可以直接通过类名来调用方法,作为对比,没有被static修饰的,普通成员方法,它依赖于对象而存在, 原因是,普通成员方法中,可以访问,普通成员变量的值,而普通对象的值又是依赖于对象而存在的 -
先出现在内存
静态成员变量, 一定先于没有被static修饰的普通成员变量,出现在内存中。因为静态成员变量随着类的创建而创建,随着类的销毁而销毁,而普通成员变量随着对象的创建而创建,随着对象的销毁而销毁,而一定是先有类后才有对象的。
············································································································································································································
- 在原有代码基础上,增加了售票延迟之后(之所以增加延迟效果,是因为延迟时调用sleep()方法,当前线程进入阻塞状态,给了其他线程占用CPU的机会,此时就会发生重复读),我们就发现了售票过程中两种错误
package com.cskaoyan.thread.threadsafe;
/**
* @author shihao
* @create 2020-04-30 23:55
*
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
*
* 1. 3个窗口要用3个线程来模拟
* 2. 3个共同售卖100张票(数据共享)
*
* 在原有代码基础上,增加了售票延迟之后,我们就发现了售票过程中两种错误
* 1. 相同的票被不同的窗口,多次售出(多卖问题)
* 2. 售出了不存在的票(超卖问题)
*
* 分析,代码中以上两种问题,出现的原因
* 1. 多卖问题
* 窗口1卖出第94张票
窗口3卖出第94张票
窗口2卖出第94张票
2. 超卖问题
窗口2卖出第1张票
窗口1卖出第0张票
窗口3卖出第-1张票
无论是多卖还是超卖问题,本质上都是多线程的数据安全问题:
多线程的数据安全问题是指:在多线程共享数据前提现,在访问共享数据的时候,访问到了错误的共享数据
*
*
*/
public class Demo02 {
public static void main(String[] args) {
SalesTask2 salesTask = new SalesTask2();
Thread window1 = new Thread(salesTask, "窗口1");
Thread window2 = new Thread(salesTask, "窗口2");
Thread window3 = new Thread(salesTask, "窗口3");
window1.start();
window2.start();
window3.start();
}
}
//线程的第二种实现方式
class SalesTask2 implements Runnable {
//表示待售卖的100张票
int tickets = 100;
@Override
public void run() {
//有票,就卖票
while (tickets > 0) {
//超卖问题分析:
// tickets 1
// 窗口2线程:发现tickets的值为1 > 0,进入while循环, 发生了线程切换 ->窗口1
// 窗口1线程:发现tickets的值为1 > 0,进入while循环, 发生了线程切换 ->窗口3
// 窗口3线程: 发现tickets的值为1 > 0,进入while循环,
//增加售票延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//分析多卖问题
// tickets-- 分3步完成(没有实现原子操作): 1.读取tickets变量的值; 2. tickets - 1; 3. tickets = tickets - 1
// tickets 的是 94
// 窗口1: 假设首先窗口1准备售票,拼接结果字符串:窗口1卖出第94, tickets - 1,此时线程切换->窗口3
// 窗口3: 假设窗口3准备售票,拼接结果字符串:窗口3卖出了第94,线程切换 -> 2
// 窗口2: 窗口2准备售票,拼接结果字符串;窗口2卖出第94
//模拟售票动作
System.out.println(Thread.currentThread().getName() + "卖出第" + tickets-- + "张票");
}
}
}
············································································································································································································
作业:
- 自己利用线程的第一种实现方式,实现如下功能(多线程引例):
a. 程序不停地在屏幕上输出一句问候的语句(比如“你好”)
(时间间隔一定要大些比如3s(或大于3s),因为在控制台窗口,输入和输出不能同时发生,
我们只能在两次输出“你好”的间隙,从键盘输入数据,才能保证键盘输入被正确接收)
b.同时,当我通过键盘输入固定响应的时候,程序停止向屏幕输出问候的语句
public class MultiThread {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
sayHelloRecycling();
waitToStop();
System.out.println("main end");
}
/**
* 在子线程中,接收键盘输入,并根据键盘输入
* ,决定是否终止,在屏幕上输出问候语句
*/
private static void waitToStop() {
new Thread() {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
while (true) {
String s = scanner.nextLine();
if ("gun".equals(s)) {
flag = false;
scanner.close();
break; //如果输入了gun,就终止程序的运行
}
}
}
}.start();
}
/**
* 在子线程中,不停的在屏幕上输出问候语句,
* 直到,循环的控制变量flag的值被改为false
*/
private static void sayHelloRecycling() throws InterruptedException {
new Thread() {
@Override
public void run() {
while (flag) {
System.out.println("哈哈, 你好!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
2.阅读如下代码:
- 回答输出的是什么内容
输出的是:Thread匿名子类的run方法 - 为什么
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(“Runnable匿名子类的run方法”);
}
}) {
@Override
public void run() {
System.out.println(“Thread匿名子类的run方法”);
}
}.start();
原因如下:
-
首先,我们创建了一个Thread的匿名内部类对象
-
该匿名Thread子类对象接收一个Ruannable接口子类的匿名内部类对象
-
在Thread子类的匿名内部类定义中,我们覆盖了父类(Thread)的run方法
-
接着我们在Thread的匿名内部类对象上调用了start()方法,启动该Thread匿名内部类对象所表示的子线程
-
在该子线程(即Thread的匿名内部类对象)上调用start()方法,start()方法会调用Thread类的run()方法,但这个run方法在Thread的匿名子类定义中被子类覆盖了。所以实际在子线程中执行并非是Thread类的run()方法,而是Thread的匿名子类中定义的run()方法(即多态效果)