JAVA进阶(06)多线程

一、三个概念

1、程序

  • 程序(Program)是一个静态的概念,一般对应于操作系统中的一个可执行文件

2、进程

(1)执行中的程序叫做进程(Process),是一个动态的概念

(2)特点:

  • 进程是程序的一次动态执行过程, 占用特定的地址空间
  • 每个进程由3部分组成:cpu、data、code,每个进程都是独立的,保有自己的cpu时间、代码、数据,进程一多会加大内存和 cpu 的负担
  • 多任务操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行

3、线程

(1)概念:一个进程可产生多个线程,同一进程的多个线程也可共享此进程的某些资源(代码、数据),线程又被称为轻量级进程

(2)特点:

  • 一个进程内部的一个执行单元,是程序中的一个单一的顺序控制流程
  • 一个进程可拥有多个并行的线程
  •  一个进程中的多个线程共享相同的内存单元,可以访问相同的变量和对象,从同一堆中分配对象并进行通信、数据交换和同步操作
  • 由于线程间的通信是在同一地址空间上进行的,使得通信更简便,信息传递速度更快
  • 线程消耗的资源非常少

å¾11-2 线ç¨å±äº«èµæºç¤ºæå¾.png

4、进程和线程的区别

  • 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位;
  • 每个进程都有独立的资源,进程间的切换会有较大的开销, 线程是轻量级的进程,多线程共享资源,线程切换的开销小;
  • 多进程是在操作系统中同时运行多个任务(程序), 多线程是在同一应用程序中有多个顺序流同时执行;

5、进程和程序的区别

  • 进程是程序的一部分,程序运行的时候会产生进程;
  • 一个程序可以没有进程,可以有一个进程,也可以有多个进程;

二、JAVA中的多线程实现

1、继承Thread类实现多线程

(1)实现步骤:   

  • 写一个类继承Thread,并写一个线程体(run 方法);
  • new 一个该类的对象;
  • 通过对象调用start()方法来启动一个线程,运行 run 方法;

(2)缺陷:由于 java 中的单继承的特性,如果该类已经继承了一个类,则无法再继承 Thread 类了

2、Runnable接口实现多线程

  • 克服了方法一的缺点
public class TestThread2 implements Runnable {//自定义类实现Runnable接口;
    //run()方法里是线程体;
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        //创建线程对象,把实现了Runnable接口的对象作为参数传入;
        Thread thread1 = new Thread(new TestThread2());
        thread1.start();//启动线程;
        Thread thread2 = new Thread(new TestThread2());
        thread2.start();
    }
}

三、线程状态

1、五种状态

    å¾11-4 线ç¨çå½å¨æå¾.png

(1) 新生状态(New)

  • 用new关键字建立一个线程对象后,该线程对象就处于新生状态

(2)就绪状态(Runnable)

   1、新生态线程调用start方法进入就绪状态,等待系统为其分配CPU

   2、以下3种状态也可进入就绪状态:

  • 阻塞线程:阻塞解除,进入就绪状态
  • 运行线程:调用yield()方法,直接进入就绪状态
  • 运行线程:JVM将CPU资源从本线程切换到其他线程

(3)运行状态(Running)

  • 获得 cpu 执行run方法,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡

(4)阻塞状态(Blocked)

   1、阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),有4种原因会导致阻塞:

  • 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态,当指定的时间到了后,线程进入就绪状态。
  • 执行wait()方法,使当前线程进入阻塞状态,当使用nofity()方法唤醒这个线程后,它进入就绪状态。
  • 线程运行时,某个操作进入阻塞状态,比如执行IO流操作,只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
  • join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

(5)死亡状态(Terminated)

   1、线程死亡的原因有两个:

  • 一个是正常运行的线程完成了它run()方法内的全部工作
  • 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(不推荐使用)

2、终止线程的典型方法(重要)

public class TestThreadCiycle implements Runnable {
    String name;
    boolean live = true;// 标记变量,表示线程是否可中止;
    public TestThreadCiycle(String name) {
        super();
        this.name = name;
    }
    public void run() {
        int i = 0;
        //当live的值是true时,继续线程体;false则结束循环,继而终止线程体;
        while (live) {
            System.out.println(name + (i++));
        }
    }
    public void terminate() {
        live = false;
    }
 
    public static void main(String[] args) {
        TestThreadCiycle ttc = new TestThreadCiycle("线程A:");
        Thread t1 = new Thread(ttc);// 新生状态
        t1.start();// 就绪状态
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程" + i);
        }
        ttc.terminate();
        System.out.println("ttc stop!");
    }
}

3、暂停线程执行sleep/yield

(1)暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:

  • sleep()方法:让线程进入阻塞状态,直到休眠时间满了,进入就绪状态,运行时有延迟
  • yield()方法:让线程直接进入就绪状态,让出CPU的使用权,运行时无明显延迟
Thread.sleep(2000);//调用线程的sleep()方法;
Thread.yield();//调用线程的yield()方法;

4、线程联合join()

(1)线程A在运行期间,调用线程B的join()方法,让B和A联合,这样线程A就必须等待线程B执行完毕后,才能继续执行

(2)示例代码:

public class TestThreadState {
    public static void main(String[] args) {
        System.out.println("爸爸和儿子买烟故事");
        Thread father = new Thread(new FatherThread());
        father.start();
    }
}
 
class FatherThread implements Runnable {
    public void run() {
        System.out.println("爸爸想抽烟,发现烟抽完了");
        System.out.println("爸爸让儿子去买包红塔山");
        Thread son = new Thread(new SonThread());
        son.start();
        System.out.println("爸爸等儿子买烟回来");
        try {
            son.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("爸爸出门去找儿子跑哪去了");
            // 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束
            System.exit(1);
        }
        System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");
    }
}
 
class SonThread implements Runnable {
    public void run() {
        System.out.println("儿子出门去买烟");
        System.out.println("儿子买烟需要10分钟");
        try {
            for (int i = 1; i <= 10; i++) {
                System.out.println("第" + i + "分钟");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("儿子买烟回来了");
    }
}

四、线程基本信息和优先级

1、获取线程基本信息的方法

表11-1线ç¨ç常ç¨æ¹æ³.png

2、线程的优先级

  • 处于就绪状态的线程,等待JVM来挑选
  • 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5
  • getPriority() 和 setPriority(10) 获得或设置线程对象的优先级
  • 优先级低只是意味着获得调度的概率低,并不是绝对先调用优先级高的线程后调用优先级低的线程

五、线程同步和并发

1、线程同步的概念

  • 多线程访问修改同一个对象,可能存在安全问题, 这时需要用到线程同步
  • 线程同步其实就是一种等待机制,多线程排队一个一个地访问这个对象

2、线程同步的实现

(1)synchronized 方法

  • 语法
public  synchronized  void accessVal(int newVal);
  • 原理: synchronized 方法控制对对象的类成员变量的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态

(2)synchronized块

  • 说明:将一个大的方法声明为synchronized 将会大大影响效率,块可以实现精确地控制到具体的成员变量,缩小同步的范围,提高效率
  • 语法:括号中的是对象锁
synchronized(syncObject)
   { 
   //允许访问控制的代码 
   }

3、死锁及解决方案

(1)死锁:一个同步块需要同时拥有两个以上对象的锁时,可能会发生死锁(synchronized 块的嵌套造成的)

           synchronized (lipstick) {//需要得到口红的“锁”;
                System.out.println(girl + "拿着口红!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                synchronized (mirror) {//需要得到镜子的“锁”;
                    System.out.println(girl + "拿着镜子!");
                }
 
            }

(2)解决方案:同步块的嵌套改为并列即可

           synchronized (lipstick) {
                System.out.println(girl + "拿着口红!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
            }

4、线程并发协作(生产者|消费者模式)

(1)3个概念

  • 生产者:指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
  • 消费者:指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
  • 缓冲区:生产者将生产好的数据放入“缓冲区”,消费者从缓冲区拿要处理的数据。

(2)缓冲区的好处

  • 实现线程的并发协作:有了缓冲区一个管放,一个管拿,互不影响;
  • 解耦了生产者和消费者: 生产者不需要和消费者直接打交道;
  • 解决忙闲不均,提高效率:生产慢依然可消费,消费慢依然可生产;

(3)示例代码

package com.wc.pro02;

public class TestProduce {
	public static void main(String[] args) {
        SyncStack sStack = new SyncStack();// 定义缓冲区对象;
        Shengchan sc = new Shengchan(sStack);// 定义生产线程;
        Xiaofei xf = new Xiaofei(sStack);// 定义消费线程;
        sc.start();
        xf.start();
    }
}

class Mantou {// 馒头
    int id;
 
    Mantou(int id) {
        this.id = id;
    }
}
 
class SyncStack {// 缓冲区(相当于:馒头筐)
    int index = 0;
    Mantou[] ms = new Mantou[10];
 
    public synchronized void push(Mantou m) {
        while (index == ms.length) {//说明馒头筐满了
            try {
               //wait后,线程会将持有的锁释放,进入阻塞状态;
               //这样其它需要锁的线程就可以获得锁;
                this.wait();
                //这里的含义是执行此方法的线程暂停,进入阻塞状态,
                //等消费者消费了馒头后再生产。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 唤醒在当前对象等待池中等待的第一个线程。
        //notifyAll叫醒所有在当前对象等待池中等待的所有线程。
        this.notify();
        // 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。
        ms[index] = m;
        index++;
    }
 
    public synchronized Mantou pop() {
        while (index == 0) {//如果馒头筐是空的;
            try {
                //如果馒头筐是空的,就暂停此消费线程(因为没什么可消费的嘛)。
                this.wait();                //等生产线程生产完再来消费;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        index--;
        return ms[index];
    }
}
 
class Shengchan extends Thread {// 生产者线程
    SyncStack ss = null;
 
    public Shengchan(SyncStack ss) {
        this.ss = ss;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("生产馒头:" + i);
            Mantou m = new Mantou(i);
            ss.push(m);
        }
    }
}
 
class Xiaofei extends Thread {// 消费者线程;
    SyncStack ss = null;
 
    public Xiaofei(SyncStack ss) {
        this.ss = ss;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            @SuppressWarnings("unused")
			Mantou m = ss.pop();
            System.out.println("消费馒头:" + i);
        }
    }
}

(4)生消模式应用情景:

  • 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
  • 对于生产者,没有生产产品之前,消费者要进入等待状态,而生产了产品之后,又需要马上通知消费者消费。
  • 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。

(5)线程通信: 在生产者消费者问题中,仅有synchronized是不够,还需要不同线程之间的消息传递(通信)

  • 注意:以下方法均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常

     è¡¨11-2 线ç¨é信常ç¨æ¹æ³.png

5、任务定时调度(Timer和Timetask

(1)代码

public class TestTimer {
    public static void main(String[] args) {
        Timer t1 = new Timer();//定义计时器;
        MyTask task1 = new MyTask();//定义任务;
        t1.schedule(task1,3000);  //3秒后执行;
        //t1.schedule(task1,5000,1000);//5秒以后每隔1秒执行一次!
        //GregorianCalendar calendar1 = new GregorianCalendar(2010,0,5,14,36,57); 
        //t1.schedule(task1,calendar1.getTime()); //指定时间定时执行; 
    }
}
 
class MyTask extends TimerTask {//自定义线程类继承TimerTask类;
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println("任务1:"+i);
        }
    }
}

(2)Timer

  • Timer类作用是定时或者每隔一定时间触发一次线程。
  • Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。

(3)TimerTask

  • TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。
  • 继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行

(3)说明:

  • 一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞
  • 如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现

猜你喜欢

转载自blog.csdn.net/stanwuc/article/details/83106701