Java并发编程学习(一)——线程

一.线程概念

在java中,并发编程是一个相当重要的话题,通过使用线程我们可以发挥多处理器的强大计算能力,可以构建多任务的应用从而提升体验,而并发编程的基础就是线程。

jdk从1.0时代就引入了线程相关的类(Thread,Runnable等),在jdk1.5中又引入了java.util.concurrent包,提供了更多便捷的并发操作。

今天我们就来走近线程这位java家族的老朋友,了解他的日常。

提到线程,就不能不提进程,在操作系统运行过程中,进程和线程都是为了便于进行多任务处理而存在的,其中进程有独立的内存空间以及相关资源(如文件句柄等),而线程是在进程内部更小的工作单元,同一个进程中的线程共享资源,有一个类比可以用来解释进程和线程:

我们的计算机就像是一座工厂,时刻都在运行,而工厂中又有很多车间。

由于工厂的电力有限,一段时间内,只能供应一个车间运行,当一个车间运行的时候,其他车间就必须停工。

进程就好比是工厂里的车间。

一个车间里,有很多工人,车间里的空间和资源(比如空调、洗手间等)是车间里的工人共享的。

每个工人就相当于进程中的线程。

简单总结一下:进程是CPU资源分配的最小单元,线程是CPU调度的最小单元。

二.线程生命周期

我们今天的主角是线程,下面来看一下线程的生命周期。
如果查看Thread类的源码,我们就会发现,Thread有如下几种状态:

public enum State {
       
        NEW, // 新创建
        RUNNABLE, // 可以被调度
        BLOCKED, // 被阻塞
        WAITING, // 等待
        TIMED_WAITING, // 限时等待
        TERMINATED; // 结束
    }

各个状态间的流转关系如下图所示:

线程生命周期

下面我们通过一段代码来感受一下线程的状态流转:

public class ThreadLifeCycle {
    // 定义一个线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " start to work:" + Thread.currentThread().getState());
            // 线程执行过程中需要调用一个同步的方法
            work();
        }
    }

    // 同步方法,将造成线程阻塞
    public static synchronized void work() {
        try {
            // 线程进入后等待2秒钟
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        MyThread job1 = new MyThread();
        MyThread job2 = new MyThread();
        Thread thread1 = new Thread(job1, "thread1");
        Thread thread2 = new Thread(job1, "thread2");
        System.out.println("thread1 befor start:" + thread1.getState());
        System.out.println("thread2 befor start:" + thread2.getState());
        thread1.start();
        thread2.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread1 after wait 100ms:" + thread1.getState());
        System.out.println("thread2 after wait 100ms:" + thread2.getState());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread1 after 5000ms:" + thread1.getState());
        System.out.println("thread2 after 5000ms:" + thread2.getState());
    }
}

运行结果如下:

thread1 befor start:NEW
thread2 befor start:NEW
thread1 start to work:RUNNABLE
thread2 start to work:RUNNABLE
thread1 after wait 100ms:TIMED_WAITING
thread2 after wait 100ms:BLOCKED
thread1 after 5000ms:TERMINATED
thread2 after 5000ms:TERMINATED

可以看到:

  • 在线程刚刚创建,调用start方法以前,两个线程的状态均为NEW;
  • 在调用线程的start方法后,线程的状态变为RUNNABLE
  • 在主线程等待100ms后,线程1处于TIMED_WAITING状态,这是由于线程1先开始执行,并获得了进入work方法的锁;而线程2由于无法获得锁,只能处于BLOCKED状态
  • 5秒钟后,两个线程都已经执行完成了,处于TERMINATED状态

三.线程的调度

由于java是平台无关的,而线程调度这种事情都是操作系统级别的,因此具体的线程调度策略取决于具体的操作系统。

当前,主流的操作系统均使用抢占式的,基于线程优先级的调度策略。这里有两个关键词:抢占式优先级

抢占式:所有的线程争夺CPU使用权,CPU按照一定算法来给所有线程分配时间片,一个线程执行完自己的时间片后需要让出CPU执行权。
优先级:所有的线程均有优先级,优先级高的线程将优先被执行。

我们在创建线程时,可以通过setPriority方法给线程设置优先级。设置完优先级后,jvm在执行时,将把这个优先级设置为操作系统的线程优先级,供cpu调度。

Thread类中定义了有关优先级的常量:

// 最低优先级
public final static int MIN_PRIORITY = 1;

// 默认优先级
public final static int NORM_PRIORITY = 5;

// 最高优先级
public final static int MAX_PRIORITY = 10;

下面我们通过程序来感受一下线程的优先级:
本人的电脑是4核,win7系统。为了能使线程出现等待和调度,我们下面将启动8个工作线程。

public class ThreadPriority {

    static class MyThread extends Thread {
        private volatile boolean running = true;

        private volatile Random random = new Random();

        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            int count = 0;
            // 不停的执行正弦预算,直到接到结束指令
            while (running) {
                Math.sin(random.nextDouble());
                count++;
            }
            System.out.println(threadName + " run " + count + " times");
        }

        public void shutDown() {
            running = false;
        }
    }

    public static void main(String[] args) {
        // 给主线程设置为最高优先级,以使其他线程启动后,主线程能够执行后续操所
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        List<MyThread> list = new ArrayList<MyThread>();
        for (int i = 0; i < 8; i++) {
            MyThread thread = new MyThread( "thread" + i);
            thread.setPriority(5);
            list.add(thread);
        }
        for (int i = 0; i < 8; i++) {
            list.get(i).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 8; i++) {
            list.get(i).shutDown();
        }

    }
}

第一次,我们将所有线程的优先级都设置为5,执行结果如下:

thread3 run 63641301 times
thread5 run 73277080 times
thread4 run 65114449 times
thread2 run 65464405 times
thread0 run 66025262 times
thread7 run 63567566 times
thread6 run 97058244 times
thread1 run 72544080 times

然后,我们修改一下线程的优先级:

……
for (int i = 0; i < 8; i++) {
    MyThread thread = new MyThread( "thread" + i);
    // 不同的线程优先级不同
    thread.setPriority(i + 1);
    list.add(thread);
}
……

执行结果如下:

thread6 run 154709310 times
thread7 run 145893865 times
thread4 run 147268692 times
thread5 run 108303568 times
thread2 run 19743170 times
thread0 run 16955990 times
thread3 run 15611271 times
thread1 run 0 times

可以发现,4,5,6,7四个线程执行的次数比0,1,2,3要多一个数量级,甚至线程1没有被分到时间片。另外也可以看出,cpu并不是完全严格的按照优先级来进行调度,指定优先级只能给cpu一个参考,低优先级的未必得不到执行,但是大体还是按照高优先级先被执行的原则的。

四.线程间的协作

在了解了线程的调度机制后,我们知道,一个线程在什么时候被调度是不确定的,即便是优先级较高的线程,也只是原则上比优先级低的线程优先执行而已,优先多少也是不确定的。

但是有时,我们需要针对不同的线程设定一些执行的规则,例如一个线程执行完成后再执行另一个线程等等,因此就需要建立线程间的协作机制。

1.wait、notify、notifyAll

这三个方法是Object类中的基础方法,不知道大家有没有疑问,反正我看到这的时候有一个疑惑:这三个方法都是用于线程间协作的,为什么不定义在Thread类中,而要定义在Object类中呢?

让我们先来了解一下这三个方法的使用,稍后再来回答这个问题。

直接上代码:

public class ThreadCooperation {

    public synchronized void testWait() {
        System.out.println(Thread.currentThread().getName() +" start");
        try {
            // 使进入该方法的线程处于等待状态
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +" end");
    }

    static class MyThread implements Runnable {
        private ThreadCooperation cooperation;

        public MyThread(ThreadCooperation cooperation) {
            this.cooperation = cooperation;
        }
        @Override
        public void run() {
            cooperation.testWait();
        }
    }

    public static void main(String[] args) {
        ThreadCooperation cooperation = new ThreadCooperation();
        for (int i = 0; i < 3; i++) {
            new Thread(new MyThread(cooperation), "thread" + i).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after 1000ms,notify");
        synchronized (cooperation) {
            cooperation.notify();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after another 1000ms,notify all");
        synchronized (cooperation) {
            cooperation.notifyAll();
        }
    }
}

在上面代码中,我们创建了3个线程,每个线程执行时会进入等待状态,然后等待主线程唤醒他们。

执行结果如下:

thread0 start
thread2 start
thread1 start
after 1000ms,notify
thread0 end
after another 1000ms,notify all
thread1 end
thread2 end

在调用notify方法时,thread0被唤醒,在调用notifyAll方法时,剩余的两个线程被唤醒。除此之外,我们也看到,当一个线程处于wait时,该线程会释放锁,以便其他线程获取,否则,再输出"thread0 start"后,就不会输出"thread2 start"和"thread1 start"了。

下面我们来回答本节提出的问题:为什么wait、notify、notifyAll方法被定义在Object类中,而不是Thread类?

因为等待,是指线程在某一个资源(对象)上等待,如我们上面的程序,线程可以在任何对象上等待;唤醒也是类似,线程在对象上等待,自然应该在同一个对象上被唤醒。因此这三个方法被定义在Object类。

2. sleep、yield、join

这三个方法都是被定义在Thread类中的。显然,他们都是指线程的某种行为。

(1)sleep方法是让当前线程休眠一段时间,即让出cpu让其他线程执行,但是它并不会释放锁。通过代码来看一下:

public class ThreadCooperation {

    public synchronized void testSleep() {

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    static class MyThread implements Runnable {
        private ThreadCooperation cooperation;

        public MyThread(ThreadCooperation cooperation) {
            this.cooperation = cooperation;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() +" start");
            cooperation.testSleep();
            System.out.println(Thread.currentThread().getName() +" end");
        }
    }

    public static void main(String[] args) {
        ThreadCooperation cooperation = new ThreadCooperation();
        for (int i = 0; i < 3; i++) {
            new Thread(new MyThread(cooperation), "thread" + i).start();
        }
    }
}

上面代码中,我们仍然创建3个线程,每个线程中都需要调用testSleep方法,一旦某个线程进入testSleep方法后,就会进入休眠状态,其他线程可以执行,但是由于第一个线程正在休眠,而且没有释放锁,所以其他线程只能等待。

执行结果如下:

thread1 start
thread0 start
thread2 start
thread1 end
thread2 end
thread0 end

可以看到,程序会先输出:

thread1 start
thread0 start
thread2 start

然后每隔一秒再输出后面的结果。说明在一个线程休眠时,其他线程得到了执行,但是被阻塞在了testSleep方法上。

(2)yield方法使当前线程变为等待执行状态,让出CPU以便其他线程执行,但是不能保证当前线程被立刻暂停,也不能保证暂定多久,甚至如果该线程的优先级高,可能刚刚被暂停,又重新获得执行。所以yield方法的行为是不甚明确的,不可靠的。

同样来看一段代码:

public class ThreadCooperation {

    public synchronized void testYield() {
        System.out.println(Thread.currentThread().getName() +" testYield start");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() +" testYield end");
    }

    static class MyThread implements Runnable {
        private ThreadCooperation cooperation;

        public MyThread(ThreadCooperation cooperation) {
            this.cooperation = cooperation;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() +" start");
            cooperation.testYield();
            System.out.println(Thread.currentThread().getName() +" end");
        }
    }

    public static void main(String[] args) {
        ThreadCooperation cooperation = new ThreadCooperation();
        for (int i = 0; i < 3; i++) {
            new Thread(new MyThread(cooperation), "thread" + i).start();
        }
    }
}

某一次执行的结果如下:

thread0 start
thread1 start
thread0 testYield start
thread2 start
thread0 testYield end
thread0 end
thread2 testYield start
thread2 testYield end
thread2 end
thread1 testYield start
thread1 testYield end
thread1 end

可以看到在输出“thread0 testYield start”之后,输出了“thread2 start”,说明thread0让出了cpu。但让出cpu并不意味着会让出锁,其他线程虽然可以执行,但是需要等待先进入testYield方法的线程执行完才能进入。

(3)join方法使父线程等待子线程执行完成后才能执行。jdk源码中对jon方法的注释是:“Waits for this thread to die”,也就是需要等待执行join方法的线程执行完。

看一下下面的代码:

public class ThreadCooperation {

    static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() +" start");
            System.out.println(Thread.currentThread().getName() +" end");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new MyThread(), "thread" + i).start();
        }
        System.out.println("main thread run");
    }
}

某一次执行结果如下:

main thread run
thread0 start
thread1 start
thread1 end
thread0 end
thread2 start
thread2 end

可以看到主线程先执行了。如果我们想让子线程先执行完,再继续执行主线程,就可以使用join方法了:

……
public static void main(String[] args) {
    List<Thread> threadList = new ArrayList<Thread>();
    for (int i = 0; i < 3; i++) {
        Thread thread = new Thread(new MyThread(), "thread" + i);
        threadList.add(thread);
        thread.start();
    }
    try {
        // 主线程需要等待thread1执行完
        threadList.get(1).join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("main thread run");
}

典型的执行结果如下:

thread0 start
thread2 start
thread2 end
thread1 start
thread1 end
main thread run
thread0 end

从中我们可以看出与不使用join方法的区别。

下面总结一下wait、notify、sleep、yield、join这几个方法:

  • wait、notify方法定义在Object类中,sleep、yield、join方法定义在Thread类中。
  • sleep、yield方法是静态方法,而join方法是实例方法。
  • wait、sleep、yield三个方法都可以时当前线程让出cpu,但其中wait方法必须在同步块中调用,而其他两个不用;wait方法针对某个对象,使线程在该对象上等待;而其他两个方法都是针对线程的;wait方法会让当前线程释放锁,而其他两个方法不会。wait方法和sleep方法都可以指定等待的时间,而yield不可以,调用yield后其行为是不可控的。
参考资料:

本我已迁移至我的博客:http://ipenge.com/37241.html

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

推荐阅读更多精彩内容