第1章 Java 多线程技能

进程和多线程的概念

什么是线程?

线程可以理解成是在进程中独立运行的子任务。比如 QQ.exe 运行时就有很多子任务在同时运行,如好友视频线程、下载文件线程、传输数据线程等等。

使用多线程的优点

可以最大限度地利用 CPU 空闲时间来处理其他任务。

线程的四种状态

  • 新建(New):处于该状态的时间很短暂。已经被分配了必须的系统资源,并执行了初始化。表示有资格获取 CPU 时间。调度器可以把该线程变为 Runnable 或 Blocked 状态。
  • 就绪(Runnable):这种状态下只要调度器把时间片分配给线程,线程就能运行。处在这种状态是可运行可不运行。
  • 阻塞(Blocked):线程能够运行,但有个条件阻止它运行。当线程处于阻塞状态时,调度器将会忽略该线程,不会分配给线程任何 CPU 时间。只有重新到达 Runnable 状态,才可能执行操作。
  • 死亡(Dead):处于死亡状态的线程是不可调度的。

一个任务进入阻塞状态,可能有如下原因:

  1. sleep()
  2. wait()
  3. 任务在等待某个输入/输出的完成
  4. 线程试图在某个对象上调用同步控制方法,但对象锁不可用,因为另一个任务已经获取了这个锁。

使用多线程

继承 Thread 类

Thread 类的结构:public class Thread implements Runnable
从源码上看,Thread 类实现了 Runnable 接口,它们之间具有多态关系。其实,使用继承 Thread 类的方式创建多线程最大的限制就是不支持多继承。所以,为了支持继承,可以使用 Runnable 接口来实现多线程。

实现 Runnable 接口

在 Java 的八个构造函数中,有两个构造函数 Thread(Runnable target) 和 Thread(Runnable target, String name) 可以传递 Runnable 接口,说明构造函数支持传入一个 Runnable 接口对象。

实例变量与线程安全

自定义线程类中的实例变量针对其它线程可以有共享和不共享之分,这在多线程之间进行交互是一个很重要的技术点。

  1. 不共享数据的情况
public class MyThread3 extends Thread {
    private int count = 5;
    public MyThread3(String name){
        super();
        this.setName(name);
    }
    public void run(){
        super.run();
        while(count > 0){
            count--;
            System.out.println("由" + this.currentThread().getName() + "计算,count=" + count);
        }
    }
}

public class Run3 {
    public static void main(String[] args) {
        //创建三个MyThread对象,并分别给线程命名
        MyThread3 a = new MyThread3("A");
        MyThread3 b = new MyThread3("B");
        MyThread3 c = new MyThread3("C");
        a.start();
        b.start();
        c.start();
    }
}

这里,因为有三个 MyThread 对象,虚拟机会为每个对象分配内存,每个对象都会得到 count 并初始化为 5,也会得到 run 方法。所以,多个线程之间是不会分享实例变量 count 的。

  1. 数据共享的情况
public class Thread4 extends Thread {
    private int count = 5;
    public void run(){
        super.run();
        count--;
        System.out.println("由" + this.currentThread().getName() + "计算,count= " + count);
    }
}

public class Run4 {
    public static void main(String[] args) {
        //创建一个myThread对象,并将该对象分别加载到5个线程中并分别给线程命名
        Thread4 myThread = new Thread4();
        Thread a = new Thread(myThread,"A");
        Thread b = new Thread(myThread,"B");
        Thread c = new Thread(myThread,"C");
        Thread d = new Thread(myThread,"D");
        
        a.start();
        b.start();
        c.start();
        d.start();
    }
}

这里,因为只有一个 Thread4 对象,域 count 只有一份,所以四个线程共享一份变量。
但是会出现这种情况:
由A计算,count= 3
由B计算,count= 3
由C计算,count= 2
由D计算,count= 1
线程 A 和 线程 B 打印出的 count 值都是 3 ,说明 A 和 B 同时对 count 进行了处理。在某些 JVM 当中,i-- 分为三步骤:

  1. 取得原有 i 值
  2. 计算 i - 1
  3. 对 i 进行赋值
    在这三个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。

将代码修改为:

public class Thread4 extends Thread {
    private int count = 5;
    synchronized public void run(){
        super.run();
        count--;
        System.out.println("由" + this.currentThread().getName() + "计算,count= " + count);
    }
}

通过在 run() 方法前加入 synchronized 关键字,使得多线程在执行 run() 方法时以排队的方式进行处理。当一个线程调用 run() 方法前,先判断 run() 方法有没有被上锁,如果上锁,说明其他线程正在调用 run 方法,必须等到其他线程对 run 方法调用结束后才可以至执行 run 方法。synchronized 可以在任意对象或方法前加锁,而加锁的那段代码称为“互斥区”或“临界区”。
非线程安全是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序执行流程。

currentThread() 方法

currentThread 方法可返回代码段正在被哪个线程调用的信息。

isAlive() 方法

isAlive() 方法的作用是判断当前线程是否处于活动状态。活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是存活的。

sleep() 方法

在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个正在执行的线程是指 this.currentThread() 返回的线程。
休眠的位置:为了让其他线程有机会执行,可以将 Thread.sleep() 的调用放在线程 run() 内,这样才能保证线程在执行过程中会休眠。

对两段代码的分析
public class MyThreadSleep extends Thread {
    public void run(){
        try {
            System.out.println("run ThreadName = " + this.currentThread().getName() + "begin");
            Thread.sleep(2000);
            System.out.println("run ThreadName = " + this.currentThread().getName() + "end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class RunSleep {
    public static void main(String[] args) {
        MyThreadSleep myThreadSleep = new MyThreadSleep();
        //下列代码依次执行,只有一个main线程
        System.out.println("begin = " + System.currentTimeMillis());
        myThreadSleep.run();
        System.out.println("end = " + System.currentTimeMillis());
    }
}

执行结果:
begin = 1542264873276
run ThreadName = mainbegin
run ThreadName = mainend
end = 1542264875277

这里 myThreadSleep.run();也是在 main 线程中完成的,所以依次执行后面的代码。

public class MyThreadSleep extends Thread {
    public void run(){
        try {
            System.out.println("run ThreadName = " + this.currentThread().getName() + "begin");
            Thread.sleep(2000);
            System.out.println("run ThreadName = " + this.currentThread().getName() + "end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class RunSleep {
    public static void main(String[] args) {
        MyThreadSleep myThreadSleep = new MyThreadSleep();//main线程执行此行代码
        System.out.println("begin = " + System.currentTimeMillis());//main线程执行此行代码
        //使用了start方法,myThreadSleep线程对象具有了执行的能力,此时相当于有了两个线程
        //这两个线程相互独立。myThreadSleep对象执行run里面的代码
        //因为线程的执行有不确定性,所以会出现下面的这种情况
        myThreadSleep.start();
        System.out.println("end = " + System.currentTimeMillis());
    }
}

执行结果:
begin = 1542265679689
end = 1542265679689
run ThreadName = Thread-0begin
run ThreadName = Thread-0end

getId() 方法

作用是取得线程的唯一标识。

停止线程

中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序。
大多数停止一个线程的操作使用 Thread.interrupt() 方法,一些轻率的家伙可能会被这个方法所迷惑,这个方法不会终止一个正在运行的线程,将会设置该线程的中断状态位,即设置为 true。线程会时不时地检查这个中断标志位,以判断线程是否应该被中断。它并不像 stop 那样会中断一个正在运行的线程。

在 Java 中,有以下三种方法可以中止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
  2. 使用 stop 方法强行终止线程,但是不推荐使用该方法。
  3. 使用 interrupt 中断线程。

当时的一个困惑,比如有两个线程,一个 main 和一个 thread-0 线程,main 执行到一半被 thread-0 抢走了是不是 main 就是中断了?
当然不是,我们看一下上面的三种中断正在运行的线程的方式就是到了,只有那三种。

interrupt() 方法
判断线程是否是停止状态
  1. this.interrupted():测试当前线程是否已经中断,线程的中断状态由该方法清除
  2. this.isInterrupted():测试线程是否已经中断
public class MyThreadInterrupt extends Thread{
    public void run(){
        super.run();
        for (int i = 0; i < 200000; i++) {
            System.out.println("i= " + (i+1));
        }
    }
}

public class RunInterrupt {
    public static void main(String[] args) { //main线程
        try {
            MyThreadInterrupt threadInterrupt = new MyThreadInterrupt();//new一个threadInterrupt线程对象
            threadInterrupt.interrupt();
            threadInterrupt.start();//相当于现在的threadInterrupt线程是runnable状态
            Thread.sleep(1000);//当前线程休眠1秒钟,休眠时另外一个线程将获得CPU时间
            threadInterrupt.interrupt();//threadInterrupt线程中断【线程不会真的中断,只是将中断标志位设置为true】
            System.out.println("是否停止1?" + Thread.interrupted());//执行这一句的是mian线程【interrupted() 方法测试当前线程是否中断,当前线程是main线程,结果为false】
            System.out.println("是否停止2?" + Thread.interrupted());//执行这一句的是mian线程【它从未中断过,所以这里打印的也是false】
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使 main 线程产生中断效果:

public class run(){
    public static void main(String[] args){
        Thread.currentThread().interrupt();
        System.out.println("是否停止1?" + Thread.interrupted());//这里的结果为true【通过上面那行代码将main线程的中断位设置为true】
        System.out.println("是否停止2?" + Thread.interrupted());//这里的结果为false【再次调用该方法清除中断位,所以这里显示的是false】
    }
}
使用 return 停止线程

将方法 interrupt() 与 return 结合使用也能实现停止线程的效果。

判断线程是否停止

暂停线程

暂停线程意味着此线程还可以恢复运行。在 Java 多线程中,可以使用 suspend() 方法暂停线程,使用 resume() 方法恢复线程运行。

suspend() 和 resume() 方法的使用
suspend() 和 resume() 方法的缺点——独占
public class SynchronizedObject {
    synchronized public void printString(){
        System.out.println("begin");
        if(Thread.currentThread().getName().equals("a")){
            System.out.println("a线程永远被suspend了!");
            Thread.currentThread().suspend();
        }
    }

public class Run6 {
    public static void main(String[] args) {
        try {
            final SynchronizedObject object = new SynchronizedObject();//这是一个公共同步对象
            Thread thread1 = new Thread(){//使用匿名内部类创建一个thread1对象
                public void run(){
                    object.printString();
                }
            };
            thread1.setName("a");
            thread1.start();
            Thread.sleep(1000);//相当于main线程休眠1秒钟,这是a线程可以执行(a线程在着休眠的1秒钟执行)
            Thread thread2 = new Thread(){//使用匿名内部类创建一个thread2对象
                public void run(){
                    System.out.println("thread2启动了,但是进入不了printString()方法!,只打印了一个begin");
                    System.out.println("因为printString()方法被a线程锁定并且永远suspend暂停了!");
                    object.printString();
                }
            };
            thread2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

begin
a线程永远被suspend了!
thread2启动了,但是进入不了printString()方法!,只打印了一个begin
因为printString()方法被a线程锁定并且永远suspend暂停了!

上面的程序显示,a 线程在运行 printString() 方法的时候被 suspend(暂停)了,所以造成公共资源被 a 占据,另外的线程不能对它进行访问,造成公共资源的浪费。

yield() 方法

yield() 方法的作用是放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 执行时间。但放弃时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。

线程的优先级

yield 方法

yield() 方法的作用是放弃当前 CPU 资源。但是放弃时间不确定,有可能刚放放弃,但又马上获得 CPU 时间片。

守护线程

在 Java 线程中有两种线程,一种是用户线程,一种是守护线程。当进程中不存在非守护线程的时候,守护线程就会自动销毁。守护线程的典型应用就是 GC (垃圾回收)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值