- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
现代的操作系统(Windows,Linux,Mac OS)等都可以同时打开多个软件(任务),这些软件在我们的感知上是同时运行的,例如我们可以一边浏览网页,一边听音乐。而CPU执行代码同一时间只能执行一条,但即使我们的电脑是单核CPU也可以同时运行多个任务,如下图所示,这是因为我们的 CPU 的运行的太快了,把时间分成一段一段的,通过时间片轮转分给多个任务交替执行.
把CPU的时间切片,分给不同的任务执行,而且执行的非常快,看上去就像在同时运行一样。例如,网易云执行50ms,浏览器执行50ms,word 执行50ms,人的感官根本感知不到。现在多数的电脑都是多核(多个 CPU )多线程,例如4核8线程(可以近似的看成8个 CPU ),也是把每个核心运行时间切片分给不同的任务交替执行.
进程(Process)是操作系统对一个正在运行的程序的一种抽象,我们可以进程简单理解为操作系统中正在运行的一个软件,即把一个任务称之为一个进程,例如我们的网易云音乐就是一个进程,浏览器又是另外一个进程.
线程(Thread)线程是一个比进程更小的执行单位,进程是线程的容器,一个进程至少有一个线程而且可以产生多个线程,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据,多线程之间比多进程之间更容易共享数据,而且线程一般来说都比进程更加高效.
。
java语言内置了多线程支持:JVM 启动时会创建一个主线程,该主线程负责执行 main 方法,一个 Java 程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等.
我们需要区分线程和线程体两个概念,线程可以驱动任务,因此需要一个描述任务的方式,这个方式就是线程体,而我们创建线程体有多种方式,而创建线程只有一种:将任务(线程体)显示的附着到线程上,调用 Thread 对象的 start()方法,执行线程的初始化操作,然后新线程调用 run() 方法启动任务.
创建线程体可以使用下面 3 种方式,然而这 3 种方式都是在创建线程体,直到调用 Thread 对象的 start() 方法时才请求 JVM 创建新的线程,具体什么时候运行有线程调度器 Scheduler 决定.
/**
* 1、定义Thread类的子类
*/
public class MyThread extends Thread {
//2、重写Thread类的run方法
//run()方法体内的内容就是线程要执行的代码
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
//3、创建线程对象
MyThread mt = new MyThread();
//4、启动线程
mt.start();
/**
* 调用线程的start()方法来启动线程,启动线程的实质是请求JVM运行相应的线程,
* 这个线程具体什么时候运行,由线程调度器(scheduler)决定
* 注意:
* 调用start()方法不代表线程能立马运行
* 线程启动后会运行run()方法
* 如果启动了多个线程,start()调用的顺序不一定就是线程启动的顺序
*/
}
//1、实现Runnable接口
public class MyRunable implements Runnable{
//2、实现run方法
@Override
public void run() {
// ...
}
public static void main(String[] args) {
//3、将实现了Runnable接口的对象传入Thread的构造方法中
Thread thread = new Thread(new MyRunable());
//4、启动线程
thread.start();
}
}
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableExample {
public static void main(String[] args) {
// 1、实现Callable接口的匿名内部类
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Callable task is running");
return 42;
}
};
// 2、将Callable包装在RunnableFuture实现类中
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 3、将FutureTask实例传递给Thread类来执行
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在日常使用中,建议能用接口实现就不要用继承 Thread 的方式来创建线程,原因如下:
一个线程对象只能调用一次 start() 方法启动新线程,并在新线程中执行 run() 方法,一旦 run() 方法 执行完毕,线程就终止死亡了。我们通过 Thread 类中的枚举类 State 来看一下 Java 线程有哪些状态:
public enum State {
/**
* 新建状态
* 还没有执行start()方法的线程状态
*/
NEW,
/**
* 可运行状态
* 在Java虚拟机中运行处于可运行状态的线程,可能正在等待其他资源,例如处理器
*/
RUNNABLE,
/**
* 阻塞状态
* 处于阻塞状态的线程正在等待监视器锁,以进入同步代码块或在调用wait()后重新进入
*/
BLOCKED,
/**
* 无限期等待状态
* 线程因调用一下方法之一而处于无限期等待状态:
* Object.wait with no timeout
* Thread.join with no timeout
* LockSupport.park
* 处于等待状态的线程正在等待另一个线程执行特定操作
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态
* 线程处于定时等待状态的原因是调用了以下方法之一,并指定了正等待时间:
* Thread.sleep
* Object.wait with timeout
* Thread.join with timeout
* LockSupport.parkNanos
* LockSupport.parkUntil
*/
TIMED_WAITING,
/**
* 已终止线程的线程状态.
* 线程已执行完毕.
*/
TERMINATED;
}
。
由源码可知,Java 的线程状态有 6 种:
线程状态的转换可以参考下图:
创建线程后未启动线程状态为 NEW,在该线程调用 start() 方法以前会一直保持这种状态。此时,JVM 会为该线程分配内存并初始化其成员变量的值,但是该线程并没有表现出任何线程的动态特征,程序也不会执行线程的执行体,即 run() 方法的部分.
下面的代码,我们可以调用 Thread.getState() 方法来获取线程的状态,可以看出打印出来的状态为 NEW.
public void ThreadTest() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ThreadTest");
}
});
System.out.println(t.getState()); // NEW
}
当在Java的Thread对象上调用start()方法后,以下过程将会发生:
处于RUNNABLE 状态的线程要么正在运行中,要么已经准备好运行但正在等待系统分配 CPU 资源.
在Java虚拟机(JVM)中,JVM 自带的线程调度器负责决定Java线程的执行顺序。它会根据线程的优先级和调度算法来确定哪个线程可以获得 CPU 时间。通常情况下,程序员可以通过设置线程的优先级来影响线程调度器的决策,但实际线程的调度仍由 JVM 负责.
当线程尝试访问某个由其他线程锁定的代码块时,该线程会因为需要等待获取监视器锁进入 BLOCKED 状态,线程获取锁后就会结束此状态.
线程正在等待另一个线程执行特定操作时处于 WAITING 等待状态,例如当线程调用以下方法时会进入 WAITING 等待状态:
调用方法 。 |
退出条件 。 |
Object.wait() 。 |
Object.notify() / Object.notifyAll() 。 |
Thread.join() 。 |
被调用的线程(Thread)执行完毕 。 |
LockSupport.park() 。 |
- 。 |
上述方法中的 wait() 和 join() 没有传入超时时间 timeout 参数,线程只能等待其他线程显示的唤醒或执行完毕,否则不会被分配 CPU 时间片.
线程在这种状态下属于期限等待,无需其他线程显示的唤醒当前线程,在一定时间内被系统自动唤醒.
阻塞和等待的区别在于:阻塞是被动的,等待是主动的。阻塞是在等待获取锁,而等待是在等待一定的条件发生.
调用方法 。 |
退出条件 。 |
Thread.sleep() 。 |
时间结束 。 |
设置了 Timeout 参数的 Object.wait() 方法 。 |
时间结束 / Object.notify() / Object.notifyAll() 。 |
设置了 Timeout 参数的 Thread.join() 方法 。 |
时间结束 / 被调用的线程执行完毕 。 |
LockSupport.parkNanos() 方法 。 |
- 。 |
LockSupport.parkUntil() 方法 。 |
- 。 |
线程执行完毕或者产生异常而结束会进入 TERMINATED 状态,进入该状态的线程已经死亡.
并发问题产生的原因是:多个线程同时对一个共享资源进行非原子性操作,这里面包含了三个产生并发问题的三个条件:多个线程同时,共享资源,非原子性操作,解决线程安全问题的本质就是要破坏这三个条件,因此可以把多线程的并行执行,修改为单线程的串行执行,即同一时刻只让一个线程执行,这种解决方式就叫做互斥锁.
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock,从而达到保护共享资源的目的,当多条线程执行到被保护的区域时,都需要先去获取到锁,这时候只能有一条线程获取到锁,执行被保护区域的代码,其他线程在保护区外部等待获取锁,直到当前线程执行完毕释放资源后,其他线程才有执行的机会.
synchronized 和 ReentrantLock 可以保证可见性、原子性和有序性,另外一个 Java 的关键字 Volatile 也可以保证可见性,另外后者还可以禁止指令重排序.
在 Java 中每个对象都可以作为锁,Synchronized 也是依赖 Java 的对象来实现锁,一共有三种类型的锁:
在使用 Synchronize 时也有三种不同的方式:
public class A {
String lockObject = new String();
// 锁定当前的实例,this锁,每个实例拥有一个锁
public synchronized void a() {};
// 修饰的是静态方法,使用的 class 锁,多个对象共享 class 锁
public static synchronized void b() {}
public void c() {
// 修饰的是代码块,使用的 lockObject 对象的锁,也是实例锁
synchronized(lockObject) {
// do something
}
// 修饰代码块,使用的 B.class 类对象锁
synchronized(B.class) {
}
}
}
public class B {
}
三种不同的使用方式有不同的应用场景,我们在使用的过程中一定要注意加锁的对象是谁,否则可能会产生意想不到的结果。在加锁时,尽量减少加锁的区域,例如能够在方法体中对代码块加锁,就不要在方法上面加锁,加锁的区域越短越好.
ReentrantLock 是 Java.util.concurrent(J.U.C)包中的锁,该锁由 JDK 实现,而 synchronized 是由 JVM 实现的.
public class ReentrantLockDemo{
private Lock lock = new ReentrantLock();
private void func() {
lock.lock(); // 加锁
try {
for (int i = 0; i < 10; i++) {
system.out.prrint(i)
}
} finally {
lock.unlock(); // 确保释放锁
}
}
}
public static void main(Stirng[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
上面的代码演示了ReentrantLock 的使用方法,显示的调用 lock()方法加锁,在 finally 中显示的释放锁.
不同点 。 |
synchronized 。 |
reentrantLock 。 |
实现方式 。 |
JVM 。 |
JDK 。 |
性能 。 |
新版本 Java 对 synchronized 进行了大量的优化,大致相同 。 |
|
等待可中断 。 |
不可 。 |
可以 。 |
公平锁 。 |
非公平 。 |
默认非公平,支持公平锁 。 |
绑定多个条件 。 |
无 。 |
帮点多个 Condition 对象 。 |
在需要使用锁时,除非需要使用 reentrantLock 的高级功能,否则优先使用 synchronized 关键字加锁,这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生支持它,而 ReentrantLock 不是所有的 JDK 版本都支持,并且使用 syschronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保释放锁.
线程池可以管理一系列线程,当有任务需要处理时,直接从线程池里面获取线程来处理,当线程处理完任务时再放回到线程池中等待下一个任务,这样可以减少每次创建线程的开销,提升资源的利用率。线程池提供了一种限制和管理资源的方式,每个线程池还维护了一些基本的统计信息,例如 已完成任务的数量等。在《Java 并发编程的艺术》一书中提到使用线程有三点好处:
可以使用内置的线程池,通过 Executor 框架的工具类 Executors 来创建预先定义好的线程池.
Executors 工具类提供的创建线程池的方法如下图所示:
。
从上图中可以看出,Executors 工具类可以创建多种类型的线程池,包括:
如下图,可以通过 ThreadPoolExecutor 构造函数来创建线程池(推荐).
。
优先推荐使用 ThreadPoolExecutor 来创建线程池,在《阿里巴巴 Java 开发手册》中指出线程资源必须使用线程池来提供,不允许在应用中自行显示创建线程,也强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式来创建线程池.
使用内置的线程池有以下缺点:
实际上内置的线程池也是调用 ThreadPoolExecutor 来创建的线程池:
// 无界队列 LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 无界队列 LinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// DelayedWorkQueue(延迟阻塞队列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
我们来看一下自定义创建线程池的参数有哪些?
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 当线程数大于核心线程数时,
// 多余的空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂,用于创建线程,一般默认
RejectedExecutionHandler handler) { // 拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
如上,ThreadPoolExecutor 中有三个很重要的参数 。
ThreadPoolExecutor 其他常见参数:
下面这张图可以看出,核心线程数量为 4,最大线程数量为 8。新任务提交到线程池时,首先判断是否有线程或者线程数量是否小于核心线程数,若满足则首先创建新的线程执行任务,当核心线程数到达 corePoolSize 时,将任务缓存到任务队列中,当任务队列存放的任务到达队列容量时,再创建新的线程,直到达到 maxnumPoolSize 的线程数量,后续再根据拒绝策略返回.
。
如果当前线程池同时运行的线程数量达到了最大线程数并且队列也已经放满了任务时,ThreadPoolExecutor 再接收到新的线程时,会执行一些预定义的拒绝策略,例如:
ThreadPoolExecutor 默认执行的是 AbortPolicy,抛出 RejectedExecutionException 来拒绝新任务。如果不想丢弃任务,也可以使用 CallerRunsPolicy 将任务回退给调用者,使用调用者线程来执行任务.
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 直接主线程执行,而不是线程池中的线程执行
r.run();
}
}
}
。
本文从进程与线程、创建线程、线程状态、线程同步和线程池等多个方面讲述了线程基础知识,希望大家对线程和线程池有了一个基础的了解。后面我们将继续深入多线程的其他知识,例如 Sychronized 的原理分析,JUC 工具类和 ThreadLocal 本地变量等知识,尽请关注.
因本人技术有限,如出现内容错误,请评论区纠正。码字不易,点个关注再走吧~ 。
最后此篇关于面试官:这就是你理解的Java多线程基础?的文章就讲到这里了,如果你想了解更多关于面试官:这就是你理解的Java多线程基础?的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在编写一个具有以下签名的 Java 方法。 void Logger(Method method, Object[] args); 如果一个方法(例如 ABC() )调用此方法 Logger,它应该
我是 Java 新手。 我的问题是我的 Java 程序找不到我试图用作的图像文件一个 JButton。 (目前这段代码什么也没做,因为我只是得到了想要的外观第一的)。这是我的主课 代码: packag
好的,今天我在接受采访,我已经编写 Java 代码多年了。采访中说“Java 垃圾收集是一个棘手的问题,我有几个 friend 一直在努力弄清楚。你在这方面做得怎么样?”。她是想骗我吗?还是我的一生都
我的 friend 给了我一个谜语让我解开。它是这样的: There are 100 people. Each one of them, in his turn, does the following
如果我将使用 Java 5 代码的应用程序编译成字节码,生成的 .class 文件是否能够在 Java 1.4 下运行? 如果后者可以工作并且我正在尝试在我的 Java 1.4 应用程序中使用 Jav
有关于why Java doesn't support unsigned types的问题以及一些关于处理无符号类型的问题。我做了一些搜索,似乎 Scala 也不支持无符号数据类型。限制是Java和S
我只是想知道在一个 java 版本中生成的字节码是否可以在其他 java 版本上运行 最佳答案 通常,字节码无需修改即可在 较新 版本的 Java 上运行。它不会在旧版本上运行,除非您使用特殊参数 (
我有一个关于在命令提示符下执行 java 程序的基本问题。 在某些机器上我们需要指定 -cp 。 (类路径)同时执行java程序 (test为java文件名与.class文件存在于同一目录下) jav
我已经阅读 StackOverflow 有一段时间了,现在我才鼓起勇气提出问题。我今年 20 岁,目前在我的家乡(罗马尼亚克卢日-纳波卡)就读 IT 大学。足以介绍:D。 基本上,我有一家提供簿记应用
我有 public JSONObject parseXML(String xml) { JSONObject jsonObject = XML.toJSONObject(xml); r
我已经在 Java 中实现了带有动态类型的简单解释语言。不幸的是我遇到了以下问题。测试时如下代码: def main() { def ks = Map[[1, 2]].keySet()
一直提示输入 1 到 10 的数字 - 结果应将 st、rd、th 和 nd 添加到数字中。编写一个程序,提示用户输入 1 到 10 之间的任意整数,然后以序数形式显示该整数并附加后缀。 public
我有这个 DownloadFile.java 并按预期下载该文件: import java.io.*; import java.net.URL; public class DownloadFile {
我想在 GUI 上添加延迟。我放置了 2 个 for 循环,然后重新绘制了一个标签,但这 2 个 for 循环一个接一个地执行,并且标签被重新绘制到最后一个。 我能做什么? for(int i=0;
我正在对对象 Student 的列表项进行一些测试,但是我更喜欢在 java 类对象中创建硬编码列表,然后从那里提取数据,而不是连接到数据库并在结果集中选择记录。然而,自从我这样做以来已经很长时间了,
我知道对象创建分为三个部分: 声明 实例化 初始化 classA{} classB extends classA{} classA obj = new classB(1,1); 实例化 它必须使用
我有兴趣使用 GPRS 构建车辆跟踪系统。但是,我有一些问题要问以前做过此操作的人: GPRS 是最好的技术吗?人们意识到任何问题吗? 我计划使用 Java/Java EE - 有更好的技术吗? 如果
我可以通过递归方法反转数组,例如:数组={1,2,3,4,5} 数组结果={5,4,3,2,1}但我的结果是相同的数组,我不知道为什么,请帮助我。 public class Recursion { p
有这样的标准方式吗? 包括 Java源代码-测试代码- Ant 或 Maven联合单元持续集成(可能是巡航控制)ClearCase 版本控制工具部署到应用服务器 最后我希望有一个自动构建和集成环境。
我什至不知道这是否可能,我非常怀疑它是否可能,但如果可以,您能告诉我怎么做吗?我只是想知道如何从打印机打印一些文本。 有什么想法吗? 最佳答案 这里有更简单的事情。 import javax.swin
我是一名优秀的程序员,十分优秀!