文档章节

读书笔记 — Java高并发程序设计 — 第二章 — 基础(上)

XuePeng77
 XuePeng77
发布于 2017/01/31 22:48
字数 3614
阅读 21
收藏 0

1 有关线程的一些事

    线程的所有状态都在Thread中的State枚举中定义:

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

    NEW状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()方法调用时,才表示线程开始执行。

    当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。

    如果遇到synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。

    WAITING和TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进入一个有时限的等待。一般来说,WAITING的线程正是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。

    当线程执行完毕后,则进入TERMINATER状态,表示结束。

2 初始线程:线程的基本操作

2.1 新建线程

// 第一种新建线程的方式;
Thread t1 = new Thread();
t1.start();

    新建线程,并将它start()起来,start()方法会新建一个线程并让这个线程调用run()方法。

// 要注意run()和start()的区别
Thread t2 = new Thread();
t2.run();

    这里要注意,上面这段代码可以通过编译,也可以正常运行。但是,却不能建立一个新线程,而是在当前线程中调用run()方法。

不要用run()来开启新线程,它只会在当前线程中,串行执行run()中的代码。

    如果想要线程做些什么,就必须重载run()方法,把任务加进去:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example1 {

    public static void main(String[] args) {

        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        };
        t1.start();
    }

}

    上面的代码使用了匿名内部类,重载了run()方法。

    还可以通过继承Thread,重载run()方法,但考虑到Java是单继承的,也就是说继承这种宝贵资源很可贵,因此可以使用实现Runnable接口来做到相同的事情:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example2 implements Runnable {

    public static void main(String[] args) {

        Thread t1 = new Thread(new Example2());
        t1.start();

    }

    @Override
    public void run() {
        System.out.println("Hello World");
    }

}

    当然,还可以使用Lambda表达式:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example3 {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            System.out.println("Hello World");
        });
        t1.start();
    }

}

2.2 终止线程

    一般来说,线程在执行完毕后会结束,无须手工关闭。但是,一些服务端后台线程可能会常驻系统,他们通常不会正常终结。比如,它们的执行体本身就是一个大大的无穷循环。

    如果查阅JDK,不难发现一个stop()方法,可以立即将线程终止,非常方便。

    但如果使用IDE写代码的话,就会立即发现stop()方法是一个被标注为废弃的方法。也就是说,在将来,JDK可能会移除该方法。

    为什么stop()会被废弃呢?是因为它过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example4 {

    public static User u = new User();

    // 定义一个User对象
    public static class User {
        private int id;
        private String name;

        public User() {
            this.id = 0;
            this.name = "0";
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }

    }

    // 改变User的线程
    public static class ChangeObjectThread extends Thread {
        @Override
        public void run() {
            // 进入一个循环体,一直修改User对象
            while (true) {
                // 每次修改前进入同步块,获取当前时间毫秒数除以1000,得到的值作为Id和Name的值
                synchronized (u) {
                    int v = (int) (System.currentTimeMillis() / 1000);
                    u.setId(v);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    u.setName(String.valueOf(v));
                }
                // 谦让线程
                Thread.yield();
            }
        }
    }

    // 读取User的线程
    public static class ReadObjectThread extends Thread {
        @Override
        public void run() {
            // 进入一个循环体读取User对象
            // 如果User对象的Id与Name属性不同时,进行输出
            while (true) {
                synchronized (u) {
                    if (u.getId() != Integer.parseInt(u.getName())) {
                        System.out.println(u.toString());
                    }
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 开始读取线程;
        new ReadObjectThread().start();
        
        // 进入一个循环一直创建修改线程
        // 开启修改线程后,将线程睡眠150毫秒
        // 接着调用stop()方法终止线程
        while (true) {
            Thread t1 = new ChangeObjectThread();
            t1.start();
            Thread.sleep(150);
            t1.stop();
        }
    }

}

    结果输出:

User [id=1485712013, name=1485712012]
User [id=1485712013, name=1485712012]

    可以看到出现了id与name的值不一致的情况,问题在于,Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。这些锁恰恰是确保线程一致性的。如果此时,修改线程正写入数据到一半,并强行终止,那么对象就会被写坏,同时,由于锁已经释放了,另外一个等待该锁的读取线程就顺理成章的读到了这个对象,悲剧也就此发生了。

2.3 线程中断

    在Java中,线程中断是一种重要的线程协作机制。从表面上理解,中断就是让目标线程停止执行的意思,事实上并非完全如此。

    严格的讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出了。至于目标线程接到通知后如何处理,则完全由目标线程自行决定。

    有三个方法与线程中断有关:

// 中断线程
public void Thread.interrupt();

// 判断是否被中断
public boolean Thread.isInterrupted();

// 判断是否被中断,并清除当前中断状态
public static boolean Thread.interrupted();

    看一段代码,调用Thread.interrupt()后,线程是否还会继续执行:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example5 {

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    Thread.yield();
                }
            };
        };
        t1.start();
        Thread.sleep(2000);
        // 这里对t1进行了中断,但是在t1中并没有中断处理的逻辑。
        // 因此,即使t1线程被置上了中断状态,这个中断也不会发生任何作用。
        t1.interrupt();
    }

}

    如果希望t1在中断后退出,就必须为它添加相应的中断处理代码:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example5 {

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    // 如果线程状态为中断,就退出循环
                    if(Thread.currentThread().isInterrupted()) {
                        System.out.println("Interruted");
                        break;
                    }
                    Thread.yield();
                }
            };
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

}

    注意,Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。

2.4 等待和通知

    为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中,而是输出Object类。这也意味着任何对象都可以调用这两个方法。

public final void wait() throws InterruptedException
public final native coid notify()

    比如,在线程A中调用了obj.wait()方法,那么线程A就会停止继续执行而转为等待状态。线程A会一直等到其他线程调用obj.notify()方法为止。这时,obj对象就成为了多个线程之间有效通信手段。

    如果一个线程调用了obj.wait(),那么它会进入obj对象的等待队列。在这个队列中可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当obj.notify()被调用时,它就会从这个等待队列中随机唤醒一个。这个选择完全是随机的。

    处理notify(),还有一个notifyAll()方法,它和notify()的功能基本一致,不同的是,它会唤醒等待队列中所有等待的线程。

    注意,wait()方法必须包含在对应的synchronzied语句中,无论是wait()或是notify()都需要获得目标对象的一个监视器。

    来看一个例子:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example6 {
    
    final static Object object = new Object();
    
    public static class T1 extends Thread {
        @Override
        public void run() {
            synchronized(object) {
                System.out.println(System.currentTimeMillis() + " -> T1 开始!");
                try {
                    System.out.println(System.currentTimeMillis() + " -> T1 等待!");
                    object.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + " -> T1 结束!");
            }
        }
    }
    
    public static class T2 extends Thread {
        @Override
        public void run() {
            synchronized(object) {
                System.out.println(System.currentTimeMillis() + " -> T2 开始! 通知一个线程!");
                object.notify();
                System.out.println(System.currentTimeMillis() + " -> T2 结束!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        t1.start();
        t2.start();
    }
    
}

    上面的代码执行后,输出的结果是:

1485851829061 -> T1 开始!
1485851829061 -> T1 等待!
1485851829061 -> T2 开始! 通知一个线程!
1485851829061 -> T2 结束!
1485851831062 -> T1 结束!

    通过时间戳可以看到,T2通知T1继续执行后,T1并不能立即执行,二十要等待T2释放object的锁,并重新获得锁后才能继续执行。

    obj.wait()和Thread.sleep()方法都可以让线程等待若干时间,除了wait()可以被唤醒外,另一个主要区别就是wait()会释放目标的锁,而Thread.sleep()不会释放任何资源。

2.5 挂起和继续执行

    JDK的API文档中,可能还会发现两个看起来非常有用的接口,即挂起(suspend)和继续执行(resume)。这两个操作是一对相反的操作,被挂起的线程必须等到resume()操作后,才能继续执行。这两个方法已经被标注为废弃方法。

    不推荐使用suspend()去挂起线程的原因,是因为suspend在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续执行。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。

    但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是,它所占用的锁不会释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然是Runnable,这也会严重影响对系统当前状态的判断。

    下面通过一段代码来说明问题:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example7 {

    private static Object obj = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");      
    
    public static class ChangeObjectThread extends Thread {
        
        public ChangeObjectThread(String name) {
            super.setName(name);
        }
        
        @Override
        public void run() {
             synchronized(obj) {
                 System.out.println("in " + super.getName());
                 Thread.currentThread().suspend();
             }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }

}

    上面的代码中,t2是被挂起的。但是它的状态是RUNNABLE,这很有可能使我们误判当前系统的状态。同时,虽然主函数中已经调用了resume(),但是由于时间先后顺序的缘故,那么resume()并没有生效!这就导致了线程t2被永远挂起,并永远占用了对象obj的锁。

    如果需要一个比较可靠的挂起函数,可以结合wait()和notify()方法:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example8 {

    private static Object obj = new Object();

    public static class ChangeObjectThread extends Thread {
        volatile boolean suspendme = false;

        public void suspendMe() {
            suspendme = true;
        }

        public void resumeMe() {
            suspendme = false;
            synchronized (this) {
                notify();
            }
        }

        @Override
        public void run() {
            while (true) {
                synchronized (this) {
                    while (suspendme) {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }

                synchronized (obj) {
                    //System.out.println("in ChangeObjectThread");
                }
                Thread.yield();
            }
        }
    }

    public static class ReadObjectThread extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (obj) {
                    //System.out.println("in ReadObjectThread");
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ChangeObjectThread t1 = new ChangeObjectThread();
        ReadObjectThread t2 = new ReadObjectThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.suspendMe();
        System.out.println("suspend t1 2 sec");
        Thread.sleep(2000);
        System.out.println("resume t1");
        t1.resumeMe();
    }

}

    程序会检查自己是否被挂起,如果是,则执行wait()方法,否则,则进行正常的处理。

2.6 等待结束和谦让

    很多时候,一个线程的输入可能非常依赖其他线程的输出,此时,需要join()操作来实现这个功能:

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

    第一个方法表示无线等待,一直阻塞当前线程,直到目标线程执行完毕;

    第二个方法给出了一个最大等待时间,如果超时,当前线程会继续往下执行;

    提供一个简单的例子:

package cn.net.bysoft.java.concurrency.design.ch02;

public class Example9 {

    public volatile static int i = 0;
    
    public static class AddThread extends Thread {
        @Override
        public void run() {
            for(i=0;i<10000000;i++);
        };
    }
    
    public static void main(String[] args) throws InterruptedException {
        AddThread at = new AddThread();
        at.start();
        at.join();
        System.out.println(i);
    }

}

    如果不加at.join()这个方法,可能输出的是0或小于100000000的数字。

    因为AddThread还没有开始执行,或者没执行完,主线程就读取i并输出了。

    另外一个方法是Thread.yeild(),它的定义如下:

public static native void yield();

    这是一个静态方法,一旦执行,它会使当前线程让出CPU。但是注意,让出CPU并不表示当前线程不执行了。当前线程让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了。

    如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多工作的机会。

© 著作权归作者所有

XuePeng77
粉丝 48
博文 146
码字总数 194285
作品 0
丰台
私信 提问
OSC 第 101 期高手问答 —— Java 高并发程序设计

OSCHINA 本期高手问答(12月02日- 12月08日)我们请来了《实战Java高并发程序设计》的作者 @葛一鸣 为大家解答关于 Java 的并行程序设计基础、思路、方法和实战 方面的问题。如: 现在的服务...

叶秀兰
2015/12/01
13K
84
Android--面试中遇到的问题总结(三)

《Android 开发工程师面试指南 LearningNotes 》,作者是陶程,由梁观全贡献部分。大家可以去知乎关注这两位用心的少年。这份指南包含了大部分Android开发的基础、进阶知识,不仅可以帮助准备...

sealin
2017/02/22
0
0
读书笔记之《Java并发编程的艺术》-线程池和Executor的子孙们

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
755
1
读书笔记之《Java并发编程的艺术》-并发编程基础

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
4K
8
读书笔记之《Java并发编程的艺术》-并发编程容器和框架(重要)

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
723
1

没有更多内容

加载失败,请刷新页面

加载更多

代理模式之JDK动态代理 — “JDK Dynamic Proxy“

动态代理的原理是什么? 所谓的动态代理,他是一个代理机制,代理机制可以看作是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成,通过代理可以有效的让调...

code-ortaerc
今天
5
0
学习记录(day05-标签操作、属性绑定、语句控制、数据绑定、事件绑定、案例用户登录)

[TOC] 1.1.1标签操作v-text&v-html v-text:会把data中绑定的数据值原样输出。 v-html:会把data中值输出,且会自动解析html代码 <!--可以将指定的内容显示到标签体中--><标签 v-text=""></......

庭前云落
今天
8
0
VMware vSphere的两种RDM磁盘

在VMware vSphere vCenter中创建虚拟机时,可以添加一种叫RDM的磁盘。 RDM - Raw Device Mapping,原始设备映射,那么,RDM磁盘是不是就可以称作为“原始设备映射磁盘”呢?这也是一种可以热...

大别阿郎
今天
12
0
【AngularJS学习笔记】02 小杂烩及学习总结

本文转载于:专业的前端网站☞【AngularJS学习笔记】02 小杂烩及学习总结 表格示例 <div ng-app="myApp" ng-controller="customersCtrl"> <table> <tr ng-repeat="x in names | orderBy ......

前端老手
昨天
16
0
Linux 内核的五大创新

在科技行业,创新这个词几乎和革命一样到处泛滥,所以很难将那些夸张的东西与真正令人振奋的东西区分开来。Linux内核被称为创新,但它又被称为现代计算中最大的奇迹,一个微观世界中的庞然大...

阮鹏
昨天
20
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部