Java多线程常见问题整理

前言

线程池源码分析

  1. 线程池的池子是哪个数据结构
  2. 线程池构造方法的参数的含义
  3. FutureTask如何获取到结果,任务没完成就ft.get()是怎么阻塞的
  4. 线程池提交runnable和callable是有什么区别和联系
  5. 工作线程Worker是如何处理池子和阻塞队列的任务的
  6. coreSize个线程数是如何保持住的
  7. 线程池是如何进程保持在哪里的(除非你手动shutDown)
  8. 数据库线程池和这个有什么联系

如何创建线程和线程池

四种方法创建线程:
1)继承Thread类创建线程,重写run方法。
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架

  1. 例子
public class MyThread extends Thread{//继承Thread类
  public void run(){
  //重写run方法
  }
}

public class Main {
  public static void main(String[] args){
   new MyThread().start();//创建并启动线程
  }
}
  1. A实现runnable接口,重写run方法。新建Thread对象B,并用A作为target,B才是一个线程。A只是一个任务。
public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}

public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread2 myThread=new MyThread2();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者    new Thread(new MyThread2()).start();
  }
}
  1. Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。call()方法可以有返回值,也可以抛出异常。

为什么要用线程池?

也就是线程池的优点:

  1. 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
  2. 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

什么是线程池

线程池的几个参数

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler) 
  1. corePoolSize 线程池中核心线程的数量
  2. maximumPoolSize 线程池中最大线程数量
  3. keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长
  4. unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等
  5. workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。
  6. threadFactory 为线程池提供创建新线程的功能,这个我们一般使用默认即可
  7. handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。

这7个参数中,平常最多用到的是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue.在这里我主抽出corePoolSize、maximumPoolSize和workQueue三个参数进行详解。
maximumPoolSize(最大线程数) = corePoolSize(核心线程数) + noCorePoolSize(非核心线程数);

(1)当currentSize<corePoolSize时,没什么好说的,直接启动一个核心线程并执行任务。

(2)当currentSize>=corePoolSize、并且workQueue未满时,添加进来的任务会被安排到workQueue中等待执行。

(3)当workQueue已满,但是currentSize<maximumPoolSize时,会立即开

启一个非核心线程来执行任务。

(4)当currentSize>=corePoolSize、workQueue已满、并且currentSize>maximumPoolSize时,调用handler默认抛出RejectExecutionExpection异常。

还有四种线程池:
(1)FixedThreadPool:
Fixed中文解释为固定。结合在一起解释固定的线程池,说的更全面点就是,有固定数量线程的线程池。其corePoolSize=maximumPoolSize,且keepAliveTime为0,适合线程稳定的场所。
(2)SingleThreadPool:
Single中文解释为单一。结合在一起解释单一的线程池,说的更全面点就是,有固定数量线程的线程池,且数量为一,从数学的角度来看SingleThreadPool应该属于FixedThreadPool的子集。其corePoolSize=maximumPoolSize=1,且keepAliveTime为0,适合线程同步操作的场所
(3)CachedThreadPool:
Cached中文解释为储存。结合在一起解释储存的线程池,说的更通俗易懂,既然要储存,其容量肯定是很大,所以他的corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE(2^32-1一个很大的数字)
(4)ScheduledThreadPool:
Scheduled中文解释为计划。结合在一起解释计划的线程池,顾名思义既然涉及到计划,必然会涉及到时间。所以ScheduledThreadPool是一个具有定时定期执行任务功能的线程池。

线程池的单例(也可作为线程安全的单例的例子)

为什么要用线程池的单例?
线程池用的好好的,用的时候创建一个,不用就不管他,那为什么要将线程池设计成单例模式呢。那么就要看看你将线程池应用的场所了。一般情况下,整个系统中只需要单种线程池,多个线程公用一个线程池,不会是每创一个线程就要创建一个线程池,那样子你还不如不用线程池呢。

 // 获取单例的线程池对象
    public static ThreadPool getThreadPool() {
        if (mThreadPool == null) { //第一次判断是否为null
            synchronized (ThreadManager.class) { // synchronized,多线程下的单例模式
                if (mThreadPool == null) { // 第二次判断是否为null,因为如果走到上一步,有可能刚好被另一个线程获得了锁,创建了对象。
                    int cpuNum = Runtime.getRuntime().availableProcessors();// 获取处理器数量
                    int threadNum = cpuNum * 2 + 1;// 根据cpu数量,计算出合理的线程并发数
                    mThreadPool = new ThreadPool(threadNum, threadNum, 0L);
                }
            }
        }
        return mThreadPool;
    }

线程池源代码

在这里插入图片描述

ThreadPoolExecutor的executor方法

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //workerCountOf(c)会获取当前正在运行的worker数量
        if (workerCountOf(c) < corePoolSize) {
            //如果workerCount小于corePoolSize,就创建一个worker然后直接执行该任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //isRunning(c)是判断线程池是否在运行中,如果线程池被关闭了就不会再接受任务
        //后面将任务加入到队列中
        if (isRunning(c) && workQueue.offer(command)) {
            //如果添加到队列成功了,会再检查一次线程池的状态
            int recheck = ctl.get();
            //如果线程池关闭了,就将刚才添加的任务从队列中移除
            if (! isRunning(recheck) && remove(command))
                //执行拒绝策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果加入队列失败,就尝试直接创建worker来执行任务
        else if (!addWorker(command, false))
            //如果创建worker失败,就执行拒绝策略
            reject(command);
}

创建线程的方法:addWorker

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        //使用自旋+cas失败重试来保证线程竞争问题
        for (;;) { //相当于while,Java里没区别,c里面性能更好
            //先获取线程池的状态
            //用来描述线程池的状态(pool control state)
            int c = ctl.get();
            int rs = runStateOf(c);

            // 如果线程池是关闭的,或者workQueue队列非空,就直接返回false,不做任何处理
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                //根据入参core 来判断可以创建的worker数量是否达到上限,如果达到上限了就拒绝创建worker
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //没有的话就尝试修改ctl添加workerCount的值。这里用了cas操作,如果失败了下一个循环会继续重试,直到设置成功
                if (compareAndIncrementWorkerCount(c))
                    //如果设置成功了就跳出外层的那个for循环
                    break retry;
                //重读一次ctl,判断如果线程池的状态改变了,会再重新循环一次
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            final ReentrantLock mainLock = this.mainLock;
            //创建一个worker,将提交上来的任务直接交给worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                //加锁,防止竞争
                mainLock.lock();
                try {
                    int c = ctl.get();
                    int rs = runStateOf(c);
                    //还是判断线程池的状态
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        //如果worker的线程已经启动了,会抛出异常
                        if (t.isAlive()) 
                              throw new IllegalThreadStateException();
                        //添加新建的worker到线程池中
                        workers.add(w);
                        int s = workers.size();
                        //更新历史worker数量的最大值
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        //设置新增标志位
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //如果worker是新增的,就启动该线程
                if (workerAdded) {
                    t.start();
                     //成功启动了线程,设置对应的标志位
                    workerStarted = true;
                }
            }
        } finally {
            //如果启动失败了,会触发执行相应的方法
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
}

源码分析看这篇就够了:
https://blog.csdn.net/programmer_at/article/details/79799267
这些文章也讲的特别好:
https://blog.csdn.net/mayongzhan_csdn/article/details/80790966
https://blog.csdn.net/u013332124/article/details/79587436

synchronize和volatile

在这里插入图片描述

  • synchronize
Object o = new Object();
synchronized(o) {
 ...
 }

上面那个代码块的意思就是谁持有了o的锁,谁执行下面的代码。

  • volatile
    在多线程的情况下,Java是通过共享内存
    上图中,如果多线程的情况下,一个线程修改了变量,另一个线程还从共享变量的副本里面读取变量是不行了。必须给这个变量加上volatile关键字,保证一个线程修改这个变量其他线程也会收到相应的变量修改通知。
    volatile还有一个功能就是禁止指令重排序,这个就涉及更底层的东西了。

HashMap, HashTable, ConcurrentHashMap

三者关系:

HashMap不是线程安全的。HashTable是线程安全的,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化。
Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
区别和联系这个讲的好

HashMap

https://blog.csdn.net/vking_wang/article/details/14166593

HashTable

基本就是HashMap加一个synchronized

ConcurrentHashMap

分段锁实现
synchronized及自旋锁实现
区别:
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

cas

什么是cas

使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS的操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程
CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。cas是非阻塞同步,synchronized是阻塞的。

CAS问题

ABA

因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。

自旋时间过长

使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

锁的分类

公平锁/非公平锁

可重入锁

独享锁/共享锁

互斥锁/读写锁

乐观锁/悲观锁

乐观锁一般会使用版本号机制或CAS算法实现。
乐观锁适合多读。悲观锁适合多写。

分段锁

偏向锁/轻量级锁/重量级锁

自旋锁

数据库中的锁

锁分类

**按锁的粒度划分:**表级锁、行级锁、页级锁
**按锁级别划分:**共享锁(读)、排它锁(写)、意向锁
**按加锁方式划分:**自动锁、显示锁
**按使用方式划分:**乐观锁、悲观锁

事务及ACID

原子性(Atomicity)

原子性是指一个事务是一个不可分割的工作单位,要不全部执行,要不全部不执行

一致性(Consistency)

事务开始之前和事务结束以后,数据库的完整性约束没有被破坏

隔离性(Isolation)

事务隔离性是指多个事务并发执行时,相互之间无影响。

持久性(Durability)

持久性即当一个事提交后,对数据库的改变是永久性的,不会被回滚。

ThreadLocal类

给线程设置上了一个局部变量。使得,不会因为多线程访问同一个资源而产生多线程同步问题。

因为这个ThreadLocal类里面放的是每个线程都拥有一个副本,线程之间彼此不会互相影响。
get方法:

 public void set(T value) {
     		//拿到当前线程对象
      	 Thread t = Thread.currentThread();
	 //拿到一个Map对象,好,那么问题来了?这个是一个什么样的map对象呢??
	 //其实这个ThreadLocalMap对象是ThreadLocal类内部自己实现的一个类似于HashMap这样一个类
        ThreadLocalMap map = getMap(t);
	//如果map非空,则将当前的ThreadLocal对象和这个set()方法的参数put到这个Map里面去,
	//如果没有将其创建。
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法

public T get() {
	//拿到当前的线程对象
 	Thread t = Thread.currentThread();
	//拿到ThreadLocalMap对象,到了这里我们进去getMap()方法里面去看一下 
	ThreadLocalMap map = getMap(t);
 	if (map != null) {
     	ThreadLocalMap.Entry e = map.getEntry(this);
     	if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
  		}
	}
	return setInitialValue();
}
// 可以看出来map的键是hash值
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);
}

ThreadLocal底层是使用一个自己实现的Map来存储用户set()进来的值。 那个map的键,即为当前的ThreadLocal对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值