Java基础-多线程基础概念 (多线程的好处;创建线程的三种方式;多线程常用方法:sleep(long),wait(),notifyAll())
Java基础-多线程基础概念
前言:多线程系列拖到现在,也该复习下
这个东西怎么说呢,也是非常常见。而且面试基本上是必问的(好吧,这是我复习的主要目的)
一共预计会有6篇,这是第一篇
推荐书籍:通过博客来理解多线程总会缺失点什么,毕竟每个人写的博客水平有不同。而且有些信息被加工后可能并不适合你
《Java核心技术 卷Ⅰ》 这本书不止是线程,其实每章内容都不错。而且对Java水平有比较大的提升
《Java编程思想》 这本书对Java水平提升帮助很大,但是老实说,感觉多线程那一篇内容很全,但是文章很乱
《Java并发编程的艺术》 多线程相关鼎力推荐
还有种好的方式就是用IDEA,通过ctrl+B快捷键直接追溯源码。不需要一字一句的去理解源码(太费时间)。看输出删除,返回值。或者其他想知道的东西。而且能看到继承,实现关系。这种方式收益也挺大
多线程相关类或接口 :Thread Runnable Callable FutureTask
大纲脑图
1. 线程简介
本段简单介绍什么是线程,进程。线程的状态,状态转换,以及线程优先级
1.1 线程与进程
进程 是资源分配的最小单元,一个进程可以有多个线程
线程 是资源调度的最小单元,同一个进程下,各个线程的内存质押共享,所以线程间的通信代价小。线程也称为轻量进程
1.2 为什么使用多线程
- 对于多CPU,多核的来说,多线程能提高程序响应速度。相当几个人做同时开工,肯定比一个人快。(但是这个也不一定,因为线程调度,分配资源都需要开销。就像一个项目组5-10个人效率,可能比20-100个人效率更高,因为开会,信息共享都需要大量时间)
- 对于单CPU单核机器(这种情况可能更常见)来说,因为有阻塞的存在,多线程效率能够更好。主要是磁盘太慢,如果是单线程,可能磁盘在007,CPU却大多数时间在喝茶。如果运行在内存中,单线程可能会更好,没有上下文切换的额外开销(例如 Redis)
- 有些场景,多线程实现起来简单。例如游戏,不同的角色同时在做不同的事情,用单线程就很难处理这种情况
单线程相当于顺序结构,一次做一件事,这件事情做完了(即使这件事中途可能要等待好久)才能做另外一件事情。
多线程相当于在一个时间段内,做几件事情
有些时候多线程好:一般是有些事情等待时间长,可以中途做其他事情
有些时候单线程好:例如我想8个小时写3篇博客,肯定是单线程好,如果中途切换着写,可能思路就没了
1.3 线程状态
Java的线程状态和操作系统的不一样
Java线程转换,其实我觉得理解其每个状态的意思就自然知道装换规则了。不用可以去记忆,没必要
- New 新建状态
- Runnable 可运行,代表线程可以被运行,可能正在运行,也可能等待时间片的过程中
- Waiting 等待 因为调用了wait而暂停。注意与Blocked区分
- Time_Waiting 计时等待,调用了sleep和wait后暂停,等待固定的时间后,进入可运行状态
- Blocked 阻塞 因为缺少某些资源*不能运行
- Terminated 终止状态 线程完成,或者说某些原因线程挂了(功成身退与中途陨落)
1.4 线程优先级
Java线程可以通过 getPriority()与setPriority() 获取和设置线程优先级
Java线程有10个优先级。默认为5。优先级是有操作系统觉得的,其实在Java程序设置了优先级,也没什么卵用,因为操作系统不认(3月份HB的绿码,BJ也不认啊)
windows有7个优先级
1.5 守护线程
守护线程,在《Java编程思想》里叫后台线程,为其他线程服务的线程,例如垃圾回收线程
可以通过 setDaemon()将线程设置为守护线程
2. Java线程相关操作
2.1 Java创建线程的三种方式
方式1: 继承Thread类,重写run()方法来创建线程
// 创建线程方式一:继承Thread类,重写Run方法
class ThreadChild extends Thread{
@Override
public void run() {
super.run();
int length = 100;
Thread.currentThread().setName("方式一:继承Thread方式实现线程");
for (int i = 0; i < length; i++){
System.out.println("这是以方式壹实现的线程,循环体i的值为" + i);
}
}
}
方式2: 实现Runnable接口,重写run()方法
// 创建线程方式二:实现Runnable接口,重写Run方法
class ThreadRunnable implements Runnable{
@Override
public void run() {
int length = 300;
Thread.currentThread().setName("方式二:实现Runnable接口方式实现线程");
for (int i = 200; i < length; i++){
System.out.println("这是以方式二实现的线程,循环体i的值为" + i);
}
}
}
方式3: 实现Callable接口,重写call()方法
// 创建线程方式三:实现Callable接口,重写Call方法
class ThreadCallable implements Callable{
@Override
public Object call() throws Exception {
int length = 900;
Thread.currentThread().setName("方式3:实现Callable接口方式实现线程");
for (int i = 800; i < length; i++){
System.out.println("这是以方式三实现的线程,循环体i的值为" + i);
}
// 我没有什么需要返回的,有点尴尬
return null;
}
}
三种方式对比
方式一-继承Thread类的方式尽量少用,一般来说继承太过于重量级(继承子类用于所有父类公有的域,方法),实现接口的方式比继承好。
一般用方式二-实现Runnable接口的方式。如果有返回值就用方式三实现Callable接口
Runable和Callable接口都只有一个方法run()。实现Callable接口的方式有返回值。而且需要和FutureTask 配合使用
需要注意的是,启动线程用的start(),不是调用run方法 如果调用run就是调用普通方法
全部类代码
package com.java.basic.multithreading;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* @author dengtiantian
*/
public class ThreadTest {
// 创建线程方式一:继承Thread类,重写Run方法
class ThreadChild extends Thread{
@Override
public void run() {
super.run();
int length = 100;
Thread.currentThread().setName("方式一:继承Thread方式实现线程");
for (int i = 0; i < length; i++){
System.out.println("这是以方式壹实现的线程,循环体i的值为" + i);
}
}
}
// 创建线程方式二:实现Runnable接口,重写Run方法
class ThreadRunnable implements Runnable{
@Override
public void run() {
int length = 300;
Thread.currentThread().setName("方式二:实现Runnable接口方式实现线程");
for (int i = 200; i < length; i++){
System.out.println("这是以方式二实现的线程,循环体i的值为" + i);
}
}
}
// 创建线程方式三:实现Callable接口,重写Call方法
class ThreadCallable implements Callable{
@Override
public Object call() throws Exception {
int length = 900;
Thread.currentThread().setName("方式3:实现Callable接口方式实现线程");
for (int i = 800; i < length; i++){
System.out.println("这是以方式三实现的线程,循环体i的值为" + i);
}
// 我没有什么需要返回的,有点尴尬
return null;
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.testThread();
}
// 测试
public void testThread(){
ThreadChild threadChild = new ThreadChild();
ThreadRunnable threadRunnable = new ThreadRunnable();
ThreadCallable threadCallable = new ThreadCallable();
//方式一开始运行
threadChild.start();
//方式二开始运行
(new Thread(threadRunnable)).start();
//方式三运行
FutureTask<Object> task = new FutureTask<>(threadCallable);
(new Thread(task)).start();
}
}
2.2 线程相关常用方法
去看源码或者API最自在,我这个也是从里面复制的
2.2.1 Object线程相关方法
- public final void wait() throws InterruptedException 暂停线程 会释放锁
- public final native void notify();随机唤醒一个线程
- public final native void notifyAll() 唤醒所有线程
这三个方法都是Object的,我记得有个很常见的问题是:wait和sleep的区别
这个是利用每个对象本身自带的锁实现的。都是本地方法
notify()这个方法其实用的少,因为是随机唤醒一个线程,他自己也不知道唤醒的谁
2.2.2Thread相关常用方法
- getXXX系列方法
- setXXX系列方法
- public void interrupt() 中断 试图停止线程
- public static boolean interrupted()测试线程是否被中断
- public synchronized void start() 启动线程
- public static native void sleep(long millis) throws InterruptedException 休眠,不会释放锁
public final void stop() 停止线程,已被放弃public final void suspend()暂停线程,已被放弃public final void resume() 唤醒线程,已被放弃- public static native void yield() 让出锁和时间片,如果他的优先级还是最高,那么他还会抢到锁
3. 线程间协作
其实这个标题当时其实是线程间的通信,后面改成同步。最后是这个
反正这节 目的是为了表达线程之间工作关系
重复:线程是共享内存,所以通信代价很低,但是这也有问题。
举个例子:父亲剥柚子,儿子吃柚子。这两个动作是可以同时进行的(在已有剥好的前提下。) 儿子吃柚子得在有柚子可以吃的前提下,就是父亲有剥好的柚子。就是得剥好才能吃(你可不要说不剥儿子就吃,我敬你是条汉子)。本节就是说明线程的协作关系。
以及还有父亲,儿子都吃瓜子。两种是竞争关系,得有瓜子才行
3.1 Synchronized和Volatile机制
Synchronized 是通过对某个共享的资源加锁,是这个资源一次只能被一个资源消耗者使用(利用对象锁,下章节详细写)
Volatile相当于,对某个资源的操作一旦发生,马上所有关注这个资源的用户都知晓(内存模型里讲最好)
3.2 等待通知机制
利用wait和notifyAll方法,对于有一定先后关系的操作,一旦 后者先执行了,马上wait,然后先者,执行后notifyAll唤醒后者消费
3.3 使用管道通信
我不记得Java管道知识点了,有点尴尬。后续补上
聊下Redis的管道和操作系统的管道缓解下尴尬
Redis的管道有点类似于JDBC里面的addBatch()和累积发送(数据攒到一定量在传输)。就是说Redis的管道为了提高效率,将多条命令攒到一起提交给服务器执行
Linux的管道相当于提供一个中间文件,线程或进程将数据放到管道,然后另外的线程或进程从管道获取数据。其实也是表明了一种先后的顺序,虚拟机里面happen-before那味儿。
3.4 ThreadLocal的使用
这个其实线程有些数据不想共享给别的线程,就相当于局部变量。知道这个我觉得就好了,其实没什么特殊的,具体用法用的时候查看具体的API或者看源码(ctrl+b)就好了。我不明白为什么很多有很多文章特意提这个
4. 小结
- 什么是线程,线程与进程的区别
- 多线程的好处。多线程一定快吗?(需要知道上下文切换是有消耗的)
- 创建线程的三种方式
- 线程相关的方法
总目录:Java进阶之路-目录
天高地迥,觉宇宙之无穷。兴尽悲来,识盈虚之有数
《滕王阁序》 王勃
博主:五更依旧朝花落
首次发布时间:2020年4月18日17:24:53
末次更新时间: