什么是并发?
并发是指在某个时间段内,多任务交替处理的能力。比如说,你有一台单核心CPU电脑,注意必须是以单核心来理解下面的例子才是正确的。你可以利用电脑边听音乐,边写PPT,还能随时斗上一盘地主。这些事情表面上看起来就是同一时刻在执行多个任务。在同一应用上又可以做多个事情,比如你玩斗地主,程序在播音乐,在处理各个玩家的出牌,还要处理玩家有聊天界面上相互骂娘的任务。在单核CPU电脑上能“同时”处理多个事情,这个叫并发。假设你是多核心电脑,你在电脑时同时进行多个任务,此时叫并行。
并发与并行相对容易混淆,核心区分就是进程是否同时执行。并发处理是为了提高程序的运行效率。
进程和线程
在计算机操作系统中可以同时运行多个进程,每个进程会有多个线程。比如同时在电脑上打开音乐应用,视频应用,那么这两个应用就是处于两个不同的进程中。进程相互是独立隔离运行的,操作系统负责给进程分配资源,比如内存资源,线程调度等。进程间通讯需要借助如socket、共享内存等方式。线程是操作系统调度的的最小单位,运行一段代码,就是在线程中运行的,线程会顺序执行代码,通过栈来存储运行过程中的数据,程序计数器记录程序已经执行到的行数(这样当线程切换,后面再次获取CPU时间片时得以恢复执行正确的代码行)。因此,进程与线程的关系也很明显,进程包含多个线程。
Java多线程与线程安全
当启动一个进程时,操作系统会分配给进程内存、cpu等资源以运行。Java多线程是指在一个Java应用中同时运行多个线程的能力。当然,如果是多核CPU计算机,那么Java应用在任一时刻,是可以同时运行多个线程的。但同时运行的这些线程却有可能同时访问一些共享内存变量,此时就涉及到线程安全了,因为极有可能不同线程同时将一个变量值读取了,并各自修改,然后相互之间还不知晓数据已改,导致程序执行错误。要保证数据安全则需要额外的机制,如锁、CAS、内存读写屏障等手段来保证线程安全。
如何创建Java线程
方式一,实现Runnable
/**
* @author kangming.ning
* @date 2020/10/12 16:14
*/
public class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println("用实现Runnable接口实现线程,ThreadName:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(new RunnableThread()).start();
//匿名方式如下
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名Runnable接口实现线程,ThreadName:" + Thread.currentThread().getName());
}
}).start();
//lambda 表达式
new Thread(() -> System.out.println("lambda 表达式Runnable接口实现线程,ThreadName:" + Thread.currentThread().getName())).start();
}
}
上面是通过Runnable方式来实现run接口,然后传递Runnable对象给Thread以实现线程运行。
方式二,继承Thread
/**
* @author kangming.ning
* @date 2020/10/12 16:21
*/
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println("用Thread类实现线程,ThreadName:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
ExtendsThread extendsThread = new ExtendsThread();
extendsThread.start();
}
}
可以看到,直接继承Thread,然后重写run方法即可实现线程。
Java创建线程的方式有很多种,但换汤不换药,Java创建线程本质上只有一种,就是继承 Thread ,然后通过Thread的run方法运行程序。常用的实现Runnable方式也是借助Thread的run方法而已。可以简单看一下源码,下面源码摘取自Thread类
/* What will be run. */
private Runnable target;
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
@Override
public void run() {
if (target != null) {
target.run();
}
}
很明显,在run方法中,判断如果target不为空,则直接运行target的run方法。所以说,本质上,Java实现线程只有一种方式。但实现Runnable这种方式建议使用,因为直接Java是单继承的,要是继承了Thread类,它就没法继承其它类了,扩展性不好。另外实现Runable的方式和线程池更搭,线程池是为了更高效的利用线程,避免线程频繁创建、销毁线程的开销,所以使用Runnable创建任务,再把任务交给线程池去执行是比较好的(应该没人傻到用继承Thread的方式,再把它扔线程池吧,虽说可以运行,但没必要)。因此,实现线程都建议采用实现Runnable接口的方式。
当前线程
任一时刻,一个CPU核心只能执行一个线程,这个线程就被称为当前线程。线程都是操作系统通过CPU轮询以获取时间片,然后执行,不会让一条线程无限执行。在Java中获取当前线程如下
/**
* @author kangming.ning
* @date 2023-02-14 17:20
* @since 1.0
**/
public class CurrentThreadDemo {
public static void main(String[] args) {
new Thread("custom thread") {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getName());
}
}.start();
System.out.println("当前线程:" + Thread.currentThread().getName());
}
}
打印如下
当前线程:main
当前线程:custom thread
可见,同样是Thread.currentThread().getName(),但获取到的线程名称不同。进入源码发现是一个本地方法,也就是获取当前线程是由虚拟机来实现的。
/**
* Returns a reference to the currently executing thread object.
*
* @return the currently executing thread.
*/
public static native Thread currentThread();
从注释也不难看出,调用这个本地方法会返回一个当前CPU正在执行的线程的对象引用,因线程也是一个对象,不同对象所记录的信息自然是不同的。