7、多线程(1)

一、基本概念

  • 1.程序:指令集 静态概念

  • 2.进程:操作系统调度程序,是一个动态的概念。是程序的一次动态执行的过程,占用特定的地址空间。每个进程都是独立的,由3部分组成cpu、data、code。缺点:内存的浪费,cpu负担重。

  • 3.线程:在进程内多条执行路径。线程是进程中一个单一的连续控制流程(执行路径)。线程又被称为轻量级进程,一个进程可拥有多个并行的线程,一个进程中的线程共享相同的内存单元/内存地址空间-->可以访问相同的变量和对象,而且它们从同一堆中分配对象-->通信、数据交换、同步操作。由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。

  • 4.比较

区别 进程 线程
根本区别 作为资源分配的单位 调度和执行的单位
开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器PC线程切换的开销小
所处环境 在操作系统中能同时运行多个任务(程序) 在同一应用程序中有多个顺序流同时执行
分配内存 系统在运行的时候会为每个进程分配不同的内存区域 除了cpu之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源)线程组只能共享资源
包含关系 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线程的,而是多条线(线程)共同完成的 线程是进程的一部分,所以线程有的时候被称为是轻量级进程或轻权进程

二、相关类的使用

2.1 使用Thread类实现多线程

这里我们给出一个简单的例子:
Rabbit_01.java

package cn.itcast.day169.thread;
/*
 * 模拟龟兔赛跑
 * 1、创建多线程,继承Thread类+重写run方法(其中的代码都是线程体)
 * 2、使用线程:创建子类对象+对象.start(),调用start方法表示线程的启动
 * */
//兔子
public class Rabbit_01 extends Thread{
    public void run() {
        //线程体
        for(int i = 0; i < 100; i++){
            System.out.println("兔子跑了: " + i + "步");
        }
    }
}
//乌龟
class Tortoise extends Thread{
    public void run() {
        //线程体
        for(int i = 0; i < 100; i++){
            System.out.println("乌龟跑了: " + i + "步");
        }
    }
}

RabbitApp_01.java

package cn.itcast.day169.thread;
//我们会将此类加到一个线程组中
public class RabbitApp_01 {
    public static void main(String[] args) {
        //创建子类对象
        Rabbit_01 rab = new Rabbit_01();
        Tortoise tor = new Tortoise();
        //调用start方法
        rab.start();//不要调用run方法,调用run方法只是调用一个普通方法,没有开启一个线程
        tor.start();//此时这里就有两条线程了
        for(int i = 0; i < 1000; i++){
            System.out.println("main " + i );
        }
    }
}

说明:这里可以看到我们通过继承Thread类实现了多线程,但是很明显有个缺点,就是如果一个类本身就已经继承了一个父类,那么此类就不能再继承Thread类来实现多线程了。

2.2 使用Runnable实现多线程

为了解决上面的问题,我们可以实现Runnable接口。优点:可以同时实现继承,实现Runnable接口方式要通用一些。

  • 1)避免单继承
  • 2)方便共享资源 , 同一份资源, 多个代理访问。

Runnable使用了静态代理的设计模(继承相同的接口),首先我们看什么是静态设计模式:
StaticProxy.java

package cn.itcast.day169.thread;
/*静态代理
 * 1、真实角色
 * 2、代理角色:要持有真实角色的引用
 * 3、二者实现相同的接口
 * */
public class StaticProxy {
    public static void main(String[] args) {
        You you = new You();//创建真实角色
        //创建代理角色+ 持有真实角色的引用
        WeddingCompany company = new WeddingCompany(you);
        //执行任务
        company.marry();
    }
}

//接口
interface Marry{
    public abstract void marry();
}

//真实角色
class You implements Marry{
    public void marry() {
        System.out.println("you and girlfriend marry");
    }
}

//代理角色
class WeddingCompany implements Marry{
    private Marry you ;
    public WeddingCompany() {}
    //这里我们通过构造方法传递真实引用,当然我们也可以使用setter方法来传递
    public WeddingCompany(Marry you) {
        this.you = you;
    }
    
    private void befor(){
        System.out.println("布置新房");
    }

    public void marry() {
        //在这里我们可以添加一些真实对象中没有的方法,完成一些额外的工作
        befor();
        you.marry();
        after();
    }
    private void after(){
        System.out.println("闹婚房");
    }
}

说明:静态代理本质上就是一个类替代执行另一个类的相关方法,在执行此方法的时候还会附加执行一些操作,而不是直接去执行实际类的方法,相当于一种扩展。在实现静态代理的时候需要注意的是,代理类和被代理类需要实现相同的接口,而执行的方法都是代理类中的方法,同时代理类中的方法需要完成被代理类中需要完成的功能。例子中代理类WeddingCompany 和被代理类You 都继承的接口是Marry,本来是You结婚,但是我们将这项工作交给代理类,不仅完成结婚的工作,还处理了一些其他事情。

下面我们看如何使用Runnable实现多线程:
Programer.java

package cn.itcast.day169.thread;
//使用Runnable创建线程
/*1、类实现Runable接口,重写run方法-->真实角色类
 * */
public class Programer implements Runnable{
    public void run() {
        for(int i = 0; i < 1000; i++){
            System.out.println("一边做a, 一边做b");
        }
    }
}

ProgramerApp.java

package cn.itcast.day169.thread;
//使用Runnable创建线程
/*1、类实现Runable接口,重写run方法-->真实角色类
* 2.启动多线程,使用静态代理
*   1)创建真实角色
*   2)创建代理角色
*   3)调用start方法启动线程 
* */
public class ProgramerApp {
    public static void main(String[] args) {
        //1)创建真实角色
        Programer programer = new Programer();
        //2)创建代理角色
        Thread proxy = new Thread(programer);
        //3)调用start方法启动线程 
        proxy.start();
        for(int i = 0; i < 1000; i++){
            System.out.println("一边聊天");
        }
    }
}

说明:这里我们看到实现实例化被代理类Programer ,然后使用此类创建Thread代理类。这样便实现了多线程。这里我们推荐使用这种方法。此种方式避免单继承的局限性,同时方便共享资源。

2.3 通过Callable接口实现多线程

  • 在上面的方式中我们可以看到使用Runnable接口时,run方法是不能有返回值的,但是有时候我们不可避免的希望得到返回值,此时我们就可以使用Callable这个接口。Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。

  • CallableRunnable有几点不同:
    1)Callable规定的方法是call(),而Runnable规定的方法是run
    2)call方法可抛出异常,而run方法是不能抛出异常的
    3)Callable的任务执行后返回值,运行Callable任务可拿到一个Future对象,而Runnable的任务是不能返回值的。Futrue表示异步计算的结果。它提供了检查计算是否完成的方法啊,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行的情况,可取任务的执行,还可以获取任务的执行的结果。
    缺点:繁琐

  • 使用方式
    1)创建Callable实现类+重写call方法
    2)借助执行调度服务ExecutorService获取Futrue对象

ExecutorService ser = Executors.newFixedThreadPool(2);
Futrure result = ser.submit(实现类对象);

3)、获取值result.get()
4)停止服务ser.shutdownNow()

Callable_01.java

package cn.itcast.day169.thread;
import java.util.concurrent.*;
/*使用Callable创建线程 */
public class Callable_01 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建两个线程
        ExecutorService service = Executors.newFixedThreadPool(2);
        Race tortoise = new Race("千年王八", 1000);
        Race rabbit = new Race("兔子", 500);
        //获取值
        Future<Integer> result1 = service.submit(tortoise);
        Future<Integer> result2 = service.submit(rabbit);
        Thread.sleep(2000);//2秒
        tortoise.setFlag(false);//停止线程体循环
        rabbit.setFlag(false);
        int num1 = result1.get();
        int num2 = result2.get();
        System.out.println("乌龟跑了 " + num1 + "步");
        System.out.println("兔子跑了 " + num2 + "步");
        //关闭线程
        service.shutdownNow();
    }
}

class Race implements Callable<Integer>{
    private String name ;//名称
    private long time ;//延时时间,用来表示速度
    private boolean flag = true;
    private int step = 0;//步
    
    public Race(){}
    public Race(String name ){
        this.name = name;
    }

    public Race(String name, long time) {
        this.name = name;
        this.time = time;
    }
    //重写call方法
    public Integer call() throws Exception {
        while(flag){
            Thread.sleep(time);
            step++;
        }
        return step;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public long getTime() {
        return time;
    }
    public void setTime(long time) {
        this.time = time;
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public int getStep() {
        return step;
    }
    public void setStep(int step) {
        this.step = step;
    }
}

说明:这里的使用方式和之前的稍微有点不同。

三、线程状态

3.1 基本概念

  • 线程被创建之后我们调用start方法,此时线程进入就绪状态(可运行状态),当cpu调度的时候进入运行状态,当线程执行完后此线程就停止了,即终止状态。但是如果线程在还没有执行完的时候被停止,那么就进入阻塞状态,如果阻塞状态解除之后线程又进入就绪状态,在之后cpu调度则还会接着进入运行状态进行执行。

  • 这里注意:死亡状态是线程声明周期中的最后一个阶段,线程死亡的原因有两个。一个是正常执行完,一个是线程被强制性的终止工作,如果通过执行stopdestroy方法来终止一个线程(不推荐使用这两个方法),前者会产生异常,后者是强制终止,不会释放锁。

  • 死亡状态如何停止线程
    在之前的例子中,其实我们已经看到,我们可以通过一个标志来停止线程。
    1、自然终止
    2、外部干涉

    • 1)线程类中定义线程体使用的标志
    • 2)线程体使用该标志
    • 3)提供对外的方法可以改变该标志
    • 4)外部根据条件调用该方法即可

使用标志位来停止线程:

package cn.itcast.day174.thread01;
public class Demo01 {
    public static void main(String[] args) {
        Study s = new Study();
        new Thread(s).start();
        //外部干涉
        for(int i = 0; i < 100; i++){
            if(i == 50){//注意:这里停下来的时间点不是很准确,由cpu决定
                s.stop();
            }
            System.out.println("main-->" + i);
        }
    }
}

class Study implements Runnable{
    private boolean flag = true;
    public void run() {
        while(flag){
            System.out.println("study thread...");
        }
    }
    //对外提供该表标志的方法
    public void stop(){
        this.flag = false;
    }
}
  • 阻塞
    • 1、join:合并线程,将多条线程合并为一个
    • 2、yield:暂停当前线程(暂停自己),执行其他线程
    • 3、sleep:暂停线程,休眠的时候不会释放锁。相关应用:
      1)与时间相关:数数和倒计时
      2)模拟网络延时

合并线程

package cn.itcast.day174.thread01;
/*合并线程 */
public class JoinDemo01 extends Thread{
    
    public static void main(String[] args) throws InterruptedException {
        
        JoinDemo01 demo = new JoinDemo01();
        Thread t = new Thread(demo);//新生状态
        t.start();//就绪状态
        //cpu调度就进入运行状态
        
        for(int i = 0; i < 100; i++){
            if(i == 50){
                //此时main方法阻塞了,必须让线程t运行完,就是刚开始谁先执行都是随机的,
                //但是当i的值为50后,便是t线程执行完后main线程才能再次执行
                t.join();
            }
            System.out.println("main..." + i);
        }
    }
    
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("join..." + i);
        }
    }
}

说明:这里其实是有两个线程,一个是线程t,一个是main。开始时是并发执行,但是到i的值为50的时候便需要t线程运行完成之后main线程才能开始执行。

暂停当前线程

package cn.itcast.day174.thread01;
/*暂停执行当前的线程(暂停自己),并执行其他线程*/
public class YieldDemo01 extends Thread{
    public static void main(String[] args) throws InterruptedException {
        
        YieldDemo01 demo = new YieldDemo01();
        Thread t = new Thread(demo);//新生状态
        t.start();//就绪状态
        //cpu调度就进入运行状态
        
        for(int i = 0; i < 100; i++){
            if(i % 20 == 0){
                //注意:这里的暂停不是很严格
                Thread.yield();//暂停自己,注意:写在谁的线程体内就暂停谁(这里暂停main),效果不是很明显
            }
            System.out.println("main..." + i);
        }
    }
    
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("yield..." + i);
        }
    }
}

说明:这里一定要注意到底是暂停哪个线程。

数数和倒计时

package cn.itcast.day174.thread01;
import java.util.Date;
import java.text.SimpleDateFormat;

//倒计时,倒数十个数,一秒内打印一个
public class SleepDemo01 {
    public static void main(String[] args) throws InterruptedException {
        test2();
    }

    public static void test1() throws InterruptedException {
        int num = 10;
        while (true) {
            System.out.println(num--);
            Thread.sleep(1000);// 暂停
            if (num <= 0) {
                break;
            }
        }
    }

    // 倒计时
    public static void test2() throws InterruptedException {
        Date endTime = new Date(System.currentTimeMillis() + 10 * 1000);
        long end = endTime.getTime();
        while (true) {
            System.out.println(new SimpleDateFormat("mm:ss").format(endTime));
            endTime = new Date(endTime.getTime() - 1000);
            Thread.sleep(1000);
            if (end - 10000 > endTime.getTime()) {
                break;
            }
        }
    }
}

模拟网络延时

package cn.itcast.day174.thread01;
//模拟网络延时,可能出现并发问题
public class SleepDemo02 {
    public static void main(String[] args) {
        Web12306 web = new Web12306();//真实角色
        //代理对象
        Thread t1 = new Thread(web, "黄牛1");//第二个参数是当前线程的名字
        Thread t2 = new Thread(web, "黄牛2");
        Thread t3 = new Thread(web, "黄牛3");
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

class Web12306 implements Runnable{
    private int num = 50;
    public void run() {
        while(true){
            if(num <= 0){
                break;
            }
            try {
                Thread.sleep(500);//500ms的延时
                //加入延时之后可能会造成资源冲突的问题,这就是并发问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + num-- + "张票");
        }
    }
}
  • 线程基本信息
方法 功能
isAlive() 判断线程是否还“活”着,即线程是否还未终止
getPriority() 获得线程的优先级数值
setPriority() 设置线程的优先级数值
setName() 给线程一个名字
getName() 取得线程的名字
currentThread() 取得当前正在运行的线程对象,也就是取得自己本身

下面给出两个例子:
MyThread.java

package cn.itcast.day174.thread01;
public class MyThread implements Runnable {
    private boolean flag = true;
    private int num = 0;

    public void run() {
        while (flag) {
            System.out.println(Thread.currentThread().getName() + num++);
        }
    }

    public void stop() {
        this.flag = false;
    }
}

InfoDemo01.java

package cn.itcast.day174.thread01;
//currentThread()静态方法
public class InfoDemo01 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        Thread proxy = new Thread(t1, "t1线程");//第二个参数是线程的名称
        //我们也可以这样设置线程名称
        proxy.setName("狗蛋");
        System.out.println(proxy.getName());
        System.out.println(Thread.currentThread().getName());//获取名称
        proxy.start();
        System.out.println("线程启动后的状态: " + proxy.isAlive());//查看线程是否“活着”
        Thread.sleep(200);
        t1.stop();
        Thread.sleep(200);
        System.out.println("线程停止后的状态: " + proxy.isAlive());
    }
}

InfoDemo02.java

package cn.itcast.day174.thread01;
//优先级
//MAX_PRIORITY  10最大
//MIN_PRIORITY  1最小
//NORM_PRIORITY  5默认
//优先级不代表绝对的优先,没有先后顺序,代表的是概率
public class InfoDemo02 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        Thread p1 = new Thread(t1, "t1线程");
        MyThread t2 = new MyThread();
        Thread p2 = new Thread(t2, "t2线程");
        p1.setPriority(Thread.MIN_PRIORITY);//设置优先级
        p2.setPriority(Thread.MAX_PRIORITY);//设置优先级
        p1.start();
        p2.start();
        
        Thread.sleep(100);
        t1.stop();
        t2.stop();
    }
}

说明:这两个例子都是用来取得线程的一些信息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容