Java多线程知识点(一. 基础)

1 多线程介绍

多线程是现代编程领域不可绕开的话题,合理的运行多线程能有效降低系统的开发与维护成本,同时显著提升系统性能,这一点在复杂系统中表现得尤为明显;一般来说,多线程的好处包括:

  • 充分利用多核系统资源
  • 简化系统抽象模型;对于一个复杂系统来说,使用单一线程处理所有任务会导致系统及其复杂,而将多个任务进行合理分类并置于各自互不干扰的独立线程中进行处理,无疑是一种更合理的组织方式
  • 更方便的异步事件处理;如对于一个需应对多客户端的服务端来说,单线程处理所有客户端请求必然会导致效率底下,业务耦合,而多线程处理多客户端请求则能有效解决该问题
  • 提升系统性能;合理的组织,充分的利用系统资源,必然带来性能的提升

万事有利有弊,多线程的好处虽多,但想合理的使用好多线程,却并不容易,多线程编程主要需要注意以下几个问题:

  • 如何保证线程安全
  • 如何避免线程间同步导致的程序执行中断;主要有死锁活锁饥饿锁
  • 如何避免不合理的多线程应用导致的性能损耗;因为多线程本身也会耗用系统资源,如线程间调度(执行,暂停)导致的线程运行上下文切换,保存与恢复线程运行上下文,如果涉及到多线程之间的数据共享,那么不可避免的需要用到数据同步以保证线程安全,而数据同步也需要消耗系统资源

2 基础知识

2.1 线程安全

所谓线程安全指的是:一个数据(基本类型数据,对象类型数据)在不需要其他的同步或协调机制的情况下被多个线程同时使用且能够持续保证正确的特性;这里面最重要的就是正确这个词,所谓正确指的就是这个数据能永远如你的逻辑预期

2.2 锁

内在锁(intrinsic lock):所有的java对象皆可作为synchronized声明的锁对象,如:

synchronized (lock) {
// Access or modify shared state guarded by lock
}

or

public synchronized void doSomething() {
    //doSomething
}

这种所有java对象锁所共有的锁特性称为内在锁或监听锁,内在锁同一时间最多只能被一个对象锁持有

可重入锁(ReentrantLock):可重入指的是,每个线程可多次获得同一个锁;可重入的特性很重要,如果一段逻辑在执行的时候在获得一个锁后,其后续逻辑又执行了一次获取该锁的操作,此时若锁不是可重入的,那么就会造成死锁;内在锁也具备可重入性

2.3 共享数据

使用synchronized可以保证同一代码块同一时间只有一个线程进入,而共享数据讨论的是如何保证数据在被多个线程同时访问的情况下还能保持正确性

2.3.1 数据的可见性(Visibility)

可见性指的是同一个对象或数据被多个线程访问的时候,每个线程看到的都是这个对象的最新值,或者说一个线程对数据的变动能被其他线程感知;最简单粗暴的保证可见性的方式就是使用synchronized关键字;在讨论可见性时,有一种情况往往被忽视,即:64位数字(double或long)型变量,对于其他的变量,即便在多线程情况下,虽然读到的不是最新的值,但至少还是一个完整的值(即便不是最新的),但是对于64位数字,JVM对其的读写会分为两个32位操作,可能一个线程对其写操作只完成了一半(第一个32位操作),另外一个线程就执行完了读操作,那么得出的是一个完全错误的值

2.3.2 volatile

锁可以保证原子性与可见性,而volatile只能保证可见性,一般在同时满足下列三个条件的情况下方可使用volatile

  • 对变量的写不依赖变量当前的值;典型应用场景如自增操作a++
  • 该变量没有包含在具有其他变量的不变式中;这一条有点绕,通过代码示例讲解更易理解:
@NotThreadSafe 
public class NumberRange {
    private volatile int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

如果初始状态是(0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,但是两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3);所以这种情况下还是需要通过synchronized保证程序正确

  • 考虑应用场景,确实不需要对变量进行加锁

2.3.3 数据的Publication与Escape

Publication很容易理解,即将一个数据或对象以某种形式暴露到其当前域之外,其暴露形式包括:暴露引用,通过方法返回,作为方法参数等;而如果一个对象在不应该被Publication的时候被Publication了,则称为Escape

2.3.4 线程约束(thread confinement)

访问共享的可变的数据一般使用同步机制保证数据准确;如果数据只能被一个线程访问,那么就不需要同步机制,这种场景就叫线程约束;实现线程约束主要有两种形式,分别为stack Confinement和ThreadLocal,stack Confinement指的是通过代码块的调用栈将变量限制于单个线程内,如:

public int loadTheArk(Collection<Animal> candidates) {
  SortedSet<Animal> animals;
  int numPairs = 0;
  Animal candidate = null;
  // animals confined to method, don't let them escape!
  animals = new TreeSet<Animal>(new SpeciesGenderComparator());
  animals.addAll(candidates);
  for (Animal a : animals) {
    if (candidate == null || !candidate.isPotentialMate(a))
      candidate = a;
    else {
      ark.load(new AnimalPair(candidate, a));
      ++numPairs;
      candidate = null;
    }
  }
  return numPairs;
}

这段代码里的numPairs ,作为一个基本类型,也不会有引用通过方法返回暴露出去,它只会存在于当前堆栈中,那么它就是thread Confinement;而ThreadLocal属于更正规的thread confinement形式,每个线程都提供了一个单独的数据存储对象,而ThreadLocal机制在于为数据生成一份单独的基于该线程的copy并存于该存储对象内,这样该数据在该线程下的任何变动都不会影响到其他线程

2.3.5 对象的不可变性

如果对象同时具备下列三个条件,则其具有不变性

  • 对象被创建后,它的状态不可变 -- 这里的状态指代的是一些业务关键值
  • 它的所有属性都是final的
  • 它的应用在构建的时候未Escape掉

一个具有不可变性的对象一定是线程安全的,不可变性的一个变种称为形式上的不可变对象,即:如果一个对象不是完全的不可变,只是在publication后不可变,那么这个对象称为形式上的不可变,如:某个数据在生成后即不会有任何逻辑会对其进行更改

如何publication一个对象取决于对象的可变性如何:

  • 对于不可变的对象,可以放心publication
  • 对于效果上的不可变对象,需要保证publication安全,即不会发生Escape
  • 而对于可变对象,不但要保证publication安全,还要保证在使用的时候线程安全

2.4 多线程下对象的组装

设计一个线程安全的类主要需要考虑下列三点:

  • 确定哪些变量(属性)与类的状态相关
  • 确定这些变量有哪些可能的取值
  • 建立一套多线程机制访问这些变量

对象的线程安全经常需与对象所代表的业务一起考虑,如对象的状态变更是否遵循特定规则(如:必须在当前值的基础上+1)

一般情况下,可通过下列四种机制保证对象线程安全:

  • 单线程约束(Thread‐confined):所谓单线程约束意思是一个对象只属于一个线程,不能被其它线程访问,既然不存在多线程的访问,那么该对象也就不存在线程安全的问题
  • 只读式共享(Shared read‐only):如果一个对象可以被多个线程访问,但是却不能被多个线程修改,则代表该对象属于只读式共享,同时该对象也是线程安全的
  • 线程安全/内部同步 (Shared thread‐safe):即一个对象在其内部已经提供了同步机制已保证对该对象的访问个操作是线程安全的
  • Guarded: 所谓Guarded对象意思是对该对象的访问必须获得特定的锁,如被另外一个线程安全的对象封装的对象可称为Guarded对象;更进一步,被另外一个Guarded对象封装的对象也是Guarded对象(有点绕),既然该对象被线程安全的对象Guarded住,那么无疑该对象也是线程安全的

最简单的保证对象线程安全的方式就是instance Confinement,即通过synchronized将对对象状态的访问封装起来,另外常见的线程安全封装方式为管程以及代理模式(通过一个线程安全的类访问另外一个线程不安全的类,以使其线程安全)

2.5 构建模块

Collections.synchronizedXxx 包含了一系列线程安全的方法,它对外暴露的每一个public方法都有synchronized加持;但是很多时候,在使用容器的时候仅仅对单个方法进行synchronized不能满足实际需求,如:

//list.get与list.remove分别线程安全

public static Object getLast(Vector list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

这段代码中一个线程执行getLast并认为当前的lastIndex为10,而待其执行至list.get时,另一个线程执行remove将最后一个元素删除掉,即便getremove都是线程安全的,整体功能上还是会出错,虽然可以在业务层增加互斥控制解决该问题,但是去带来了另外一个性能的问题,Concurrent Collections可以有效解决该问题,还是那句话,有利必有弊,Concurrent Collections也有一些缺点,如ConcurrentHashMap就不能保证在多线程情况下sizeisEmpty等方法的返回值绝对准确,但是在多线程情况下,主要的操作都是get, put, containsKey 和 remove等,所以一般情况下都可放心大胆使用ConcurrentHashMap

线程的阻塞与打断:任何抛出InterruptedException的方法都是阻塞方法,面对InterruptedException,有两种处理方式:

  • 继续丢给外层堆栈进行处理
  • 捕捉并自行处理

有一点需要注意,如果当前代码处于一个单独的Runnable中,那么你应该自行处理该InterruptedException,因为此种情况直接抛出异常会导致异常悄无声息的消失掉,没有任何机制会去处理

2.6 同步器

同步器是一种基于各线程相关状态以协调其执行顺序的对象;在Java库中,有很多同步器类:

  • latch
    latch可以阻塞多个线程直到达到一个terminal state,它就像一扇门,在没有达到terminal state时,门是关着的,直到状态触发,门将会打开,且永远打开,不能再关上,即terminal state不会再改变(所以叫terminal state)
  • FutureTask
    FutureTask可以启动一个任务,等到执行完后返回结果,如:
public class Preloader {
    private final FutureTask<ProductInfo> future =
    new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
        public ProductInfo call() throws DataLoadException {
            return loadProductInfo();
        }
    });
    private final Thread thread = new Thread(future);
    public void start() { thread.start(); }
    public ProductInfo get() throws DataLoadException, InterruptedException {
        try {
            return future.get();
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof DataLoadException)
                throw (DataLoadException) cause;
            else
                throw launderThrowable(cause);
        }
    }
}
  • Semaphores
    通过Semaphores可控制同一时间内可以有几个线程同时访问特定资源
  • Barriers
    Barrierslatch类似,也是阻塞一系列线程直到某一事件发生,它们的主要不同点在于latch等待的是一个事件,而Barriers等待所有线程都到达一个Barriers point(barrier.await()),方会触发执行后续代码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容

  • 一:java概述:1,JDK:Java Development Kit,java的开发和运行环境,java的开发工...
    ZaneInTheSun阅读 2,655评论 0 11
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,255评论 11 349
  • 前言:虽然自己平时都在用多线程,也能完成基本的工作需求,但总觉得,还是对线程没有一个系统的概念,所以,查阅了一些资...
    justCode_阅读 708评论 0 9
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,645评论 18 399
  • 读经感恩日记第181篇 2017年6月8日(农历五月十三) 星期四天气 :晴 读经人: 冰昱源曦妈 读经第24周...
    做幸福快乐女人阅读 369评论 0 0