多线程 1
基本概念
程序
程序(program):为完成特定任务、用某种语言编写的一-组指令的集合。即指一段静态的代码,静态对象。在计算机操作系统中,程序就表现为一个可执行文件。
进程
进程(process):程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程,即进程的生命周期。
➢ 程序是静态的,进程是动态的;
➢ 每一个进程,系统是分配一个唯一id标识(pid);
➢ 进程作为系统资源分配的最小单位,系统在运行时会为每个进程分配不同的内存区域。
线程
线程(thread): 进程可进一 步细化为线程,是-一个程序内部的一 条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。
➢ 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
➢ 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会帶来安全的隐患。
进程与线程关系图:
单核CPU / 多核CPU
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员,如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他, 等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。
如果是多核CPU的话, 才能更好的发挥多线程的效率。(现在的服务器都是多核的)。
例如:一个Java应用程序java.exe, 其实至少有三个线程: main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
时间片
现代操作系统比如Mac OS X, UNIX, Linux, Windows等,都是支持多任务的操作系统。所谓的多任务,就是操作系统可以同时运行多个任务。
操作系统(如Windows、Linux) 的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。
这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在"同时进行”,这也就是我们所说的并发。
内核态与用户态
一般的操作系统(如Windows、Linux)对执行权限进行分级:用户态和内核态。
- 操作系统内核作为直接控制硬件设备的底层软件,权限最高,称为内核态,或核心态。
- 用户程序的权限最低,称为用户态。
需要注意的是:当用户态转变为内核态时,是比较消耗资源的!!!
并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片轮转调度的方式)同时执行多个任务。比如:秒杀、多个人做同一件事。
进程的状态
- 就绪:进程处于可运行的状态,只是CPU时间片还没有轮转到该进程,则该进程处于就绪状态。
- 运行:进程处于可运行的状态,且CPU时间片轮转到该进程,该进程正在执行代码,则该进程处于运行状态。
- 阻塞:进程不具备运行条件,正在等待某个事件的完成。
多线程的优点
思考:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,那么为何仍需多线程呢?
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验;
- 提高计算机系统CPU的利用率;
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
何时需要多线程?
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
线程的创建和使用
Thread中的常用方法:
- start():启动当前线程: 调用当前线程的run();
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中;
- currentThread(): 静态方法,返回执行当前代码的线程;
- getName(): 获取当前线程的名字;
- setName(): 设置当前线程的名字;
- yield(): 释放当前cpu的执行权;
- join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态;
- stop():已过时。当执行此方法时,强制结束当前线程;
- sleep(long millitime):让当前线程“睡眠’指定的millitime毫秒。在指定的millitime毫秒时间内, 当前线程是阻塞状态;
- isAlive():判断当前线程是否存活。
参考代码:
package test_MultiThreading;
public class ThreadTest_3 {
public static void main(String[] args) {
//为该分线程起个名字~~
//方式一:在下面构造一个带参数的构造函数,初始化的时候直接命名~~
MyThread_3 myThread_3 = new MyThread_3("Thread-1");
//方式二:调用setName()函数
//myThread_3.setName("分线程1");
myThread_3.start();
//给主线程起个名字~~
Thread.currentThread().setName("主线程");
for (int i=1;i<100;i++){
if (i % 2 != 0){
//Thread.currentThread().getName()--->获取当前进程名字
System.out.println(Thread.currentThread().getName()+":"+ i);
}
//在主线程中,当运行到i=21的时候,主线程断开,进入Thread-1线程中~~
if (i == 21){
try {
myThread_3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class MyThread_3 extends Thread{
@Override
public void run(){
for (int i=1;i<100;i++){
if (i % 2 == 0){
//让进程睡眠一段时间,单位:ms
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread().getName()--->获取当前进程名字
System.out.println(Thread.currentThread().getName()+":"+ i);
}
//当i能够整除20的时候,释放当前线程的使用权~~
if (i%20 == 0){
Thread.currentThread().yield();
}
}
}
//创建一个带参数的构造函数
public MyThread_3(String name){
super(name);
}
}
方式一:继承Thread类
步骤
1.创建一个继承于Thread类的子类;
2.重写Thread类的run()—>将此线程需要执行的操作声明在run()函数中;
3.创建Thread类的子类的对象;
4. 通过此对象调用start()—>启动当前线程,调用当前线程的run()函数。
例1:遍历100以内的所有偶数。
参考代码:
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
//2.重写Thread类的run()
@Override
public void run(){
for (int i=1;i<100;i++){
if (i % 2 == 0){
//Thread.currentThread().getName()--->获取当前进程名字
System.out.println(Thread.currentThread().getName()+":"+ i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread myThread_1 = new MyThread();
//4. 通过此对象调用start()
myThread_1.start();
//注意事项:
//myThread.run();
//通过这种方式启动的线程不是多线程~~
//如果此时,我们需要再次调用myThread线程,不可以直接再次调用start(),
//而是需要重新创建对象后再调用~~
MyThread myThread_2 = new MyThread();
myThread_2.start();
//测试一下多线程
for (int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+":"+i*i);
}
}
}
运行结果:
这个输出结果是可以看出,该程序运行是开辟了3个线程进行运行的!
注意:
- 我们也可以使用myThread.run()的方式直接启动线程,但是这就不再是多线程了,而是全部都在main这个主线程下运行程序~~
- 如果此时,我们需要再次调用myThread线程,不可以直接再次调用start(),而是需要重新创建对象后再调用~~否则会抛IllegalThreadStateException异常。
例2:创建两个分线程,分别遍历100以内的所有偶数和所有奇数。
参考代码:
public class ThreadTest_2 {
public static void main(String[] args) {
MyThread myThread_1 = new MyThread();
MyThread myThread_2 = new MyThread();
myThread_1.start();
myThread_2.start();
}
}
class MyThread_1 extends Thread{
@Override
public void run(){
for (int i=1;i<100;i++){
if (i % 2 == 0){
//Thread.currentThread().getName()--->获取当前进程名字
System.out.println(Thread.currentThread().getName()+":"+ i);
}
}
}
}
class MyThread_2 extends Thread{
@Override
public void run(){
for (int i=1;i<100;i++){
if (i % 2 != 0){
//Thread.currentThread().getName()--->获取当前进程名字
System.out.println(Thread.currentThread().getName()+":"+ i);
}
}
}
}
运行结果:
Thread的匿名子类
在上面两个例子中,我们发现这样一个问题:我们所创建的分线程,只是使用了一次就不用了,每次使用需要创建新的对象,其实还是挺麻烦的~~那么我们就可以使用Thread的匿名子类。
//创建Thread类的匿名子类的方式
new Thread(){
@Override
public void run(){
//具体重写内容
}
}.start();
方式二:实现Runnable接口
步骤
1.创建一个实现了Runnable接口的实现类;
2.实现类去实现Runnable 中的抽象方法run();
3.创建实现类的对象;
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
5.通过Thread类的对象 调用start()。
参考代码:
public class ThreadTest_5 {
public static void main(String[] args) {
//3.创建实现类的对象
MyThread_5 myThread_5 = new MyThread_5();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
Thread thread1 = new Thread(myThread_5);
thread1.setName("线程1");
//5.通过Thread类的对象 调用start()
thread1.start();
//再启动一个线程
Thread thread2 = new Thread(myThread_5);
thread2.setName("线程2");
thread2.start();
}
}
//1.创建一个实现Runnable接口的实现类
class MyThread_5 implements Runnable{
//2.实现类去实现Runnable中的run()抽象方法
@Override
public void run() {
for (int i=0;i<100;i++){
if (i%2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
我们会有这样一个疑问:因为调用start()方法后,线程启动,随后会执行当前线程内的run()函数,但是我们当前线程是Thread,为什么执行下来是调用的下面Runnable中的抽象run()方法呢?
通过阅读源码可以发现,Thread类中run()函数中的target是一个Runnable类型的值~~故而可以执行到Runnable节课欧中的抽像方法!
例1:创建三个窗口卖票,总票数100张。(方式一实现)
参考代码:
public class ThreadTest_4 {
public static void main(String[] args) {
MyThread_4 window1 = new MyThread_4();
MyThread_4 window2 = new MyThread_4();
MyThread_4 window3 = new MyThread_4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class MyThread_4 extends Thread{
private static int ticket = 100; //注意这里必须要用static修饰
@Override
public void run(){
while(true){
if (ticket > 0){
System.out.println(getName() + ":卖票,票号为:"+ ticket);
ticket--;
}else{
break;
}
}
}
}
运行结果:
我们会发现,运行结果中有3个票号为100的票,这是因为涉及到线程安全问题~ ~后面会讲如何解决!
例2:创建三个窗口卖票,总票数100张。(方式二实现)
参考代码:
public class ThreadTest_6 {
public static void main(String[] args) {
MyThread_6 window = new MyThread_6();
Thread window1 = new Thread(window);
Thread window2 = new Thread(window);
Thread window3 = new Thread(window);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class MyThread_6 implements Runnable{
private int ticket = 100; //注意这里没有用static修饰
@Override
public void run() {
while(true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:"+ ticket);
ticket--;
}else{
break;
}
}
}
}
运行结果:
思考一下,这道例题用方式一和方式二实现的最大区别在于定义stick总数的时候有没有用static修饰?
方式一必须要用是因为方式一种创造出了三个对象,那么如果不用static修饰,每个创造对象,stick就会+100,最后就有300张stick。
方式二不需要用static修饰stick,是因为方式二中只创造了一个对象,这一个对象放到了三个构造器中,这三个线程只使用了一个对象。
Runnable的匿名子类
// 使用匿名类创建 Runnable 子类对象
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//具体重写内容
}
});
两种创建方式的比较
综合上面两个方式及其例题,我们发现,线程的创建和使用中,方式二更好一下~~
方式一的局限性在于java的单继承特性,拿购票窗口举例,如果使用方法一的方式实现多线程,现实中的购票窗口还有其他特性,也许还需要继承其他父类,那么方法一下面它就无法继承其他类了,所以还是使用方式二实现Runnable接口比较方便。
方式二能够更好的处理多个线程有共享数据的情况,例如购票窗口中的总票数和窗口之间的关系,方式二在创建中就不需要按每个窗口来创建对象,只需要创建一个对象,然后调用好对象的构造器就可实现多个对象的创建。
方式一和方式二之间的联系就在于两种方法都需要重写run()函数,将线程要执行的逻辑声明在run()函数中。
线程中断的两种方法
1. 通过共享的标记来进行沟通
public class FlagStopThread {
//使用标志位isQuit标识是否继续执行线程中的任务,true中断,false继续执行
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+ ":好!停止转账!");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()+ ": 让李四开始转账。");
thread.start();
Thread.sleep( 1000);
System.out.println(Thread.currentThread().getName()+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true;
}
}
2. 调用 interrupt() 方法来通知
public class InterruptThread {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+ ": 有内鬼,终止交易!");
break;
}
}
System.out.println(Thread.currentThread().getName()+ ": 好!停止转账!");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()+ ": 让李四开始转账。");
thread.start();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
重点说明下第二种方法:
通过 thread 对象调用 interrupt() 方法通知该线程停止运行。thread 收到通知的方式有两种:
- 如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式
通知,清除中断标志; - 否则,只是内部的一个中断标志被设置
thread 可以通过,Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志,Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
第二种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
Thread的优先级
线程的调度
调度策略
1.时间片
2.抢占式,高优先级的线程抢占CPU
线程的优先级等级
优先级分类
- MAX_ PRIORITY:10
- MIN_ PRIORITY:1
- NORM_ PRIORITY: 5
涉及的方法
- getPriority():返回线程优先值;
- setPriority(int newPriority) :改变线程的优先级。
说明
- 线程创建时继承父线程的优先级;
- 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。|
~~未完待续