Java多线程知识整理

时间:2023-03-08 15:59:18
Java多线程知识整理

多线程

1. 多线程基础

多线程状态转换图

Java多线程知识整理

普通方法介绍
yeild

yeild,线程让步。是当前线程执行完后所有线程又统一回到同一起跑线。让自己或者其他线程运行,并不是单纯的让给其他线程。

join

等待线程结束;调用线程等待当前线程结束后才能往下执行,阻塞线程之意。join本质是在当前对象实例上调用线程wait()

如下所示:输出完 thread-1后再输出end

public static void main(String[] args) throws InterruptedException{
System.out.println("main start"); Thread t1 = new Thread(new Worker("thread-1"));
t1.start();
t1.join();
System.out.println("main end");
}
sychronized

sychronized确保安全外,还能保证线程之间的可见性和有序性

  1. 指定加锁对象:对给定的对象加锁,进入同步代码前要先获得给定对象的锁
  2. 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要先获得当前实例的锁。
  3. 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要先获得当前类的锁。

2. JDK并发包

重入锁(ReentrantLock)

决定了线程是否可以访问临界区资源。object.wait()和object.notify()起到线程等待和通知的作用。这些工具能实现多线程相互之间的协作作用

sychronized功能的扩展-重入锁

重入锁:

重入锁使用ReentrantLock来实现

public class ReentrantLocker implements Runnable {

    public static ReentrantLock locker = new ReentrantLock();

    private static int i = 0;

    @Override
public void run() { for (int j = 0; j < 1000; j++) {
locker.lock();
try {
i++;
} catch (Exception e) {
e.printStackTrace();
}finally {
locker.unlock();
}
}
} public static void main(String[] args) throws Exception{
ReentrantLocker locker1 = new ReentrantLocker();
// 对同一个对象加锁。important
Thread t1 = new Thread(locker1);
Thread t2 = new Thread(locker1);
t1.start();
t2.start();
/**调用join方法,阻塞当前线程,待所有线程执行完再往下执行*/
t1.join();
t2.join();
System.out.println(i);
}
}
重入锁对比sychronized有点
  1. 中断响应

    对于sychronized来说,一个线程要访问被同步的资源,要么继续执行,要么继续等待。而使用重入锁,线程可以被中断。程序可以取消对锁的请求,避免死锁的发生。
  2. 锁超时设置
//表示线程对资源锁等待时长。
//如果在规定的时间内没有获取到锁,
//则线程获取锁失败
lock.tryLock(5, TimeUnits.SECONDS)

如果tryLock没加参数,就不需要等待。

3. 公平性

公平锁会按照时间先后顺序,保证先到先得,后到者后得。公平锁的一大特点是:不会产生饥饿现象,只要你排队,最后还是能得到资源的

重入锁条件-Condition
信号量(Semaphore)

信号量是对锁的扩展,无论是内部锁sychronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量指定多个线程同时访问同一资源

  1. 构造函数
public Semaphore(int permits);
public Semahore(int permits, boolean fair);

构造信号量对象时,permits参数表示的是:同时多少个线程能访问同一资源。

读写锁(ReadWriteLock)

读写分离锁可以有效的较少锁竞争,用锁分离机制来提升系统性能。

读写锁的访问约束情况

--
非阻塞 阻塞
阻塞 阻塞

倒计时器(CountDownLatch)

可以让某一线程等待直到倒计时结束,再开始执行。当调用wait方法的时候,主线程被阻塞,直到countdown减到0的时候,程序再往下执行。

循环栅栏(CyclicBarrier)

作用是阻止线程继续执行,要求线程在栅栏处等待,计数器可以循环使用。假如将计数器设置为10,那么凑齐第一批10个进程数后计数器会归为0,然后接着凑齐下一批10个线程

线程池

声明一个线程池

线程池的作用是复用线程,减少系统开销。声明一个线程池有如下方法

/**
该方法返回一个固定线程数量的线程池。
当有一个新的任务提交,线程池有空闲的线程就提交,没有空闲的话就缓存到待执行列表中,
直到有空闲的线程才执行
*/
public ExecutorService newFixedThreadService(int threadNum);
/**
该方法值返回包含一个线程的线程池,
如果有多个任务提交也是将多余的任务保存到一个任务队列中,待有空闲的线程才执行
**/
public ExecutorService newSingleThreadExecutor();
/**
返回可根据实际情况调整线程数量的线程池,线程池中的线程数量是不一定的,但还是能够复用空闲线程的
*/
public ExecutorService newCachedThreadPool();
线程池的内部实现

通过源代码可以看出newFixedThreadServicenewSingleThreadExecutor,newCachedThreadPool内部都是由ThreadPoolExecutor来处理的

ThreadPoolExecutor的内部实现

public ThreadPoolExecutor(
// 线程池中线程数量
int corePoolSize,
// 线程池中允许线程最大数量
int maximumPoolSize,
// 线程池中线程数量超出corePoolSize的线程的存活时间
long keepAliveTime,
// 时间单位
TimeUnit unit,
// 任务队列,被提交尚未被执行的任务
BlockingQueue<Runnable> workQueue,
// 拒绝策略,任务太多来不及处理,如何拒绝任务
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}

其中workqueue是指那些尚未被执行的线程队列。它是BlockingQueue的接口对象。在ThreadPoolExecutor中可以使用如下几种 BlockQueue

  • 直接提交的队列
  • 有界的任务队列
  • *任务队列
  • 优先任务队列

ThreadLocal

为每个线程运行时存储所需参数。

如下是线程安全的日期格式化方法

public class SafeDataFormat {
static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>();
static Date date = new Date();
static String dateStr = "2019-04-27 04:12:41";
static class ParseDate implements Runnable { @Override
public void run() {
if (null == simpleDateFormatThreadLocal.get()) {
simpleDateFormatThreadLocal.set(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
try {
System.out.println(simpleDateFormat.parse(dateStr)); } catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 2000; i++) {
executorService.execute(new ParseDate());
} executorService.shutdown();
}
}
ThreadLocal实现原理

先看ThreadLocal的get(), set()方法。

set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

从代码中可以看出首先获取当前线程对象,然后通过getMap()拿到当前线程的ThreadLocalMap,并将值更新到这个map中。

get方法
public T get() {
Thread t = Thread.currentThread();
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();
}

首先get()获取到当前对象的ThreadLocalMap对象,然后将当前线程作为key来获取内部的实际数据。

这些变量是维护在Thread内部的(ThreadLocalMap定义所在类),意味着只要线程不退出,对象的引用就一直都存在着。

ThreadLocalMap的实现使用了弱引用,java虚拟机在进行垃圾回收时,如果发现变量是弱引用,就会立即回收

CAS 比较交换 CompareAndSweep

由于其的非阻塞性,它对死锁天生免疫。

CAS的算法过程

包含三个参数CAS(V, E, N)。V表示要更新的变量,E表示预期值,N表示新值。仅当 V = E时,才会将V设置成N;如果V值跟E值不同,则说明已经被其他线程醉了更新,当前线程什么都不做,最后CAS返回当前V的真实值

CAS操作是抱着乐观的态度进行的,它总认为自己可以完成操作。当多个线程同时CAS同一个值额时候,只有一个线程会胜出并成功更新。失败的线程不会被挂起,仅是被告知操作失败,并允许再次尝试

简单的说,CAS需要额外给出期望值,也就是你认为这个变量最终的样子。如果这个变量最终跟你的预期不同,你就重新读取再次更新就好了。

AtomicInteger

是可变、线程安全的。基于CAS的;就内部实现来说,AtomicIntger保存了核心字段

//代表了当前AtomicInteger的实际值
private volatile int value;
// 保存着value字段在AtomicInteger对象的偏移量。实现AtomicInteger的关键
private static final long valueOffset;

关注一下incrementAndGet()

public final int incrementAndGet() ;
for(; ; )
int v current = get();
int next = cueernt + 1;
if (compareAndSet(current, next))
return next

CAS操作未必是成功的,因此对于不成功的情况,要不断的去重试