目录
1、认识线程
1.1概念
线程组成了进程,进程是操作系统分配资源的最小单元,而线程是操作系统调度的最小单位,一个线程就是一个执行流,每个线程有自己独立的代码,多个线程之间“同时”执行着多份代码。
1.2为什么要有线程
“并发编程”成为刚需,单核CPU的发展遇到瓶颈,想要提高算力,需要多核CPU(可以简单地理解为多个CPU组成),虽然多进程也能实现“并发编程”,但线程比进程更加轻量:
①创建线程比创建进程更快
②销毁线程比销毁进程更快
③调度线程比调度进程更快
※进程和线程的区别:
①从属关系:进程是由线程组成的,每个进程至少包含一个线程,即主线程
②共享资源的方式:同一个进程的线程之间共享一个内存空间
③描述侧重点不同:进程是系统分配资源的最小单位,线程是系统调度的最小单位。
④上下文的切换速度不同:线程上下文切换速度较快。
⑤操作对象不同:进程由OS调度操作,线程由程序员编码进行操纵。
※切记 线程也不是越多越好,如果创建的线程太多,可能会导致“狼多肉少”的情况,也就会造成恶意争抢和线程过度调用的问题,反而降低了效率。
2、线程的使用
2.1线程创建
2.1.1继承Thread 进行实现
/**
* 继承Thread 创建线程
*/
public class ThreadDemo3 {
public static void main(String[] args) {
//获得当前的线程
Thread mainThread = Thread.currentThread();
System.out.println("线程名称:"+mainThread.getName());
Thread thread = new MyThread();
//开启线程
thread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
//具体的业务执行代码
Thread thread = Thread.currentThread();
try {
Thread.sleep(600);//sleep 为Thread类中的静态方法 用类调用
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程的名称:"+thread.getName());
}
}
※ 不常用这种继承的用法,因为java有单继承局限,一旦继承了Thread类就无法继承其他类
2.1.2 实现Runnable 接口进行实现
①
/**
* 实现Runnable 接口新建线程
*/
public class ThreadDemo4 {
public static void main(String[] args) {
//创建Runnable
MyThread2 myThread2 = new MyThread2();
//创建一个线程
Thread thread = new Thread(myThread2);
//启动线程
thread.start();
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
//具体的业务代码
Thread thread = Thread.currentThread();//得到当前线程
System.out.println("线程执行:"+thread.getName());
}
}
②Runnable接口匿名内部类实现
/**
* Runnable 匿名内部类来创建
*/
public class ThreadDemo5 {
public static void main(String[] args) {
//匿名内部类
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//业务代码
Thread t = Thread.currentThread();
System.out.println("执行任务"+t.getName());
}
});
//启动线程
thread.start();
}
}
③ 使用Lambda 来创建 Runnable JDK8 以上推荐此种
/**
* 使用lambda 来创建 Runnable
*/
public class ThreadDemo6 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
//具体的业务
Thread t = Thread.currentThread();
System.out.println("任务执行"+t.getName());
});
//启动线程
thread.start();
}
}
2.1.3 Callable + Futrue
以上创建的方式有一个共同的问题,那就是没有返回值,也就是线程执行完成之后,主线程没有办法拿到新的线程执行结果的,所以有了如下方式:
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo9 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
//新线程执行的业务代码
String[] arrs = new String[]{"java","MySQL","Thread"};
//Random random = new Random();
//随机返回一个字符串
String result = arrs[new Random().nextInt(3)];
System.out.println(Thread.currentThread().getName()+"---字符串"+result);
return result;
}
});
//创建新线程
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
String result = futureTask.get();
System.out.println(Thread.currentThread().getName()+"--新线程的返回值"+result);
}
}
2.2常见的构造方法
①初始化线程名
②分组
import java.util.Random;
public class ThreadDemo12 {
public static void main(String[] args) {
// 1.创建一个线程分组(女子100米比赛)
ThreadGroup group = new ThreadGroup("thread-group");
// 2.定义一个公共的任务(线程的任务)
Runnable runTask = new Runnable() {
@Override
public void run() { // 业务(任务)
// 生成一个 1-3 秒的随机数
int num = (1 + new Random().nextInt(3));
// 跑了 n 秒之后到达了终点
try {
Thread.sleep(num * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 得到执行此方法的线程
Thread t = Thread.currentThread();
System.out.println(t.getName() + "——选手到达终点:" + num + "s");
}
};
// 3.线程(运动员)
Thread t1 = new Thread(group, runTask); // 创建选手 1
Thread t2 = new Thread(group, runTask); // 创建选手 2
Thread t3 = new Thread(group, runTask); // 创建选手 3
// 开跑
t1.start();
t2.start();
t3.start();
// 所有人全部到达重点之后宣布成绩
while (group.activeCount() != 0) {
}
System.out.println("宣布比赛成绩");
}
}
2.3线程的常用属性
①线程ID
②线程的名称
③线程的状态
所有线程的状态:
for(Thread.State item : Thread.State.values()){
System.out.println(item);
}
※线程状态的转变
线程状态(共计6种)
NEW 新建状态,当线程被创建,但是未启动之前的状态
RUNNABLE 运行状态 细化为:运行(得到时间片运行中状态)和就绪(未得到时间片就绪状态)两种状态
BLOCKED 阻塞状态(如果遇到锁,线程就会变为阻塞状态等待另一个线程释放锁)
WAITING 无限期等待状态
TIMED_WAITING 有限期等待状态
TERMINATED 销毁状态,当线程执行结束之后变为此状态。
④线程的优先级
※ 注意事项:同时启动多个线程,多个线程设置了不同的优先级,并不是优先级最高的就一定先执行完之后再执行低优先级的线程,而是高优先级的线程获取到CPU时间片的概率更多,整个的执行大致符合高优先级的线程先执行完。
2.4 线程的分类
2.4.1 守护线程
又称为后台线程,为用户线程服务的,当一个程序中所有的用户线程都结束之后,那么守护线程也会结束,获取当前线程是否为守护线程:thread.isDaemon()
true = 守护线程 false = 用户线程
2.4.2 用户线程
main 线程 默认为用户线程
※结论:
main线程(主线程)默认为非守护线程
在用户线程中创建子线程也是用户线程(默认情况下)
在守护线程中创建c
设置守护线程
判断守护线程
注意事项:线程的类型不能在线程的运行期间设置,也就是说线程的设置不能再start之后进行,必须在之前,否则JVM会报错。
守护线程vs用户线程
用户线程在java程序中相当重要,JVM一定要等所有的用户线程执行完之后才能自然结束,而守护线程是为用户线程服务的,所以当所有的用户线程执行完毕后不管守护线程是否在执行,JVM都会退出执行。
2.5 线程的常用方法
2.5.1 isAlive()
2.5.2 join()线程等待
等待某个线程执行完之后,再执行后续代码
与while(t.isAlive())相较,写法更加优雅,运行时所用的资源更少,用whileu循环等待的化,程序在一直执行。
2.5.3 interrupt()线程终止
a)使用自定义标识符来终止线程
b)使用interrupt()终止线程
配合 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 来使用
/**
* 使用 interrupt 终止线程
*/
public class ThreadInterrupet2 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("终止标志位:" +
Thread.currentThread().isInterrupted());
while (!Thread.interrupted()) {
// while (!Thread.currentThread().isInterrupted()) {
System.out.println("正在转账...");
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
e.printStackTrace();
// break;
// }
}
System.out.println("啊?险些误了大事!");
// System.out.println("终止标志位2:" + Thread.currentThread().isInterrupted());
// System.out.println("终止标志位2:" + Thread.currentThread().isInterrupted());
// System.out.println("终止标志位2:" + Thread.currentThread().isInterrupted());
// System.out.println();
System.out.println("终止标志位4:" + Thread.interrupted());
System.out.println("终止标志位4:" + Thread.interrupted());
System.out.println("终止标志位4:" + Thread.interrupted());
});
// 启动线程
thread.start();
Thread.sleep(100);
// 终止线程
thread.interrupt();
System.out.println("有内鬼,终止交易!");
}
}
※isInterrupted 和 Interrupted 的区别:
① interrupted 属于静态方法,所有程序都可以直接调用的全局方法;而isInterrupted 属于某个实例的方法,需要用实例来调用
②interrupted 在使用之后会重置标识符,而isInterrupted不会重置标识符
2.5.4 yield 让出CPU的执行权
yield 方法会让出CPU的执行权,让线程调度器重新调度线程,但还是有一定的机率再一次调用到出让CPU的线程上的,这一次它就会继续往下执行,因为yield 已经执行过了(不会再次yield)。
2.5.5 线程的休眠
①使用sleep
②使用TimeUnit
使用TimeUnit 可以更便捷,不用去计算,而使用sleep,单位只能是毫秒,如果需要休眠较长的时间单位还需要计算。
3、线程的安全问题※
程序在多线程的执行环境下,程序的执行结果不符合预期。
线程安全问题导致的原因:
3.1 抢占式执行
3.2 多个线程同时修改同一个变量
//预期的返回结果为0,一个进行++,一个进行--,
但如果开启多线程,最后返回的不为预期结果,
这时可以采取一下措施:1、两个线程分割开来,
一个执行完毕后再开启另一个;2、同时开启两个线程,
但这两个线程独立操作自己的变量。
public class ThreadDemo16 {
static class Counter {
// 变量
private int number = 0;
// 循环次数
private int MAX_COUNT = 0;
public Counter(int MAX_COUNT) {
this.MAX_COUNT = MAX_COUNT;
}
// ++ 方法
public int incr() {
int temp = 0;
for (int i = 0; i < MAX_COUNT; i++) {
temp++;
}
return temp;
}
// -- 方法
public int decr() {
int temp = 0;
for (int i = 0; i < MAX_COUNT; i++) {
temp--;
}
return temp;
}
public int getNumber() {
return number;
}
}
static int num1 = 0;
static int num2 = 0;
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(100000);
Thread t1 = new Thread(() -> {
// ++ 操作
num1 = counter.incr();
});
Thread t2 = new Thread(() -> {
// -- 操作
num2 = counter.decr();
});
// 启动多线程进行执行
t2.start();
t1.start();
// 等待两个线程执行完
t1.join();
t2.join();
// 打印结果
System.out.println("最终结果:" + (num1 + num2));
}
}
3.3 操作是非原子性问题
非原子性问题就是可拆分,不是一部操作,比如++/-- 操作:
线程前两步的加载和数据更新都正常进行,到了第三步写回CPU时,由于线程一得到了时间片,而线程二没得到时间片,因此线程一先将-1写进CPU,而稍后线程二得到时间片,继续执行,此时再写入CPU就覆盖了原先的-1的值,使结果变成了1,与预期结果不符。
3.4 内存可见性问题
可见性:一个线程对共享变量值的修改,能够被另一个线程看到。
※前置知识 java 内存模型(JMM):java虚拟机规范中定义了Java内存模型,⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果。
线程之间共享主内存,每一个线程都有自己的工作内存,当线程需要读取一个共享变量时,需要从主内存拷贝一份到自己的工作内存,进行更新后再写入主内存,但主内存中的数据不可见,所以可能导致线程一已经修改了主内存中的数据,而线程二不知道,又对其进行了修改。(与③很像)
3.5 指令重排序问题
JVM有一套自己的最优执行顺序(编译器优化),然后将写的代码顺序打乱,按自己的执行顺序来,导致与预期不符。