Java 多线程

进程(Process)是程序的一次动态执行,每个进程都有自己独立的地址空间,进程之间共享数据变得困难,进程之间通过争抢 CPU 的时间片实现多个进程的并发执行。为了进一步提高程序的并发性,引入了多线程。线程(Thread)是进程内部更小的可执行单元,有了线程,进程不再是处理器调度的基本单位,而变成了资源的提供者,进程内部的线程成为最小执行单元,同一个进程内部的线程共享进程提供的资源。

同进程的状态转换一样,线程也有5个基本状态:新生态(New)就绪态(Ready)运行态(Run)阻塞态(Block)死亡态(Dead)

关于5中状态的转换可以参看操作系统的相关知识。这里简单介绍就绪态,指的是线程已经完全准备好,就只差CPU资源,他就可以执行了。

线程的创建

 

Java 的线程分为两种:用户线程守护线程

线程的创建(实例化)有两种方式:继承 Thread 类和实现 Runable 接口。

通过继承并重写父类的方法就可以得到自定义的线程类,然后实例化自定义的线程类得到自定义的线程对象。

 class MyThread extends Thread{
     public void run() {
    for(int i=0;i<10;i++){
        System.out.println("我是线程: "+Thread.currentThread().getName()+"- "+i+"次");
        }       
    }
 }

查看 Thread 源码就可以看到,Thread 类实际上是实现了 Runable 接口

 //Thread.class
 class Thread implements Runnable {}

并且提供了几种不同的构造方法,其中一种就是通过 Runable 接口来实例化 Thread 对象

 public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
    }

于是,我们也可以写一个实现了 Runable 接口的实现类,传入 Thread 构造方法来实例化 Thread 对象。

 class MyRunable implements Runnable{
     public void run() {
    for(int i=0;i<20;i++){
        System.out.println("我是线程: "+Thread.currentThread().getName()+"-"+i+"次");
        }   
     }  
 }

最后的输出结果为:

通过线程对象的 start() 方法,将线程变为就绪态,他会开辟新的线程来执行 run() 中的逻辑。值得注意的是,main 函数也是一个线程,叫做主线程,线程名为 main。

线程方法

 

public static void sleep(long millisecond)

他是一个静态方法,使线程休眠(毫秒)并让出 CPU,休眠时任何线程中断当前线程,会抛出 sleep interrupt 异常,接着当前线程的中断状态会被清除。

static Thread currentThread()

静态方法,返回当前对象的引用。通过对象的 getName() 获得线程名称。

public final void join() throws InterruptedException

加入这个线程,等待这个线程死亡,如果传入了时间,就让加入的线程先执行传入的时间长度。

public final void setDaemon(boolean on)

将此线程设为守护线程(true)或用户线程(false),当运行的唯一线程为守护线程时,Java 虚拟机将退出。

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。

public static void yield()

暂停当前正在执行的线程,并执行其他线程(让出此次CPU执行时间片,下次继续竞争)。

isAlive()

测试是否是活动状态。

setPriority()

设置线程优先级,优先级越高越先执行。

public void interrupt()

中断这个线程,但只是做了中断标记,抛出异常后会继续执行。

public static boolean interrupted()

测试当前线程是否中断,线程的中断状态 不受该方法的影响。

如果要中断线程,最好还是在 while 循环中使用 flag 标记的方法。

 public class ThreadTest2 {
    public static void main(String[] args) {
        MyThread1 mt = new MyThread1();
        mt.start(); //启动线程

        for(int i=0;i<50;i++){
            if(i==30){
 //             mt.interrupt(); //中断线程
                mt.flag = false;
            }
        }       
    }
 }

 class MyThread1 extends Thread{
    public boolean flag = true;
    public void run() {
        while(flag){
            System.out.println("线程"+Thread.currentThread().getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }       
    }
 }

线程同步

 

为了说明同步的重要性,先看一个样例:

 package com.learnThread;

 public class ThreadTest3 {

    public static void main(String[] args) {
        Runnable mr = new MyRunnable3();

        Thread t1 = new Thread(mr); //开辟两个线程
        Thread t2 = new Thread(mr);
        t1.start();
        t2.start();
    }
 }

 class MyRunnable3 implements Runnable{
    public int source = 10;
    public void run() {
        for(int i=0;i<10;i++){
            if(source>0)
                source--;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("source的值为:"+source);    
        }
    }
 }

运行输出的结果为:

分析一下为什么会出现这种结果,一种可能的情况是:线程 t1 执行,使 source-1,接着进入休眠,休眠让出处理结果,t2 执行,source 此时是 9,他也对 source 执行 source-1 操作,此时 source=8,接着 t2 进入休眠,当 t1 休眠结束时,接着往下执行,输出 source的值 8。此时 t2 可能仍在休眠,t1 继续执行,对 source-1,再休眠,t2 休眠结束,执行输出 source 的值8,再对 source-1,t1休眠结束,执行 source 输出,此时结果为 6。这样,造成了对临界资源操作出现紊乱。

而我们需要的结果是:每次线程对临界资源操作后,线程读到的是正确的数据。上述会出现2次相同的值得原因在于:t1 休眠时,t2又对 source 操作,导致 t1 读到的数据不是他所认为的 -1 操作后的值。因此,关键在于,每次线程操作临界资源的时候,只能有 t1 在临界区,而其他线程不能进入临界区。

线程在并发执行的时候,有时候会出现两个线程同时进入临界区操作临界资源的情况。这就需要使用同步和互斥对共享数据的访问进行控制。

同步指的是程序执行的先后次序,互斥指的是每次只有一个进程或线程使用临界资源。多个线程操作临界资源必须互斥地进行。有三种方法可以实现同步:

同步块synchronize(要同步的对象){ 要对共享数据的同步操作 }

同步方法public synchronize void method(){ 要对共享数据的同步操作 }

使用 Lock 锁使用实现了 Lock 接口的类操作临界资源。

第一种方法:使用 synchronize 同步块进行同步:

 class MyRunnable4 implements Runnable{
    public int source = 10;
    private Object obj = new Object();
    public void run() { 
        synchronized (obj) { //把要操作的临界资源放在同步块中,参数 obj 也可以换成 this
            for(int i=0;i<10;i++){
                if(source>0)
                    source--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("source的值为:"+source);    
            }
        }   
    }
 }

synchronize 传递的参数是要同步的对象,如何理解:通过 synchronize 关键字会在同步对象上加上同步标记,控制他们的同步运行。由于所有的对象都有一个父类 object,实例化的线程类也是 object 类型,当运行到同步块的时候,这个线程对象就会被打上同步标记,这些同步线程就会互斥访问临界资源。当在临界区的线程休眠时,通过 API 可以知道,sleep() 会让线程让出处理机,但是不会释放监视器,在这里指的就是同步锁,因此,当前线程虽然释放了 cpu 的使用权,但是仍然被锁在了临界区,其他线程无法进入临界区,只能等待,直到休眠时间结束。根据以上理解,这里的同步对象参数也可以使用 this 关键字,代表当前对象。当执行到同步块时,当前对象会被打上同步标记,打上同步标记的线程会互斥访问临界资源。

同步过后的输出结果为:

第二种方法:使用同步方法进行同步:

 class MyRunnable4 implements Runnable{
    public int source = 10;
    private Object obj = new Object();
    public void run() {     
        method(); //调用同步方法
    }

    //使用同步方法进行同步
    public synchronized void method() {
        for(int i=0;i<10;i++){
            if(source>0)
                source--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("source的值为:"+source);    
    }   
    }
 }

输出结果同上。

第三种方法:使用 Lock 进行同步:

Lock 的 API:

同使用同步锁:

 class MyRunnable4_1 implements Runnable{
    public int source = 10;
    private final ReentrantLock lock = new ReentrantLock(); //实例化一个同步锁
    public void run() { 
        //使用同步锁
        for(int i=0;i<10;i++){
            lock.lock(); //调用同步锁的锁方法,将操作临界区锁起来
            if(source>0)
                source--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("source的值为:"+source);    
            lock.unlock(); //调用同步锁的解锁方法
        }   
    }
 }

程序执行的结果同上。

多线程同步遵循几个原则:

    1. 保持同步块的代码简洁,因此不要将无关的执行放在同步块。

    2. 在持有锁时,不要对其他对象调用方法。

    3. 不要再同步块阻塞,因为同步耗费临界区资源。

关于死锁

 

在写多线程并发的程序的时候,尤其要注意避免造成线程死锁。

死锁出现的根本原因是对有限资源的竞争,死锁发生的四个条件和由此产生的死锁避免的方法可以参看操作系统方面的知识。

这里解释一个新的方法  wait() 方法。wait() 方法使线程进入等待状态,并释放监视器的所有权。直到其他对象调用 notify() 或者 notifyAll() 方法唤醒当前线程,才能继续执行。

线程池

 

线程池是在空闲队列中预先创建一定数量的线程,用完以后会对线程回收,减少频繁创建和销毁对象的开销。

Java 中线程池的顶级接口是 Excutor,是一个执行线程的工具,查看一下 API:

线程池的接口是 ExcutorService ,是 Excutor 的子接口,查看它的 API:

还有一个重要的类 Excutors

Excutors 类能够返回 ExcutorService 类型的对象,用来创建常见的不同类型的线程池。

下面选择 4 中常见的线程池创建方法来实现。

ExecutorService es = Executors.newSingleThreadExecutor();

创建单线程线程池,线程池里只有一个线程运行,可以保证线程的所有任务按任务提交的顺序执行。

ExecutorService es = Executors.newFixedThreadPool(2);

创建固定大小的线程池,可以让线程池中有参数个进程并发执行。

ExecutorService es = Executors.newCachedThreadPool();

创建可缓存的进程池,线程池大小超过任务所需线程会回收部分线程(60s不执行的任务),任务数增加时,又能添加新的线程,线程池的大小依赖操作系统或 JVM 能创建的最大线程大小。

ExecutorService es = Executors.newScheduledThreadPool(2);

创建一个无限大小的线程池,此线程池支持定时以及周期性执行任务。

 public class ThreadTest5 {

    public static void main(String[] args) {
        ExecutorService es = Executors.newSingleThreadExecutor(); //创建单线程线程池
        es.execute(new MyRunable5()); //执行线程
        es.execute(new MyRunable5()); //执行线程
        es.shutdown(); //关闭线程池
    }
 }

执行的结果为:

实践的具体实现参看 GitHub:https://github.com/HelloSwordsman/JavaPractice.git





评论

  1. #1

    dUoxKpta 2019-09-06 01:15:01
    dUoxKpta

  2. #2

    WEmXeAbt 2019-09-05 22:19:17
    WEmXeAbt

  3. #3

    zuLdJdBo 2019-09-05 19:41:57
    zuLdJdBo