java并发(一):线程基础篇

简介: 线程的创建很简单,一般是集成Thread类或者实现Runnable接口,我就不细说了。然后,要牢记多线程的3大特性:多线程的三个特性:原子性、可见性、有序性原子性:是指一个操作是不可中断的。

线程的创建很简单,一般是集成Thread类或者实现Runnable接口,我就不细说了。然后,要牢记多线程的3大特性:
多线程的三个特性:原子性、可见性、有序性
原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值为1,线程B给他赋值为-1。那么不管这两个线程以何种方式。何种步调工作,i的值要么是1,要么是-1.线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。

可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行来说,可见性问题是不存在的。

有序性:在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。

而共享变量的写操作出错,最重要的是原子性,一般多线程的问题主要抓住这个。

线程安全问题

一般多线程编程都会遇到线程安全的问题,线程安全总体来说是因为多个线程竞争共享资源造成的。比如:

public class Test{

    private int num = 0;
    
    public void add(int value){
        this.num = this.num + value;
    }   
}

两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。如果两个线程同时执行这个对象的add()方法,会造成这种现象:线程A先读到num为0,此时恰好线程B也读到num为0,然后A,B同时执行加2和加3的操作,如果A先赋值num为2,然后B又赋值num为3,会造成最后结果为3;或者反过来,造成num为2,使得最后的结果无法预料。
如果线程并没有共享资源,那么多线程执行的代码是安全的,比如:
类方法中局部变量或者局部对象引用

public class Test{
    
    public void add(int value){
        int num = 0;
        String a = new String("aa");
        num = num + value;
    }   
}

还有一种安全的方法,就是每个线程都是执行同一个类不同对象的方法,虽然代码相同,但是不同的对象空间,也不会出现问题,如servlet。

线程状态

线程的状态实现通过 Thread.State 常量类实现,有 6 种线程状态:new(新建)、runnnable(可运行)、blocked(阻塞)、waiting(等待)、time waiting (定时等待)和 terminated(终止)。状态转换图如下:


img_5f0edd3b974bda9e9cedc82c069fccfe.png

线程状态流程大致如下:

  • 线程创建后,进入 new 状态
  • 调用 start 或者 run 方法,进入 runnable 状态
  • JVM 按照线程优先级及时间分片等执行 runnable 状态的线程。开始执行时,进入 running 状态
  • 如果线程执行 sleep、wait、join,或者进入 IO 阻塞等。进入 wait 或者 blocked 状态
  • 线程执行完毕后,线程被线程队列移除。最后为 terminated 状态。

ThreadLocal

ThreadLocal与线程同步无关,它虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。
它的API介绍如下:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。

ThreadLocal定义了四个方法:

  • get():返回此线程局部变量的当前线程副本中的值。
  • initialValue():返回此线程局部变量的当前线程的“初始值”。
  • remove():移除此线程局部变量当前线程的值。
  • set(T value):将此线程局部变量的当前线程副本中的值设定为指定值。
    除了这四个方法,ThreadLocal内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
    对于ThreadLocal需要注意的有两点:
  1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。
    下图是Thread、ThreadLocal、ThreadLocalMap的关系


    img_45838b67b8fe7e3d2b56cfe71a17bf93.png

ThreadLocal示例

package com.xushu.multi;

public class Test{
    
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        // 实现initialValue()
        @Override
        protected Integer initialValue() {
            return 0; //这里返回了一个0
        }
        
    };
    
    public int nextSeq(){
        count.set(count.get() + 1);
        
        return count.get();
    }
    
    private static class SeqThread implements Runnable{
        private Test te;

        SeqThread(Test te) {
            this.te = te;
        }
        
        @Override
        public void run() {
            for(int i = 0; i < 3; i++){
                System.out.println(Thread.currentThread().getName() + " seqCount :" + te.nextSeq());
            }
        }
    }
    
    public static void main(String[] args) {
        Test te = new Test();
        
        Thread t1 = new Thread(new SeqThread(te));
        Thread t2 = new Thread(new SeqThread(te));
        Thread t3 = new Thread(new SeqThread(te));
        Thread t4 = new Thread(new SeqThread(te));
        
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

可以看出,每个线程都有自己的一个变量副本,所以从根本上避免了读同一个变量。但是,如果在initialValue()方法中,如果return的是一个共有变量,那就是所有的线程都访问同一个变量了,所以ThreadLocal就失效了。这篇文章有解析。

ThreadLocal源码解析

ThreadLocal虽然解决了这个多线程变量的复杂问题,但是它的源码实现却是比较简单的。ThreadLocalMap是实现ThreadLocal的关键,我们先从它入手。

ThreadLocalMap

ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:

 
       static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

从上面代码中可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用(关于弱引用这里就不多说了,感兴趣的可以关注这篇博客Java 理论与实践: 用弱引用堵住内存泄漏

ThreadLocalMap的源码稍微多了点,我们就看两个最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。
set(ThreadLocal> key, Object value)

 
    private void set(ThreadLocal<?> key, Object value) {
 
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
 
        // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
        int i = key.threadLocalHashCode & (len-1);
 
        // 采用“线性探测法”,寻找合适位置
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
 
            ThreadLocal<?> k = e.get();
 
            // key 存在,直接覆盖
            if (k == key) {
                e.value = value;
                return;
            }
 
            // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
            if (k == null) {
                // 用新元素替换陈旧的元素
                replaceStaleEntry(key, value, i);
                return;
            }
        }
 
        // ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
 
        int sz = ++size;
 
        // cleanSomeSlots 清楚陈旧的Entry(key == null)
        // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

这个set()操作和我们在集合了解的put()方式有点儿不一样,虽然他们都是key-value结构,不同在于他们解决散列冲突的方式不同。集合Map的put()采用的是拉链法,而ThreadLocalMap的set()则是采用开放定址法(具体请参考散列冲突处理系列博客)。掌握了开放地址法该方法就一目了然了。

set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:

private final int threadLocalHashCode = nextHashCode();

从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():

    private static AtomicInteger nextHashCode = new AtomicInteger();
 
    private static final int HASH_INCREMENT = 0x61c88647;
 
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
 
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。

get()
  • 返回当前线程所对应的线程变量
 
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
 
        // 获取当前线程的成员变量 threadLocal
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 从当前线程的ThreadLocalMap获取相对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
 
                // 获取目标值        
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。

getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
set(T value)
  • 设置当前线程的线程局部变量的值。
 
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,如下:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
initialValue()
  • 返回该线程局部变量的初始值。
    protected T initialValue() {
        return null;
    }

该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。

remove()
  • 将当前线程局部变量的值删除。
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。

参考文献

1.并发编程网

目录
相关文章
|
8天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
48 17
|
19天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
4天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
21天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
21天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
21天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
45 3
|
21天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
116 2
|
29天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
48 6
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
59 3