Java并发线程基础

什么是进程和线程?

从操作系统层面理解:

·进程就是运行着的程序,它是程序在操作系统的一次执行过程,是一个程序的动态概念,进程是操作系统分配资源的基本单位。

·线程可以理解为一个进程的执行实体,它是比进程粒度更小的执行单元,也是真正运行在cpu上的执行单元,线程是操作系统调度资源的基本单位。

进程中可以包含多个线程,需要记住进程是操作系统分配资源的基本单位,线程是操作系统调度资源的基本单位。

这时候聪明的小朋友就会问了,唉朱老师朱老师,调度和分配都是什么意思呀?
个人的理解是,进程所谓的分配资源更加面向物理层,比如内存和文件句柄;
线程,虽然一个进程内的多个线程可以共享该进程的内存空间和其他资源,但是每个线程有自己的执行路径和状态信息。操作系统会根据线程的优先级、等待时间等因素,决定哪个线程可以获得 CPU的使用权,从而实现并发执行。这个就是调度。

从Java程序理解 :

·启动一个Java程序,操作系统就会创建一个Java进程。

·在一个进程里可以创建多个线程,所以在一个]ava程序里,可以自定义创建多个线程,这些线程拥有各自独立的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。

线程优先级

操作系统基本采用时间分片的形式来分配处理器资源给线程运行,一个线程如果用完了一个 时间片就会发生线程调度,即便线程还没执行完也需要等待下一次分配。所以线程能获得越多的时间片分配,也就能更多的使用处理器资源,而优先级就是一个可以指定线程应该多分还是少分时间片的属性。

Java线程可以通过setPriority()方法来设置优先级,默认是5,而可以设置的范围是1-10。针对频繁阻塞(频繁休眠或者I0操作较多)的线程应该设置高优先级,而计算较多,耗CPU的线程则应该设置更低的优先级,避免处理器被独占。这就好比一场考试要做100道1+1和1道IMO,为了全部做完肯定是先把1+1全做了对吧。

线程状态

实例化创建之后一共有五个状态:

RUNNABLE: 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作”运行中’。

BLOCKED: 阻塞状态,表示线程阻塞于锁。

个人理解:blocked相当于是synchronized的独占机制,和Lock不同。
Lock 源码层面,底层还是使用的 LockSupport.park(),所以根据上图,会进入 waitting

WAITING: 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些等待动作(通知或中断)。

TIME_WAITING: 超时等待状态,该状态不同于WAITING状态,它可以在指定的时间自行返回。

TERMINATED: 终止状态,表示当前线程已经执行完毕。

线程构造方式

首先需要明确,实现接口比继承Thread类实现线程更好。这是因为:

· Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;

· 类可能只要求可执行就行,继承整个 Thread 类开销过大。

线程构造方式有三种:

1.实现Runnable接口

2.实现Callable接口

3.继承Thread类

线程中断/终止

简单来说,当线程A要通知线程B终止时,会发送interrupt方法进行中断操作;反过来,线程通过isInterrupted()方法来判断自己是否被执行了中断操作,并做出响应。

InterruptedException

为什么线程不应该强制停止?

线程是否停止应该由线程本身决定,而不是其他线程进行强制关闭。

可以设想一下,我们很多工作并不是可以贸然停止的,例如A想要停止B时,B线程正在写入一个文件,可能文件正写入一半,如果立即停止那数据就是不完整的。但是对于A来说,它是感知不到B进行到什么阶段的,所以也没办法选择一个最佳时机来停止B,要想安全稳妥的停止B线程,确实只能B线程自行决策。

再看几种停止线程的错误方法。比如 stop(),suspend()和resume(),这些方法已经被 Java 直接标记为 @Deprecated。如果再调用这些方法,IDE 会友好地提示,我们不应该再使用它们了。是因为 stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务夏然而止,会导致出现数据完整性等问题。

线程通信

现实的多线程模式下,每一个线程并不是独立的执行就完事了更常见的是线程之间需要相互协作才能更好的完成一项任务。协作的过程中,就免不了线程之间需要相互通信。Java提供的一些可以完成线程间通信的机制:

1.Volatile: 当两个线程A,B共同使用一个普通共享变量的时候,线程A对变量进行了修改,另外一个线程B是不能保证一定能看到最新值的(原因参考上篇)。这就导致了线程之间的可见性问题,并发程序基于此运行是会出错的。为了解决这个问题,Java提供了Volatile关键字,被该关键字修饰的变量不会存在B线程读不到最新值的情况。

2.Synchronized: 这是]ava提供的另外一个关键字,它可以修饰方2.法或者同步块,被修饰之后能够确保同一时间只有一个线程可以处于方法或者同步块中,所以在方法和同步块中去访问共享变量,可以保证可见性和排他性。

3.Thread.join():A B两线程,A调用B.join(),表示A需要等待B完全执行完成,才会从B.join()处返回继续执行。当然也支持join(long)和 join(long,int)两种超时返回。调用join后,A线程会处于等待(WAITING)或 超时等待(TIMED WAITING)状态。

4.wait/notify: Java还提供了“等待/通知”的机制来进行线程间的协作运行。

通知机制

ThreadLocal

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal是以ThreadLocal对象为Key,任意对象为值的存储结构(其底层是在线程里维护了一个map,map的key就是各种ThreadLocal对象),当一个Key-Value值被存储之后,会一直附带在线程上,所以你可以在线程执行的任何位置再通过这个ThreadLocal对象取到存入的一个值。另外设定或修改值的方式是SET(T),获取值的方式是get()。

以下是使用ThreadLocal的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {

private static class PrintTime {
// 这里创建了一个ThreadLocal对象,叫做TIME_THREADLOCAL_OBJECT
private static final ThreadLocal<Long> TIME_THREADLOCAL_OBJECT = new ThreadLocal<>();

public static final void begin(){
// set方法只需要传value值,无需传key,是因为TIME_THREADLOCAL_OBJECT对象会作为key
TIME_THREADLOCAL_OBJECT.set(System.currentTimeMillis());
}

public static final long end(){
// get方法无需传key,是因为TIME_THREADLOCAL_OBJECT对象会作为key
return System.currentTimeMillis()-TIME_THREADLOCAL_OBJECT.get();
}
}

public static void main(String[] args) throws InterruptedException {
PrintTime.begin();
TimeUnit.SECONDS.sleep(2);
long time = PrintTime.end();
System.out.println(time+"毫秒");
}
}

为什么ThreadLocal会造成内存泄露?如何解决

ThreadLocalMap中的key为ThreadLocal的弱引用,value为强引用。如果ThreadLocal对象没有被外部强引用垃圾回收时key会被清理掉,但value不会。这时key=null,而value不为null,如果不做处理,value将永远不会被GC掉,就有可能发生内存泄漏。ThreadLocalMap的实现中考虑了这个问题,在调用get/set/remove时会清理掉key为null的entry。在编程时如果意识到当前编写的run方法里不再会使用ThreadLocal对象了,最好手动调用remove。

有点像 redis 删除过期 key 的策略有懒惰删除和定期删除。懒惰删除在获取 key 时发现过期了才会删除该 key,但你不去用这个 key 触发不了懒惰删除,导致 key-value 常驻内存。