(六)多线程附录


1、线程的加入

线程在启动后,并不一定能立即争抢到CPU,但使用join()方法后,线程会优先抢到CPU,示例代码如下:

class Test implements Runnable{
    public void run(){
        for(int i = 1 ;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
        }
    }
}
public class Demo1 {

    public static void main(String[] args) {
        Test t = new Test();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        t0.start();
        t1.start();
        
        for(int i = 1 ;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
        }

    }
}

此时结果如下:

主线程与T0、T1线程在争抢CPU,此时对代码稍加修改:


class Test implements Runnable{
    public void run(){
        for(int i = 1 ;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
        }
    }
}
public class Demo1 {

    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        t0.start();
        /*
         * 此时只有主线程与T0线程
         * T0线程虽然启动但不一定抢得到CPU
         * 但使用join()方法后
         * 主线程会将CPU让给T0线程
         */
        t0.join();
        t1.start();
        
        for(int i = 1 ;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
        }
    }
}

此时结果如下:

因为主线程将CPU让给了T0线程,此时只有T0线程在执行任务,因此结果是T0线程先循环10次,之后主线程抢到CPU并且启动了T1线程,然后主线程再与T1线程争抢CPU。
  再次修改示例代码,将t0.join()放在T1线程启动之后:

class Test implements Runnable{
    public void run(){
        for(int i = 1 ;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
        }
    }
}
public class Demo1 {

    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        t0.start();
        t1.start();

        // 将t0.join()放在T1线程启动之后
        t0.join();
        
        for(int i = 1 ;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+"十循第"+i+"次");
        }
    }
}

此时结果如下:

出现这种结果是因为join()方法只会让主线程等待,其他线程不受影响,此时t0.join()放在T1线程启动之后,当主线程进入等待,T0线程与T1线程仍然会争抢CPU,只要T0线程执行完任务代码,主线程就可以开始执行,并且与T1线程开始争抢CPU。


2、线程的正确停止

如何让线程停止?虽然在API文档中可以查阅到stop()方法,但已经过时,目前来说,只能等待线程将任务代码运行完成,之后自然停止。但线程内大多使用循环,为了能够使循环结束,应该使用设置退出标志的办法来实现线程停止,示例代码如下:

class Test implements Runnable{
    //创建退出标志flag
    boolean flag = true;
    public void run(){
        //使用退出标志
        while(flag){
            System.out.println(Thread.currentThread().getName()+"测试线程启动");
        }
    }
}
public class Demo2 {

    public static void main(String[] args) {
        Test t = new Test();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        
        t0.start();
        t1.start();
        /*
         * 设置能改变退出标志的任意方法
         * 例如定义一个变量i
         * 当i循环到500时
         * 改变退出标志为false
         * 实现任务代码的循环结束
         * 从而结束线程
         */
        int i = 1 ;
        while(true){
            if(i++==500){
                t.flag=false;
                //结束该死循环
                break;
            }
        }
    }
}

此时结果如下:
<br />
<br />
<br />
<br />
<br />
  并不是忘记上传图片,而是因为示例代码本身运行就是没有结果的。出现这种现象的原因是当主线程启动T0、T1线程之后,CPU依然被主线程占有,于是主线程继续向下执行,将flag改为了false,因此当T0、T1线程抢到CPU之后,经过判断退出标志为false,就直接跳过了循环,什么也没有执行,因此当然没有结果。针对这种现象,对代码做如下修改:

class Test implements Runnable{
    boolean flag = true;
    public void run(){
        while(flag){
            System.out.println(Thread.currentThread().getName()+"测试线程启动");
            /*
             * 此时可以不使用sleep()方法
             * 线程仍然会正常停止
             * 使用只是为了减少循环的次数
             * 方便查看结果
             */
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo2 {

    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        
        t0.start();
        t1.start();
        
         //此处的sleep()方法必不可少
         
        Thread.sleep(20);
        int i = 1 ;
        while(true){
            if(i++==500){
                t.flag=false;
                break;
            }
        }
    }
}

此时结果如下:

修改的方式即为在T0、T1线程启动之后,增加sleep()方法,使目前正在运行的线程放弃CPU执行休眠,也就是使主线程休眠20毫秒,这时T0、T1线程就会去各自执行任务代码,等主线程重新抢占到CPU并且向下执行改变退出标记的值后,T0与T1线程经过判断flag为false,就结束循环,线程也因此结束。


3、线程的“错误”停止

interrupt()方法虽然是中断线程,但实际上该方法并不能正确的停止线程。

API文档中关于该方法有提到中断状态这一概念,需要结合下面两个方法来理解:

public static boolean interrupted()

interrupted()方法是一个静态方法,用于测试当前线程是否已经中断。线程的中断状态由该方法清除。如果当前线程已经中断,则返回 true,否则返回 false。

public boolean isInterrupted()

isInterrupted()方法是一个实例方法,用于测试线程是否已经中断。线程的中断状态不受该方法的影响。如果该线程已经中断,则返回 true,否则返回 false。

简单来讲,这两个方法都会返回一个boolean类型的值来表示我们当前的线程是否中断,当线程在某些条件(详情查阅 Java官方API都不成立的情况下,调用interrupt()方法会设置该线程的中断状态。之后当我们调用interrupted()方法或isInterrupted()方法时会得到一个true值, 表明线程已中断。在某些条件之中,重点关注这种情况:

在调用Object类的wait()、wait(long)或 wait(long, int)方法,或者该类的join()、join(long)、join(long, int)、sleep(long)或 sleep(long, int)方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException。

当线程在调用上述方法时,一旦其他线程调用interrupted()方法,就会收到一个InterruptedException,这也就是在使用上述方法时需要用try-catch块捕获异常的原因。不过不仅如此,注意重点,中断状态会被清除,这就意味着当我们再去调用interrupted()方法或isInterrupted()方法时,将无法得到正确的返回值,示例代码如下:

public class InterruptThread extends Thread{

    public static void main(String[] args) {
        InterruptThread thread = new InterruptThread();
        System.out.println("启动线程");
        thread.start();
        try {
            sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("中断线程");
        thread.interrupt();
        try {
            sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("程序停止");
        
    }
    public void run(){
        while(true){
            System.out.println("线程正在运行……");
            /*
             * 减少运行结果的输出
             * 使每秒只输出一行结果信息
             * 便于查看
             * 相当于sleep(1000)
             * 那为何不直接使用sleep(1000)呢?
             */
            long time = System.currentTimeMillis();
            while((System.currentTimeMillis()-time<1000)){
            }
        }
    }

}

run()方法中是一个无限循环的任务代码,始终执行输出"线程正在运行……",当启动线程后,输出"启动线程",之后休眠3秒,会提示"中断线程",中断线程后再次休眠3秒会提示"程序停止",那么程序会停止吗?

此时结果如下:

可以很明显的看到interrupt()方法并没有使线程停止,当然还可以使用正确的线程停止方式来修改上述代码:

public class InterruptThread extends Thread{

    public static void main(String[] args) {
        InterruptThread thread = new InterruptThread();
        System.out.println("启动线程");
        thread.start();
        try {
            sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("中断线程");
        thread.interrupt();
        try {
            sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("程序停止");
        
    }
    public void run(){

         //使用修改退出标志的方式使循环结束

        while(!this.isInterrupted()){
            System.out.println("线程正在运行……");
            long time = System.currentTimeMillis();
            while((System.currentTimeMillis()-time<1000)){
            }
        }
    }

}

此时结果如下:

此时线程正确的停止了,是因为通过修改退出标志使得while循环结束,从而结束了线程。这里再解释一下之前注释中提到的问题,为什么使用while((System.currentTimeMillis()-time<1000)){}而不直接使用Thread.sleep(1000),因为当Thread调用sleep(1000)方法时,就会导致中断状态被清除,同时抛出异常,因此while循环的退出标志!this.isInterrupted()也会失效,不能返回正确的中断状态,线程因此也无法停止。


4、守护线程

守护线程(Daemon Thread),是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个经典的守护线程,并且这种线程并不属于程序中不可或缺的部分,与守护线程对应的是用户线程(User Thread)。因此,当所有的非守护线程结束时,不论守护线程的任务代码是否执行完毕,程序都将会停止。

在使用守护线程时需要注意一下几点:

  1. 线程使用setDaemon()必须在线程启动,也就是start()方法之前,否则会抛出IllegalThreadStateException异常,即不能把正在运行的常规线程设置为守护线程。
  2. 在守护线程中产生的新线程也是守护线程。
  3. 守护线程切记不要去访问固有资源,如文件、数据库,因为它可能会在任何时候发生中断。

将线程转换为守护线程可以通过调用Thread对象的setDaemon()方法来实现,示例代码如下:

//创建守护线程
class DaemonThread implements Runnable{
    public void run(){
        for(long i = 0;i<9999999L;i++){
            System.out.println("守护线程T1执行第"+i+"次");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//创建用户线程
class UserThread extends Thread{
    public void run(){
        for(int i = 0;i<5;i++){
            System.out.println("---用户线程执T0行第"+i+"次---");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Demo3 {

    public static void main(String[] args) {
        
        DaemonThread dt = new DaemonThread();
        UserThread ut = new UserThread();
        
        Thread t0 = new Thread(ut);
        Thread t1 = new Thread(dt);
        
        //将T1线程设置为守护线程
        t1.setDaemon(true);
        
        t0.start();
        t1.start();

    }
}

此时结果如下:

通过结果可以看出虽然守护线程设置了足够多的循环次数,但是当用户线程执行完任务代码之后,守护线程尚未执行完毕也跟随停止了。


5、volatile关键字

与之前讲解的synchronized类似,Volatile相当于轻量级的synchronized,在某些情况下比synchronized的开销更小,它在多线程开发中保证了共享变量的“可见性”,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改后的值。
  最常见的用法是修饰退出标志,当退出标志被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取最新的退出标志。但普通的共享变量被修改之后,什么时候被写入主存是不确定的,因此当其他线程去读取时,此时内存中可能还是原来的旧值,来看代码示例:

//假设线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
//任务代码
do();
}

//线程2
stop = true;

代码示例很常见,之前列举的设置退出标志也是这样操作的。但是事实上,这段代码并不一定会完全运行正确,将线程中断。在极大多数的时候,示例代码是能够把线程中断的,但是也有非常小的概率导致无法中断线程,一旦发生这种情况就会造成死循环了。
  出现这种情况的原因是每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将退出标志的值拷贝一份放在自己的工作内存当中。当线程2更改了退出标志的值之后,但是还没来得及写入主存当中,便去执行其他代码了,那么线程1由于不知道线程2对退出标志做出了更改,因此还会一直循环下去。
  但是用volatile关键字修饰后,当线程2进行修改时,会导致线程1的工作内存中缓存的退出标志无效,因此线程1再次读取退出标志时会去主存读取,那么线程1读取到的就是最新的正确的值。


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

推荐阅读更多精彩内容