Java语言从诞生之初就多线程作为基础模块内置在jdk中,这是因为合理恰当地使用多线程,程序性能会有很大提升。
为什么要使用多线程?为什么恰当使用多线程性能就会提升?
底层硬件是由操作系统来调度的,而应用程序都是跑在操作系统上的,因此,有必要复习一下操作系统的发展史。
目录
volatile和synchronized关键字实现线程通信
ThreadLocal、ThreadLocalMap和Thread的关系
ThreadLocal为什么使用弱引用,为什么存在内存泄漏问题?
操作系统发展史
手工操作:纸带作为输入输出的媒介,机器是单线程式的,专一地干活。
批处理系统:计算机能够成批处理多个作业,机器是单线程式的,吞吐量增大,局限在于流水线式工作,顺序固定,CPU利用率低。
多道程序系统:多个程序同时加入内存并运行,并交替在CPU上运行,当一个程序因为IO请求而暂停运行时,CPU会切换到另一个程序继续运行。局限在于CPU切换不及时,很可能导致需要及时处理的程序没有得到及时处理。
分时系统:时间片轮转,使得每个程序都“走走停停”,在多用户使用一台机器的情况下给用户的感觉就好像自己独占一台机器一样。这就是多线程的缘起——多进程切换。
实时系统:程序对实时性要求高,通常用于军工业、航空业等。
通用操作系统:个人计算机操作系统、网络操作系统、分布式操作系统等,它们都是集成电路、微处理器和计算机网络、分布式处理、巨型计算机的发展产物。
什么是线程,和进程的区别?
进程是系统分配资源的基本单位,如电脑上打开一个软件如QQ,一个进程可以用多个线程,这些线程共享进程的资源。线程是CPU调度的最小单位,是轻量级的进程。
初识Java线程
查看有哪些Java线程在运行
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.
getThreadName());
}
}
为什么要使用多线程?
- 更多的处理器核心。单核一次只能处理一个线程,而多核是真正同时能处理多个线程。
- 更快的响应时间。相当于一项任务交给多个人分工完成,自然响应时间短。
- 更好的编程模型。Java为多线程编程提供了友好的编程环境。
线程优先级
Java的线程优先级分为1~10,最低优先级是1,默认是5,最高优先级是10。
优先级越高,执行的几率越大。
public class Priority {
static volatile boolean notEnd=true;
static class Job implements Runnable{
long count=0;
@Override
public void run() {
while(notEnd) {
count++;
}
}
}
public static void main(String[] args) {
Job j1=new Job();
Thread thread=new Thread(j1);
thread.setPriority(1);
Job j2=new Job();
Thread thread2=new Thread(j2);
thread2.setPriority(10);
thread.start();
thread2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
notEnd=false;
System.out.println("优先级="+thread.getPriority()+", 执行次数"+j1.count);
System.out.println("优先级="+thread2.getPriority()+", 执行次数"+j2.count);
}
}
import java.util.ArrayList;
import java.util.List;
public class Priority {
static volatile boolean notEnd=true;
static class Job implements Runnable{
long count=0;
@Override
public void run() {
while(notEnd) {
count++;
}
}
}
public static void main(String[] args) {
List<Job> list=new ArrayList<>();
List<Thread> threadList=new ArrayList<>();
for(int i=1;i<=10;i++) {
Job job = new Job();
list.add(job);
Thread thread = new Thread(job);
thread.setPriority(i);
threadList.add(thread);
}
try {
Thread.sleep(100);//预备
} catch (InterruptedException e) {
e.printStackTrace();
}
//开始
for(int i=0;i<10;i++) {
threadList.get(i).start();
}
try {
Thread.sleep(10000);//执行10秒
} catch (InterruptedException e) {
e.printStackTrace();
}
notEnd=false;//结束
for(int i=0;i<10;i++) {
System.out.println("优先级="+threadList.get(i).getPriority()+", 执行次数"+list.get(i).count);
}
}
}
大体上,优先级高的线程执行次数更多。
线程状态
需要说明的是,操作系统中的进程状态有五种或七种:
五种状态:新建,就绪,运行,阻塞,终止
七种状态:在五种状态的基础上加上了就绪挂起和等待挂起。挂起是指将进程暂时调离内存,之后可能会从外存中重新调入内存(唤醒,或解除挂起)。
Java线程的六种状态:
- NEW,新建状态,或初始状态,从NEW到RUNNABLE态需要线程调用start()方法。
- RUNNABLE,运行状态,包括RUNNING状态(运行中)和READY状态(就绪),即把就绪和运行态合并称为RUNABLE状态,可运行态。
- BLOCKED状态,线程竞争锁失败时的状态,待其它线程释放锁时,若该线程竞争锁成功,则重新进入RUNNABLE状态。
- WAITING状态,线程调用wait()方法,主动释放锁的状态,一直等待,直到其它线程notify该线程;当然,除了wait方法,还有join()方法(后面会分析),LockSupper.park()方法。
- TIMED_WAITING状态,超时等待状态,线程调用sleep(段时间),wait(段时间),LockSupport.parkNanos(段时间),LockSupport.parkUntil(未来某个时间点),会进入该状态,满足条件后,会自动调用接触休眠的方法。
- TERMINATED状态,终止状态,线程把run方法执行完了的状态。
RUNNABLE状态中,处于RUNNING状态的线程调用yield()方法会主动让出cpu,进入READY就绪状态,处于READY状态的线程若被系统调度,则进入RUNNING执行态。
WAITING等待状态和BLOCKED阻塞状态的区别:BLOCKED状态是一群线程在锁的外面尝试竞争锁(synchronized外面)失败后进入的状态,而wait方法只有拿到锁(synchronized里面)的线程才能调用,否则会抛出异常,调用wait方法后,当前线程进入等待状态,并让出锁,直到其它线程notify或notifyAll唤醒它。
调用WAITING状态的线程,当被notify后,会进入BLOCKED同步队列中,状态变为BLOCKED状态,和同步队列中其它线程一起竞争锁。
顺便提一下,join方法底层调用的是wait(time),time=0表示无限等待,否则进入超时等待。
wait和notify是经典的等待-通知机制,它们都需要线程在持有对象锁的状态才能调用,wait方法调用后马上会释放锁并一直等待,notify方法调用后不会释放锁,直到线程离开同步区。若在synchronized代码块外面调用,则会抛出IllegalMonitorStateException 。
wait一般这么使用,注意是while,不是if:
synchronized(obj) {
while(条件不满足){
obj.wait();
}
...
}
notify则是:
synchronized(obj) {
...
使得条件满足
obj.notify();
}//到这里才释放锁
Daemon线程——守护线程
public class TestDaemon {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("hhhh!");
}
};
Thread t1=new Thread(runnable);
t1.setDaemon(true);
t1.start();
}
}
没有任何输出!!
守护线程的任务就是服务于广大的普通线程群众的,当运行中的只有守护线程时,JVM不会执行守护线程(没意义啊)。线程创建默认是普通线程,只有setDaemon(true)后才是守护线程。
你可能会说,不是还有main函数主线程吗?是的,没错,看如下代码:
public class TestDaemon {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("hhhh!");
}
};
Thread t1=new Thread(runnable);
t1.setDaemon(true);
t1.start();
System.out.println("main hh ");
}
}
加了一行输出后,只输出了主函数的字符串,而守护线程没有输出,再看如下代码:
public class TestDaemon {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("hhhh!");
}
};
Thread t1=new Thread(runnable);
t1.setDaemon(true);
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main hh ");
}
}
好的,终于输出了守护线程的run方法的字符串。上面没输出,是因为在没有休眠的状态下,线程执行地太快了,快到让JVM认为主线程形同虚设。
Thread t1=new Thread(runnable);
t1.start();
t1.setDaemon(true);
会抛出异常:
必须在线程启动前设置为守护线程,启动后不能设置了,好比受精卵,受精后就确定性别了,不能更改了。
启动和终止线程
创建和启动线程
新线程由哪个线程创建,哪个线程就是新线程的父线程。比如在main方法中new Thread(),那么main线程就是新线程的父线程。
启动线程调用start()方法;
中断线程
调用线程的interrupt()方法就可以中断该线程。
线程通过调用isInterrupted()方法来判断是否被中断了,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。
当然,thread.isInterruptted()与Thread.interrupted()是不同的。
thread.isInterruptted(),不会重置中断标志位,中断后第一次调用是true,第二次以后都是true:
Thread.interrupted(),会重置中断位,中断后第一次调用是true,第二次以后都是是false;
线程在wait(),sleep(),RUNNABLE状态都会响应中断。
过期的suspend(),resume(),stop()
好比在B站看视频,suspend就是按下暂停键,resume就是恢复播放,stop关闭B站页面。
它们都是过期的方法,不推荐使用。
suspend方法调用后不会释放已占有的资源,可能会引起死锁;
stop方法调用后不能保证资源正常释放。
安全终止线程
终止线程最佳方式是设置一个boolean类型的标志位,通过改变boolean类型的值来退出循环,让线程安全地终止。
public class TestTerminal {
static class Job implements Runnable {
private volatile boolean on=true;
@Override
public void run() {
while(on) {
System.out.println("do sth!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("执行完了,线程终止!");
}
public void cancel(){
on=false;
}
}
public static void main(String[] args) throws InterruptedException {
Job job=new Job();
new Thread(job).start();
Thread.sleep(500);
job.cancel();
}
}
线程间的通信
线程间的通信有:volatile(可见性)和synchronized(锁)关键字,等待/通知(wait/notify)机制,管道输入/输出流,Thread.join(),ThreadLocal的使用。
volatile和synchronized关键字实现线程通信
volatile主要通过可见性实现线程间通信的,例如两个线程共享一个变量,线程A修改了共享变量(相当于发送了消息),线程B读到了线程A修改后的值(相当于接收了消息)。
synchronized通过加锁的方式实现线程通信,只有拿到锁的线程才能执行同步代码块,竞争锁失败的线程会加入到同步队列中,状态为BLOCKED阻塞状态,当持有锁的线程释放,会重新尝试获取锁。拿到锁的线程在同步块里修改了信息(相当于发送了消息),另一个线程之后也拿到锁,访问修改后的信息(相当于接收了消息)。
wait/notify机制
wait一般这么使用,注意是while,不是if:
synchronized(obj) {
while(条件不满足){
obj.wait();
}
...
}
notify则是:
synchronized(obj) {
...
使得条件满足
obj.notify();
}//到这里才释放锁
管道输入/输出流
public class PipedReaderWriter {
static class MyPipedReader implements Runnable {
PipedReader pipedReader;
MyPipedReader(PipedReader p ) {
pipedReader=p;
}
@SneakyThrows
@Override
public void run() {
int receive=0;
while(((receive=pipedReader.read())!=-1)){
System.out.print((char)receive);
}
}
}
public static void main(String[] args) throws IOException {
PipedWriter pipedWriter=new PipedWriter();
PipedReader pipedReader=new PipedReader();
MyPipedReader myPipedReader=new MyPipedReader(pipedReader);
new Thread(myPipedReader).start();
pipedWriter.connect(pipedReader);//必须建立连接
int sent=0;
while((sent=System.in.read())!=-1) {
pipedWriter.write((char)sent);
}
}
}
Thread.join()实现线程通信
public class TestJoin {
public static void main(String[] args) throws Exception {
Thread thread=new Thread(()->{
System.out.println("Thread start");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread end");
});
Thread.sleep(100);
thread.start();
thread.join();
System.out.println("main terminate.");
}
}
主线程调用线程thread的join方法,主线程会等待thread线程执行完,才会继续执行主线程。join操作有点像把两个线程合并为一个的意思。
ThreadLocal实现线程通信
ThreadLocal是线程的变量,以ThreadLocal对象为key,任意对象为value存储在线程的ThreadLocalMap中。
public class TestThreadLocal {
static ThreadLocal<Integer> local=new ThreadLocal<>();
static void set(Integer v) {
local.set(v);
}
static Integer get() {
return local.get();
}
public static void main(String[] args) {
set(15);
System.out.println(get());
}
}
ThreadLocal、ThreadLocalMap和Thread的关系
一个Thread有一个ThreadLocalMap对象,一个ThreadLocalMap可以存储多个以ThreadLocal为key、以对象为value的键值对。
实际访问时,是通过ThreadLocal来管理ThreadLocalMap的,看源码。
ThreadLocal类中的get方法,获取值:
处理的逻辑:获取当前线程,获取当前线程的ThreadLocalMap,然后通过map.getEntry(this)来获取键值对。
ThreadLocal为什么使用弱引用,为什么存在内存泄漏问题?
垃圾回收器:一个对象只有弱引用指向它,下次我就收了它。
ThreadLocal设计为弱引用也是为了使得垃圾回收器及时回收不需要的ThreadLocal对象,上例ThreadLocalTest是一个强引用,而ThreadLocalMap中是以弱引用存储的。
例如,当一个线程在一个方法内使用了ThreadLocal,通常都会有一个强引用指向Entry<ThreadLocal,value>对象,而离开方法后,就没有强引用指向它了。就没必要再存储ThreadLocal对象了。
然而,仍然存储内存泄漏问题,GC回收ThreadLocal后,对应的Entry的key=null,而value并没有回收,也永远拿不到value值(因为访问value是通过ThreadLocal->ThreadLocalMap进行访问的),造成了内存泄漏,直到线程执行完了,value对象才会被回收。
ThreadLocal与synchronized区别
synchronized是实现多线程共享数据的方式,而ThreadLocal是实现数据隔离的方式。
参考书籍:
Java并发编程的艺术