目录
1、Java线程的实现方式
(1)方式一:使用Thread类或继承Thread类
// 方式1:使用Thread
Thread thread = new Thread() {
public void run() {
//要执行的任务
System.out.println("通过new Thread方式执行任务");
}
};
thread.start();
(2)方式二:实现 Runnable 接口
配合Thread 把【线程】和【任务】(要执行的代码)分开:
// 方式2:使用Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("通过Runnable方式执行任务");
}
};
new Thread(runnable).start();
(3)方式三:使用有返回值的 Callable
public class CallableTask implements Callable {
@Override
public Object call() throws Exception {
return new Random().nextInt();
}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());
System.out.println("通过Callable方式执行任务,返回结果:" + future.get());
但本质上,Java创建线程只有一种方式,就是new Thread(),然后调用start()方法。不论是继承Thread重写run()方法,还是直接实现Runnable接口,本质都是一样的,因为Java在创建线程后都会去调用 Runnable.run() 方法执行任务。定义一个 Runnable(new Thread) 和把 Runnable 作为参数传递(接口方式)并没有什么区别。
线程池方式,最终也是使用 DefaultThreadFactory 创建线程
2、Java线程创建原理详解
对比三种执行任务方式的区别
public class ThreadExecuteTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "通过Runnable方式执行任务");
}
};
// 三种调用方式的区别
// 方式一:使用start() -> 操作系统创建线程
// java Thread(映射) --> jvm JavaThread(JVM虚拟机) ----> os Thread(具体操作系统)
new Thread(runnable, "线程1").start();
// 方式一:new Thread(),然后直接调用run() -> 普通对象的方法调用
new Thread(runnable, "线程2").run();
// 方式三:直接调用 -> 普通对象的方法调用
runnable.run();
}
}
执行结果
从结果看出,只有通过 start() 调用任务,才会真正的创建线程,其他方式都是普通的对象方式调用。进入 start() 方法的源码,方法中调用了本地方法 start0()
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); // 调用start0()
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// JVM方法start0()
private native void start0();
对于底层 start0() 方法的执行流程,流程图如下: // 很重要,创建线程是操作系统进行的
线程创建和启动流程总结:
- 使用 new Thread() 定义一个线程对象,然后调用 start() 方法进行 Java 层面的线程启动
- 调用本地方法 start0(),然后会去调用JVM中的JVM_StartThread方法进行线程创建和启动
- 调用 new JavaThread() 会进入内核模式,由底层操作系统进行线程创建
- 底层操作系统新创建的线程为Initialized装态(初始化),调用 sync -> wait() 方法进行等待,直到被唤醒才会执行 thread -> run();
- 回到JVM层,会将Java中的Thread和JVM中的Thread进行绑定
- 调用Thread::start() 方法进行线程启动,并将线程状态设置成RUNNABLE,接着调用OS::start_thread() 根据不同的操作系统选择不同的线程启动方式
- 线程启动后状态设置成RUNNABLE,唤醒等待线程,执行 thread -> run() 方法
- JavaThread::run() 方法会回调 new Thread 中复写的 run() 方法
所以,Java创建线程会涉及到内核模式的调用,非常消耗CPU资源,应该尽量让Java线程能够得到复用,减少不必要的重复创建。
Java线程属于内核级线程,JDK1.2 基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线 程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。
3、什么是协程?
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特 性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。// go语言有协程机制
![](https://i-blog.csdnimg.cn/blog_migrate/e1fa9ba0151f7c23c9e0769ca052efc2.png)
协程的特点在于是一个线程执行。那么和多线程比,协程有何优势?
- 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
- 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
- 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
注意: 协程适用于被阻塞的,且需要大量并发的场景(网络IO),不适合大量计算的场景。
4、Java线程调度机制
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调 度和抢占式线程调度
(1)协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
(2)抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield() 可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。// 避免线程阻塞
Java线程调度是抢占式调度,如果希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。
Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至 Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。
一个简单的小测试:
public class SellTicketTest implements Runnable {
/*** 车票 */
private int ticket;
private static Map<String, Integer> threadMap = new ConcurrentHashMap<>();
public SellTicketTest() {
this.ticket = 1000;
}
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
if (ticket > 0) {
try {
// 线程进入暂时的休眠
Thread.sleep(2);
String threadName = Thread.currentThread().getName();
if (threadMap.get(threadName) == null) {
threadMap.put(threadName, 1);
} else {
threadMap.put(threadName, threadMap.get(threadName) + 1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 计算余票
ticket--;
}
}
Thread.yield();
}
}
public static void main(String[] args) throws InterruptedException {
SellTicketTest ticketTest = new SellTicketTest();
Thread thread1 = new Thread(ticketTest, "thread1");
Thread thread2 = new Thread(ticketTest, "thread2");
Thread thread3 = new Thread(ticketTest, "thread3");
Thread thread4 = new Thread(ticketTest, "thread4");
// priority优先级默认是5,最低1,最高10
// 高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上讲高优先级的线程在高概率下被执行,
// 并不意味着只有当高优先级的线程执行完之后,低优先级的线程才执行。
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MIN_PRIORITY);
thread3.setPriority(Thread.MIN_PRIORITY);
thread4.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
Thread.sleep(5000); // 保证程序执行完
Integer sum = 0; // 统计总数
for (Map.Entry<String, Integer> entry : threadMap.entrySet()) {
System.out.println("线程名称:" + entry.getKey() + ",调度次数:" + entry.getValue());
sum += entry.getValue();
}
System.out.println("总共次数:" + sum);
}
}
以上结果表明,Java优先级并不是很靠谱,如果多次执行,获得的结果也不同。