java线程基础

前言- CPU竞争策略

操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。

  1. 在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。如果在 时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程 序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

  2. 抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。因此可以看出,在抢 占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。

在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一 个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。

1-线程的优先级

在java线程中,可以在构造线程时通过setPriority()方法设定线程的优先级,优先级为从1-10的整数(默认为5),优先级越高系统分配的时间就越多;这里有一个设置优先级的一个常用经验知识:对于频繁阻塞的线程(经常休眠,IO操作等)需要设置较高的优先级,因为这些经常阻塞的线程即使设置为较高的优先级,但是在大部分时间里,处于阻塞状态,会让出CPU;而对于偏重计算的(将会长时间独占CPU)线程设置为较低的优先级,防止其他线程的不会长时间得不到执行。

Thread thread = new Thread(job);
thread.setPriority(10);
thread.start();

注意:但是在很多系统下面对线程优先级的设置可能无效(如类unix的分时系统)

2-线程的状态

给定一个时刻,线程只能处于6种状态其中的一种状态

  1. NEW:初始状态,线程被构建,但是还没有调用start()方法。
  2. RUNNABLE:运行状态,java线程将操作系统中的就绪状态和运行两种状态笼统的称作运行中。
  3. BLOCKED:阻塞状态,特指线程阻塞于锁synchronized关键字修饰的方法或者方法块),并将该线程加入同步队列中
  4. WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(比如通知或者中断),并将该线程加入等待队列中。需要注意的是调用LockSupport.park()方法和Thread.join()会使得线程进入这个状态,而不是阻塞状态。
  5. TIME_WAITING:超时等待状态,该状态是WAITING状态的超时版本,它可以在指定的时间自行返回。一般由带有超时设置的方法调用引起。
  6. TERMINATED:终止状态,表示当前线程已经执行完毕。
线程状态转移图,源自《java并发编程的艺术》

注意上图 Object.join()有误,应改成Thread.join()

线程start()和run()方法的区别
  • thread.start()方法

调用此方法将会由操作系统任务调度器在新创建的线程中执行run()方法,可能不会立刻执行,由任务调度器调度,但是一定是在新创建的线程中执行。重复调用start方法将抛出异常IllegalThreadStateException

  • thread.run()方法

Thread实现了Runnable接口,默认实现是调用target的run方法,调用此方法并不会再新创建的线程去执行run方法,只会在调用Thread.run()方法的线程本地执行,和调用一个普通对象的一个方法效果一样,可以被重复调用。

线程方法
  • Thread.sleep(long n)静态方法
  1. 当n = 0 时,thread 线程主动放弃自己CPU控制权,进入就绪状态。这种情况下只能调度优先级相等或者更高的线程,低优先级的线程很有能永远得不到执行,当没有符合条件的线程时,当前会一直占用CPU,造成CPU满载。
  1. 当n > 0 时,Thread线程将会被强制放弃CPU控制权,并睡眠n毫秒,进入阻塞状态。这种情况下所有其他任意优先级就绪的线程都有机会竞争CPU控制权。无论有没有符合的线程,都会放弃CPU控制权-,因此CPU占用率较低。
  2. 上述1、2是从线程调度的角度分析的,无论1、2,都不会释放对象的锁,也就是说如果有synchronized方法块,其他线程仍然不能访问共享数据,该方法抛出中断异常。
  • thread.join()

使得调用thread.join()语句的线程等待thread线程的执行完毕,才从这个语句返回,并继续这个线程,该方法也需要捕获中断异常。这个方法含有超时重载版本

  • Thread.yield()静态方法

将thread线程放入就绪队列中,而不是同步队列,由操作系统去调度。如果没有找到其他就绪的线程,则当前线程继续运行,比thread.sleep(0)速度快,只能让相同优先级的线程得以运行。

重点分析join方法的实现


如何实现join方法的语义?

  1. ** 方法内部调用Object.wait()方法进行等待。**
  2. 当线程终止时,会调用线程自身的notifyAll()方法,通知所有等待在该线程对象监视器上的线程。

属于经典的等待/通知模式

例子




解析:假设有两个线程A、B,在B中调用方法A.join,由于join是同步方法,线程B排他获取方法所属的对象监视器锁,即线程对象A的监视器锁;线程B获取线程A的对象监视器锁成功后,在join方法内部,调用的是this.wait()方法,即在线程B在线程A对象上等待并释放线程A上的对象监视器锁。

方法内部有两个循环判断:

  1. join(0):Object.wait(0),在第一个while循环里始终对线程A是否终止进行判断,如果还在运行,则使线程B等待,直到被通知或者中断,当被唤醒时还得去判断线程A是否终止,如果终止则在获取监视器锁后从join方法返回继续代码,否则继续等待。
  2. join(millis > 0) : Object.wait(millis)分析方法和上面基本一样,只不过加了超时返回,即从wait方法返回时判断是否超时,如果超时则在获取对象锁后跳出循环,从join方法返回继续执行。
对象方法

object.wait(),object.notify(),object.notifyAll()

  1. 这3个方法在使用之前都要获取object对象的锁,即在synchronized(object){ object.wait();}
  2. 调用wait()方法后,线程状态将由running变为waiting,并将当前线程放置到等待队列中,并释放object上的锁。
  3. notify() 和notifyAll()方法调用后,等待线程依旧不会从wait方法返回,而是将等待队列的一个或全部的线程移动到同步队列中,被移动的线程状态变为blocked,然后通知线程从同步方法块返回,并释放object上锁,只有下一次锁的竞争中,等待线程成功获取到object上的锁,才从wait方法返回。

3-线程的创建

提供了三个方法来创建Thread

  • 继承Thread类来创建线程类,重写run()方法作为线程执行体。

缺点:
线程类继承了Thread类,无法在继承其他父类。
因为每条线程都是一个Thread子类的实例,因此多个线程之间共享数据比较麻烦。

  • 用实现了Runnable接口的对象作为target来创建线程对象。

推荐,用来将没有返回值和不会抛出异常的方法体run()传递给线程去执行

  • 用实现了Callable接口的对象作为task提交给线程池ExecutorService 通过submit方法来提交执行

推荐,用来将有返回值和会抛出异常的方法体run()传递给线程去执行

4-线程中断

中断是一种线程之间通信机制,但是中断不像其名字一样会让线程中断,而是线程通过循环判断查看中断标志位,来及时的查看中断状态并采取下一步的操作

  1. 其他线程通过该线程的interrupt()方法对其进行中断操作。
  2. 线程通过调用自身的isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()将清除当前线程的中断标志并返回之前线程的中断状态;如果该线程已经处于终结状态,无论是否中断,则调用该对象的isInterrupted()都将返回false
  3. 抛出InterruptedException异常的方法,比如Thread.sleep(),这些方法在抛出异常之前Java虚拟机会先将该线程的中断标志位清除,然后再抛出InterruptedException异常,这时在调用isInterrupted()方法进行判断将返回false

5-等待/通知的经典线程间通信范式

  • 等待方遵循如下原则
  1. 获取对象的锁。
  1. 如果条件不满足,那么调用对象的wait()方法,被通知后还要检查条件。
  2. 条件满足则执行对应的逻辑 。
synchronized(object对象){
        while(条件不满足){
              object.wait();
        }
        对应的处理逻辑;
}
  • 通知方遵循如下原则
  1. 获取对象的锁。
  1. 改变条件
  2. 通知所有等待在对象上的线程
synchronized(object对象){
        改变条件
        object.notifyAll();
}
6-ThreadLocal

线程本地变量,是一个以TreadLocal变量为键,任意对象为值的存储结构,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本,ThreadLocal将变量的范围限制在一个线程的上下文当中,使得变量的作用域为线程级别。

  1. ThreadLocal仅仅是个变量访问的入口;
  2. 每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有所有已经初始化的ThreadLocal值对象的引用;
  3. 只有在线程中调用ThreadLocal的set(),或者get()方法时都会在当前线程中绑定这个变量,否则不会绑定。第一次get()方法调用将会进行初始化(如果set方法没有调用过),而且初始化每个线程值进行一次。
  4. 初始化方法

允许对默认初始化方法进行重写

// 默认初始化方法
protected T initialValue(){
        return null;
}

ThreadLocal源码分析

  1. set()
// ThreadLocal.java
public void set(T value) {
   //1.首先获取当前线程对象
   Thread t = Thread.currentThread();
   //2.获取该线程对象的ThreadLocalMap
       ThreadLocalMap map = getMap(t);
       //如果map不为空,执行set操作,以当前threadLocal对象为key
       //实际存储对象为value进行set操作
       if (map != null)
           map.set(this, value);
       //如果map为空,则为该线程创建ThreadLocalMap
       else
           createMap(t, value);
   }
ThreadLocalMap getMap(Thread t) {
   //线程对象持有ThreadLocalMap的引用
   return t.threadLocals;
}
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
  1. get()
public T get() {
    //1.首先获取当前线程
    Thread t = Thread.currentThread();
    //2.获取线程的map对象
    ThreadLocalMap map = getMap(t);
    //3.如果map不为空,以threadlocal实例为key获取到对应Entry,然后从Entry中取出对象即可。
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    //如果map为空,也就是第一次没有调用set直接get
    //(或者调用过set,又调用了remove)时,为其设定初始值
    return setInitialValue();
 }
private T setInitialValue() {
    T value = initialValue();//获取初始值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

场景一:为每一个线程分配一个递增无重复的ID

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalDemo {
    public static void main(String []args){
        for(int i=0;i<5;i++){
            final Thread t = new Thread(){
                @Override
                public void run(){
                    System.out.println("当前线程:"+Thread.currentThread().getName()
                        +",已分配ID:"+ThreadId.get());
                }
            };
            t.start();
        }
    }
    static class ThreadId{
        //一个递增的序列,使用AtomicInger原子变量保证线程安全
        private static final AtomicInteger nextId = new AtomicInteger(0);
        //线程本地变量,为每个线程关联一个唯一的序号
        private static final ThreadLocal<Integer> threadId =
                new ThreadLocal<Integer>() {
                    @Override
                    protected Integer initialValue() {
                        //相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,
                        //会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
                        return nextId.getAndIncrement();
                    }
                };

       //返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
        public static int get() {
            return threadId.get();
        }
    }
}

说明:ThreadID是线程共享的,所以需要原子类来保证线程访问的安全性,而ThreadID的成员变量threadId是线程封闭的,只是线程本地变量初始化时需要访问原子类(多个线程同时访问引起 )

场景二:web开发中,为每一个连接创建一个ThreadLocal保存session信息,如果web服务器使用线程池技术(比如Tomcat)进行线程复用,则每一次连接都要重新的set,以保证session为本次连接的信息。当session结束,调用remove方法,将线程本地变量从线程的ThreadLocalMap中移除。

7-等待超时

主要学习的是剩余时间的计算

等待超时模式的经典模式

// 同步方法
public synchronized Object get(long millss) throws InterruptedException {
    // 获取将来时间点
    long future = System.currentTimeMillis)() + millis;
    // 初始化剩余时间为millis,从这可以看出超时等待时间并不是十分的严格
    long remaining = millis;

    // 超时等待判断,当返回值的结果不满足并且剩余时间小于0时,从循环退出
    while((result == null) && remaining > 0){
        wait(remaining);
        remaining = future - System.currentTimeMillis();
    }
    return result;
}

参考链接
https://hacpai.com/article/1488015279637

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

推荐阅读更多精彩内容

  • 线程的状态 新建状态:用new语句创建的线程对象处于新建状态,此时它和其它的java对象一样,仅仅在堆中被分配了内...
    稻田上的稻草人阅读 556评论 0 1
  • java线程基础 线程和进程进程 : 进程是系统进行资源分配和调度的一个独立单位。进程由程序、数据和进程控制块三部...
    你好667阅读 204评论 0 0
  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,709评论 12 45
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,948评论 1 18