Java学习笔记--线程

来源:互联网 发布:同学会搞破鞋 知乎 编辑:程序博客网 时间:2024/05/20 00:16

从我第一篇Java学习笔记系列开始,到现在所写的程序全是单线程程序,也就是程序从main()进入到结束只有一个流程,有时候我们需要设计程序拥有多个流程,也就是我要说的多线程(multi-thread)程序。


线程简介

我们先来看一个龟兔赛跑的例子(单线程实现):
题目要求:
设计一个龟兔赛跑游戏,赛程长度为10歩,每经过一秒,乌龟前进一步,兔子则可能前进两歩也有可能睡觉。

import static java.lang.System.out;public class TortoiseHareRace {    public static void main(String[] args){        boolean[] flags = {true, false};        int totalStep = 10;                  //总步数        int tortoiseStep = 0;                //乌龟步数        int hareStep = 0;                    //兔子步数        out.println("龟兔赛跑开始...");        while(tortoiseStep < totalStep && hareStep < totalStep){            tortoiseStep++;                  //乌龟步数加一            out.printf("乌龟跑了 %d 步...\n", tortoiseStep);            boolean isHareSleep = flags[((int) (Math.random()* 10))% 2];   //随机睡觉            if(isHareSleep){                out.println("兔子睡着了");            }else{                hareStep += 2;                out.printf("兔子跑了 %d 步...\n", hareStep);            }        }    }}

再来看看多线程是怎么实现的:

乌龟:

public class Tortoise implements Runnable {    private int totalstep;    private int step;    public Tortoise(int totalstep){        this.totalstep = totalstep;    }    @Override    public void run(){        while(step < totalstep){            step++;            System.out.printf("乌龟跑了 %d 歩\n", step);        }    }}

兔子:

public class Hare implements Runnable {    private boolean[] flags = {true, false};    private int totalstep;    private int step;    public Hare(int totalstep){        this.totalstep = totalstep;    }    public void run(){        while(step < totalstep){            boolean isHareSleep = flags[((int) (Math.random()* 10))% 2];            if(isHareSleep){                System.out.println("兔子睡着了");            }else{                step += 2;                System.out.printf("兔子跑了 %d 歩\n", step);            }        }    }}

Main函数:

public class TortoiseHareRace2 {    public static void main(String[] args){        Tortoise tortoise = new Tortoise(10);        Hare hare = new Hare(10);        Thread tortoisethread  = new Thread(tortoise);        Thread harethread = new Thread(hare);        tortoisethread.start();        harethread.start();    }}

从上面的代码中我们可以看到线程使用方面一些基本的语法,在这里我就不再赘述。
其实,在Java中我们想要在main()以外设计独立的流程,可以攥写操作类接口java.lang.Runnable,流程的进入点是run方法。


Thread与Runnable

一般来说,我们可以认为JVM是一台虚拟的计算机,(粗略认为)但他只安装了一颗成为主线程的CPU,可执行main()定义的执行流程,如果想为JVM加装CPU,就要创建Thread实例,要启动额外CPU就要调用Thread的start方法。额外CPU的执行流程进入点,可以定义在Runnable接口的run方法中。

除了将流程定义在Runnable的run方法中,我们还可以继承Thread类,重新定义run方法,我建议使用第一种方法,因为我们操作Runnable接口的好处就是比较有弹性,你的类还有机会继承其他类。若我们继承了Thread之后,那我们的类就只是一种Thread,一般我们为了直接利用Thread中定义的方法,才会选择继承Thread。


线程生命周期

Daemon线程

主线程会从main方法开始执行,知道main方法结束之后停止JVM。如果主线程中启动了额外的线程,默认会等待被启动的所有线程都执行完run方法才终止JVM。如果一个线程被标示为Daemon线程,在所有的非Daemon线程都结束时,JVM就会自动终止。

我们可以使用setDaemon方法来设定一个线程是否为Daemon线程,来看一个例子:

public class DaemonDemo {    public static void main(String[] args){        Thread thread = new Thread(){            public void run(){                while(true){                    System.out.println("Orz");                }            }        };        thread.setDaemon(true);   //把此线程定义为Daemon线程        thread.start();    }}

如果我们没有使用setDaemon设定为true,那么程序会不断的输出Orz而不终止。使用isDaemon方法可以判断这个线程是否为Daemon线程。
我们默认所有的Daemon线程产生的线程也是Daemon线程。


Thread基本状态图

这里写图片描述

在调用Thread实例start方法后,基本状态为可执行(Runnable),被阻断(Blocked),执行中(Running)。

实例化Thread并执行start方法之后,线程进入Runnable状态,此时线程尚未真正开始执行run方法,必须等待排班器(Scheduler)排入CPU执行,线程才会执行run方法,进入Running状态。线程看起来是同时执行,但实际上同一时间点上,一个CPU还是只能执行一个线程,只是CPU会不断切换线程,且切换的动作很快,所以看起来像是同时执行。

线程具有优先权,可以使用Thread的setPriority方法设定优先权,可设定值在1到10之间,默认是5,超出1到10的设定值会抛出IllegalArgumentException,数字越大优先权越高,排班器越优先排入CPU,如果优先权相同,则输流执行。

有几种状况会使线程进入Blocked,如调用Thread.sleep(),进入synchronized前竞争对象锁定的阻断,调用wait方法的阻断,等待输入输出完成。运用多线程,当某线程进入Blocked时,让另一线程排入CPU执行(成为Running),避免CPU空闲下来,经常是改进效能的方式之一。

线程因为输入/输出进入Blocked状态,在完成输入/输出之后,会回到Runnable状态,等待排班器排入执行(Running状态)。一个进入Blocked状态的线程,可以由另一个线程调用该线程的interrupt方法,让他离开Blocked状态。

举个例子来说,使用Thread.sleep()会使线程进入Blocked状态,现在有其他线程调用该线程的interrupt方法,会抛出InterruptedException异常对象,这是让线程醒过来的方式。来看一个简单的范例:

public class InterruptedDemo {    public static void main(String[] args){        Thread thread = new Thread(){            @Override            public void run(){                try{                    Thread.sleep(99999);                }catch (InterruptedException ex){                    System.out.println("我醒了");                }            }        };        thread.start();        thread.interrupt();       //主线程调用thread的interrupt()    }}

安插线程

如果A线程正在运行,流程中允许B线程加入,等到B线程执行完毕之后在继续A线程流程,可以使用join方法完成这个 需求。

当线程使用join方法加入至另一线程的时候,另一线程会等待被加入的线程完成工作,然后继续它的动作。来看一个例子:

public class JoinDemo {    public static void main(String[] args) throws InterruptedException{        out.println("Main thread 开始...");        Thread threadB = new Thread(() -> {            out.println("Thread B开始...");            for (int i = 0; i < 5; i++){                out.println("Thread B 执行...");            }            out.println("Thread B 结束...");        });        threadB.start();        threadB.join();          //将线程B加入主线程流程        out.println("Main thread 将结束...");    }}

有时候可能加入的线程处理太久,你不想无止境等待这个线程工作完毕,则可以在join()时指定时间,如join(10000),着表示加入流程的线程至多可以处理10000毫秒,也就是10秒,如果加入的线程还没有执行完毕就不管他了,目前线程可继续执行原本的工作流程。


停止线程

线程完成run方法之后,就会进入Dead,进入Dead(或已经调用过start方法)的线程不可以再次调用start方法,否则会抛出IllegalThreadStateException。

Thread类上有定义stop方法,不过被标示为Deprecated,这表示这些API虽然有,但由于一些原因并没有删除它,所以写程序的时候是十分不建议使用这样的API,这样的API还有像线程的暂停,重启(suspend(), resume())等,如果我们要停止线程或者是暂停,重启必须视需求操作,让线程跑完应有的流程,而非直接停止!


关于ThreadGroup

每个线程都属于某个线程组群。若在main方法中产生一个线程,该线程会属于main线程组。可以使用以下程序获得目前线程所属的线程群组名:

Thread.currentThread().getThreadGroup().getName();

每个线程产生时,如果没有规定所属群组,则归入产生该子线程的线程群组。线程一旦归入某个群组,就无法再更换。

我们可以使用java.lang.ThreadGroup类来管理群组中的线程。可以使用以下方式产生群组,并在产生线程时指定所属群组:

ThreadGroup group1 = new ThreadGroup("group1");ThreadGroup group2 = new ThreadGroup("group2");Thread thread1 = new Thread(group1, "group1's member");Thread thread2 = new Thread(group2, "group2's member");

ThreadGroup中的某些方法,可以对群组中的所有线程产生作用。例如,interrupt方法可以中断群组中的所有线程,setMaxPriority方法可以设定群组中所有线程的最大优先权。

如果想要一次取得群组中的所有线程,可以使用enumerate方法。

Thread[] threads = new Thread[threadGroup1.activeCount()];threadGroup1.enumerate(threads);

activeCount方法取得群组中的线程数量,enumerate方法要传入Thread数组,这会将线程对象设定至每个数组索引。

ThreadGroup中还有个uncaughtException方法,群组中某个线程发生异常儿未捕捉时,JVM会调用此方法进行处理。如果ThreadGroup还有父ThreadGroup,就会调用父ThreadGroup的uncaughtException方法,否则看看是否为ThreadDead实例。若是则什么都不做,若不时则调用异常的printStrackTrace方法。如果必须定义ThreadGroup中线程的异常处理行为,可以重新定义次方法,例如:

public class ThreadGroupDemo {    public static void main(String[] args){        ThreadGroup group  = new ThreadGroup("group") {            @Override            public void uncaughtException(Thread thread, Throwable throwable) {       //得到异常线程,异常信息                System.out.printf("%s: %s\n", thread.getName(), throwable.getMessage());            }        };        Thread thread = new Thread(group, () -> {            throw new RuntimeException("测试异常");        });        thread.start();    }}

uncaughtException方法的第一个参数可取得发生异常的线程实例,第二个参数可取的异常对象。

在JDK5之后,如果线程组之中的线程发生异常,uncaughtException方法处理顺序是:

  • 如果ThreadGroup有父ThreadGroup,就会调用父ThreadGroup的uncaughtException方法;
  • 否则看看Thread是否使用setUncaughtExceptionHandler方法设定Thread.UncaughtExceptionHandler实例,有的话就会调用其uncaughtException方法;
  • 否则看看异常是否为ThreadDead实例,若是的话则什么都不做,若不是的话调用异常的printStrackTrace方法。

synchronized与volatile

如果一个类会使线程存取同一对象相同资源时因发竞速现象,我们就说这个类是不具备线程安全的类,具体的例子我不在贴出,有疑问的读者可以自行百度。


使用synchronized

每个对象都会有个内部锁定,或称为监控锁定。被标示为synchronized的区块将会被监控,任何线程要执行synchronized区块都必须先取得指定的对象锁定。如果线程A已经取得对象锁定开始执行synchronized区块,B线程也想执行synchronized区块,会因无法取得对象锁定而进入等待锁定的状态,直到A线程释放锁定,B线程才有机会取得锁定对象儿执行synchronized区块。

实际上在等待对象锁定的时候,线程也会进入Blocked状态。线程若因尝试执行synchronized区块而进入Blocked,在取得锁定之后,会先回到Runnable状态,等待CPU排班器排入Running状态。

在之前讨论的Collection与Map都未考虑线程安全,可以使用Collections的synchronizedCollection(),synchronizedList(),synchronizedSet(),synchronizedMap()等方法,这些方法会将传入的Collection,List,Set,Map操作对象打包,返回具线程安全的对象。例如我们如果经常对List进行添加和移除的工作:

List<String> list = new ArrayList<>();synchronized(list){    ...    list.add("...");}...synchronized(list){    ...    list.remove("...");}

那么我们可以这样简化:

List<String> list = Collection.synchronizedList(new ArrayList<String>());

在Java中的synchronized提供的是可重入同步,也就是线程取得某对象锁定之后,若执行的过程中又要执行synchronized,尝试取得锁定的对象来源又是同一个,可以直接执行。

因为线程无法取得锁定时会造成阻断,所以不正确的使用synchronized有可能造成效能低落,另一个问题则是死结。例如:有些资源在多线程下交叉使用,就有可能造成死结。来看一个例子:

class Resource{    private String name;    private int resource;    Resource(String name, int resource){        this.name = name;        this.resource = resource;    }    String getName(){        return name;    }    synchronized int doSome(){        return ++resource;    }    synchronized void cooperate(Resource resource){        resource.doSome();        System.out.printf("%s: 整合 %s 的资源\n", this.name, resource.getName());    }}public class DeadLockDemo {    public static void main(String[] args){        Resource resource1 = new Resource("resource1", 10);        Resource resource2 = new Resource("resource2", 20);        Thread thread1 = new Thread(() -> {            for(int i = 0; i < 10 ; i++){                resource1.cooperate(resource2);            }        });        Thread thread2 = new Thread(() -> {            for(int i = 0; i < 20 ; i++){                resource2.cooperate(resource1);            }        });        thread1.start();        thread2.start();    }}

上面的程序发生死结是几率问题。多次执行后会发现,又是程序可顺利执行完成,有时程序会整个停顿。

造成上面的原因在于,thread1调用resource1.corportate(resource2)时,thread1会取得resource1的锁定,若此时thread2也调用resource2.corportate(resource1),就会取得resource2的锁定,凑巧的是thread1现在打算运用穿入的resource2调用doSome(),那么它就会尝试取得resource2的锁定,但是现在resource2的锁定在thread2手上,所以thread1就会被阻塞,相同的,resource1被传入resource2的方法之中,那么现在thread2就会尝试取得resource1的锁定,但是resource1的锁定在thread1手上,所以thread2也会被阻塞,最后就造成死结的情况。

我们在程序设计中,应尽量避免死结的发生。


使用volatile

synchronized要求达到所标示区块的互斥性与可见性,互斥性是指synchronized区块同时间只能有一个线程,可见性是指线程离开synchronized区块后,另一线程接触到的就是上一线程改变后的对象状态。

对与可见性的要求可以使用volatile达到变量范围。在讨论变量的可见性之前,我们先来看一个例子:

class Variable1 {    static int i = 0, j = 0;    static void one(){        i++;        j++;    }    static void two(){        System.out.printf("i = %d, j = %d\n", i, j);    }}public class Variable1Test{    public static void main(String[] args){        Thread thread1 = new Thread(() -> {            while(true){                Variable1.one();            }        });        Thread thread2 = new Thread(() -> {            while(true){                Variable1.two();            }        });        thread1.start();        thread2.start();    }}

在这个程序运行之后有可能出现j远大于i的结果。当thread2调用Variable1.two()取得i值之后,有可能切换thread1不断执行Variable1.one()多次,在切回thread2,就有可能发生这种情况。

我们可以像之前那样给one,two方法上标示synchronized,这样每次thread1调用one时,thread2就必须等待thread1释放锁定,才能调用two,thread2调用two时,thread1就必须等待thread2释放锁定,才能调用one。这样的确可以解决问题,但是当我们调用one时,其他线程就不能调用two,反之亦然。这样会看到执行速度明显变慢。

对于为何j会大于i,这和线程的“快取”有关,在这里不详细描述,要阻止这种情况的发生,我们可以在变量上声明volatile,表示变量是不稳定的,易变的,但即使这样,也只是提高了i,j相等的几率。被标示为volatile的变量不允许线程快取。实际上,如果我们要保证i,j相等的话,就要使用synchronized。


等待与通知

wait(),notify(),notifyAll()是Object定义的方法,可以通过这三个方法控制线程释放对象的锁定,或者通知线程参与锁定竞争。

在线程执行synchronized范围的程序代码期间,若调用锁定对象的wait方法,线程会释放对象锁定,并进入对象等待集合而处于阻断状态,其他线程可以竞争对象锁定。

放在等待集合的线程不会参与CPU排班,wait()可以指定等待时间,时间到了之后线程会再次加入排班,如果指定时间0或不指定,则线程会持续等待,直到被中断(调用interrupt())或是告知(notify())可以参与排班。

被竞争锁定的对象调用notify方法时,会从对象等待集合中随机通知一个线程加入排班,如果调用notifyAll方法,所有等待集合中的线程都会被通知参与排班。

线程调用对象wait方法时,会先让出synchronized区块的使用权并等待通知,或是等待指定时间,直到被notify方法或时间到时,(取得对象锁定之后)在从调用wait()处开始执行。

来举一个生产者,店员,消费者之间的例子,消费者每次生产一个int整数交给店员:

public class Producer implements Runnable{    private Clerk clerk;    public Producer(Clerk clerk){        this.clerk = clerk;    }    public void run(){        System.out.println("生产者开始生产整数... ...");        for(int product = 1; product <= 10; product++){            try{                clerk.setProduct(product);        //将产品交给店员            }catch (InterruptedException ex){                throw new RuntimeException(ex);            }        }    }}

程序中使用for循环生产1~10的整数,Clerk代表店员,可通过setProduct方法将生产的整数交给店员。

消费者从店员处取走int整数:

public class Consumer implements Runnable {    private Clerk clerk;    public Consumer(Clerk clerk){        this.clerk = clerk;    }    public void run(){        System.out.println("消费者开始消耗整数... ...");        for(int i = 1; i <= 10; i++){            try{                clerk.getProduct();        //从店员处取走产品            }catch (InterruptedException ex){                throw new RuntimeException(ex);            }        }    }}

程序中使用for循环来消费10次整数,可通过Clerk的getProduct方法,从店员处取走整数。

店员一次只能持有一个int整数,必须尽到要求等待与通知的职责:

public class Clerk {    private int product = -1;    public synchronized void setProduct(int product) throws InterruptedException{        waitIfFull();       //看看店员有没有空间收产品,没有的话就稍后        this.product = product;        System.out.printf("生产者设定 %d \n", this.product);        notify();         //通知等待中的线程(消费者)    }    private synchronized void waitIfFull() throws InterruptedException{        while(this.product != -1){         //店员由产品,没有空间            wait();        }    }    public synchronized int getProduct() throws InterruptedException{        waitIfEmpty();              //看看店员有没有货,没有的话就稍后        int p = this.product;        this.product = -1;           //表示货物被取走        System.out.printf("消费者取走 %d \n", p);        notify();                  //通知等待集合中的线程(生产者)        return p;    }    private synchronized void waitIfEmpty() throws InterruptedException{        while(this.product == -1){            wait();              }    }}

因为线程有可能在未经notify方法,interrupt方法或逾时情况下私自苏醒,所以wait方法一定要在条件式成立的循环中执行。

用以下程序示范生产者,消费者,店员:

public class ProducerConsumerDemo {    public static void main(String[] args){        Clerk clerk = new Clerk();        new Thread(new Producer(clerk)).start();        new Thread(new Consumer(clerk)).start();    }}

程序的运行结果希望大家自己能够尝试一下。

5 0
原创粉丝点击