1.认识线程
线程的概念:
一个线程就是一个”执行流”,每一个线程之间都可以按照顺序执行自己的代码,多个线程之间”同时”执行多份代码
进程包含线程
一个进程可以包含一个线程或多个线程(多个线程可能是多个CPU核心上同时运行, 也可能在一个CPU核心上,通过快速调度,进行运行)
每个线程是独立的执行流,多个线程之间也是并发执行的
线程是操作系统调度运行的基本单位
进程是操作系统资源分配的基本单位
一个进程的多个线程之间,共用一份系统资源(内存空间 , 文件描述符)
2.Thread及常用方法
五种创建线程的方法
a.使用继承Thread,重写run方法
class MyThread extends Thread {
@Override
//run方法可以称为线程的入口方法
public void run() {
while(true) {
System.out.println("hello thread");
try {
//sleep 是 Thread 的一个静态方法
Thread.sleep(1000);
//休眠过程中,中途唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//调用操作系统的api,创建新线程,新线程里调用了t.run
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
多个线程在CPU的调度执行顺序上时随机的(打印顺序不确定)
b.使用实现runnable接口,重写run方法
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
c.继承Thread 使用匿名内部类
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
thread.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
d.实现Runnable,使用匿名内部类
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
e.使用Lambda 表达式(推荐)
Thread t = new Thread(() -> {}
()里面放参数 只有一个参数可以省略
{} 里面写java 代码, 只要一行也可以省略
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
Thread类里的其他一些方法
start() : 真正从系统创建一个线程,新线程执行run方法;
run() : 表示线程的入口方法,线程启动执行什么逻辑(交给系统去自动调用)
中断一个线程
public class ThreadDemo9 {
public static boolean isQuit;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!isQuit) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程终止");
});
t.start();
isQuit = true;
}
}
上述使用自己创建的变量来控制循环
而java中 Thread类内置了一个标志位来控制循环
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
//currentThread 获取当前线程实例,此处得到的对象就是t;
//isInterrupted 就是t对象自带的一个标志位
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
break;
}
}); //或者 }, "Thread_0"); 可以自定义线程名;
//开始线程
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//把t内部标志位设置成true;也可以唤醒sleep
t.interrupt();
}
}
interrupt方法的作用:
1.设置标志位为true
2.如果该线程正在阻塞(比如执行sleep)此时就会把阻塞状态唤醒,通过抛出异常让sleep立即结束
(sleep 被唤醒后会自动把标志位清空(true 变为 false)使得下次循环可以继续执行)
sleep被唤醒清空标志位的目的:让线程自身对线程何时结束有一个明确的控制
并不是立即中断(用代码灵活控制)
join() -等待一个线程
线程之间并发执行,操作系统对线程的调度是无序的
无法判断谁先谁后
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1");
}
});
t.start();
t.join();
System.out.println("2");
}
在main线程调用t.join
表示让main 线程等待 t 线程先结束再往下执行(保证t先结束)
join() 还可以添加参数表示最大等待时间
3.线程的状态
如何查看线程
在你安装的java jdk bin 目录中
使用管理员模式打开jconsole.exe;
先运行你所编译的线程
直接点击不安全的连接
线程的六个状态
4.多线程带来的线程安全问题*
本质上是因为多线程的调度顺序是不确定的(抢占式执行)
线程不安全的原因
a.抢占式执行
b.多个线程修改同一个变量
c.修改操作不是原子的
举例说明
public class ThreadDemo10 {
static class Countor{
public int count = 0;
public void add () {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Countor countor = new Countor();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 6000; i++) {
countor.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 6000; i++) {
countor.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(countor.count);
}
}
结果并不是预期的 12000
出现这种情况的原因要和线程的随机调度相关
countor.add() 这个方法本质是三个CPU指令
Load: 加载 把内存中的数据加载到CPU寄存器上;
Add: 在寄存器上进行count++;
Save:保存 把寄存器的值保存回到内存当中;
每次加法操作的三个指令都是随机的
有可能数据还没有Save, 另一个线程就读取到了未修改的值(类似于事务脏读问题)
如何解决这一问题
我们可以把countor.add() 变为原子的
引入了加锁 后续说明
d.内存可见性引起的
(多线程环境下,由于编译器对代码的优化,而产生了bug)
举例说明
import java.util.Scanner;
public class ThreadDemo11 {
public static int a = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (a == 0) {
}
System.out.println("线程1循环结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数");
a = scanner.nextInt();
});
t1.start();
t2.start();
}
}
目的是输入一个非零,打印"线程1循环结束";
结果显示
显然结果不符
这就是由内存可见性导致的
while (a == 0) { }
此时CPU存在
Load加载指令:从内存读取数据到CPU寄存器,
Compare比较指令:比较寄存器的值是否是零
而读取寄存器是比读取内存快了几个数量级的 (每秒执行上亿次)
此时编译器就认为Load开销大,而每次Load读的结果(0)都一样,编译器就会把Load优化了,只保留第一次读取的Load(0),后面改的就没有用了(a = scanner.nextInt())
t1线程频繁读取主内存效率较低,就被优化成直接读取直接的工作内存(Compare寄存器),
t2线程修改了主内存的结果,但是t1线程没有读取主内存,导致修改不能被识别
为了解决上述问题引入了volatile(不稳定)关键字 后续说明
e.指令重排序引起的
指令重排序也是有关编译器的优化
保证整体逻辑不变的情况下,调整代码的执行顺序,使得程序变得更加的高效;
(多线程环境下也不好说了)
5.加锁
适用场景多个线程写
java中利用关键字:synchronizedj 进行加锁操作
public class ThreadDemo10 {
static class Countor{
public int count = 0;
public void add () {
synchronized (this) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Countor countor = new Countor();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 6000; i++) {
countor.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 6000; i++) {
countor.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(countor.count);
}
}
a.代码块进行加锁
b.类进行加锁
(如果修饰的是static方法,就是给类对象进行加锁)
此时当t1线程进行countor.add()的时候
t2线程就会进入阻塞等待
保证了++操作变为了原子的,解决了这个问题
加锁本质就是把并发变成串行
当然多个线程对同一个锁对象加锁就会产生锁竞争问题
6.volatile关键字
适用场景 一个线程读 一个线程写
被volatile 修饰的变量,编译器就会知道它是不稳定的就会禁止优化,每次都是从内存上读取数据
import java.util.Scanner;
public class ThreadDemo11 {
volatilepublic static int a = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (a == 0) {
}
System.out.println("线程1循环结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
a = scanner.nextInt();
});
t1.start();
t2.start();
}
}
a被volatile 修饰后t1线程就可以正常退出来了
7.wait and notify
线程的调度顺序是无序的(某些需求下希望线程有序)
wait:让线程等一等;
notify: 通知线程起来了,继续执行
wait 和 notify 都是object类的方法只要是个类对象(基本数据类型除外)都可以使用
wait 必须写到synchronized 代码块里面
线程中wait做的事情:
a.是当前执行的线程进行等待
b.解开锁
c.等待被唤醒,尝试重新获取锁
public class ThreadDemo12 {
public static void main(String[] args) {
Object block = new Object();
Thread t1 = new Thread(() -> {
synchronized (block) {
try {
System.out.println("开始等待");
block.wait();
System.out.println("等待结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (block) {
System.out.println("开始通知");
block.notify();
System.out.println("结束通知");
}
});
t2.start();
}
}
8.面试题
进程和线程区别*
1.进程包含线程
2.进程有自己独立的内存空间和文件描述符,同一个进程的多个线程之间,共享同一份地址空间和文件描述符
3.进程是操作系统资源分配的基本单位,线程是操作系统的调度执行的基本单位
4.进程之间具有独立性,一个进程挂了不影响另一个进程;而同一个进程的多个线程之间,一个线程挂了,可能把整个进程带走,影响其他的线程
方法重载和重写的区别
相容:父类返回值的派生类(子类)
run 和 start 的区别
start() : 真正从系统创建一个线程,新线程执行run方法;
run() : 表示线程的入口方法,线程启动执行什么逻辑(交给系统去自动调用)
java如何编译的
执行java程序的前提要把类加载出来
.java源代码文件 通过javac 编译成.class 字节码文件使用java命令来运行
JVM就可以执行.class文件(把文件内容读取到内存中 类加载) 解析并且在内存中构造出对应的类对象
wait 和 sleep 的区别
wait需要搭配synchronized使用,而sleep不需要
wait 是Object的方法,而sleep是Thread的静态方法