学习目录
前言
很久之前就听过互联网架构中有三高,高可用、高并发、高性能,多线程是处理高并发问题的基石,起步阶段一定要对线程有一个系统深刻的印象,为以后做准备
一、进程与线程
线程(Thread):一个程序内部的一条执行路径。
进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,进程由线程创建,线程的容器也是程序的实体
在我们平时使用IDEA的过程中,执行完一段程序,运行框最后都会弹出这样的一段话,表示这个进程结束标志着程序退出
二、线程的创建
在Java中通过java.lang.Tread来代表线程,按照面向对象的思想,Tread类必须给咱提供实现多线程的方式,所以对于程序员来说就派生了以下几种实现方式
1.继承Thread类
就像这样:
public static void main(String[] args) {
MyThread m1 = new MyThread();
m1.start();//启动线程
}
class MyThread extends MyThread{...}
:在Java面向对象中的继承是单继承模式,通过上述方式创建线程虽然编码简单,但是会导致无法继承其他类,不利于扩展
2.实现Runable接口
就像这样
public static void main(String[] args) {
MyThread m1 = new MyThread();
new Thread(m1).start();//实现接口启动线程的方式
}
class MyThread extends MyThread{...}
:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性更强
:多了一层对象包装,如果线程有执行结果是不可以直接返回的
3.匿名内部类实现
本质还是实现接口 ,只不过是一种简写形式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过匿名内部类方式启动的线程");
}
}).start();
4.实现Callable、FutureTask接口
三、线程创建的本质(🚩)
通过上述三种方式,不难发现都需要通过start()方法启动线程,然后执行重写的run()方法,大家千万不要以为实现线程效果的是run()方法啊,他就是一个普通的不能再普通的方法,我们只是根据规则重写了一下,接口的目的是用来规范,这里的重写run()方法就是一种规范性的体现,甚至你去利用ctrl+B查看它的源码结果都是本地找不到用法
其实,而不是run(),start0()方法才是本地方法,是JVM机在调用,底层是通过C++来实现的,感兴趣的可以去debug追一下源码private native void start0();
四、Thread常用API、构造器
1.API: 比较常用的有,获取线程名称——getName()、设置名称——setName()、获取当前线程对象currentThread()、启动线程——start()、终止线程——interrupt()
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello world!");
System.out.println("当前线程名称:"+Thread.currentThread().getName());
}
}
运行结果:
Hello world !
当前线程名称:Thread-0
2.线程休眠——sleep(),让线程休眠指定时间(单位是毫秒)
Thread.sleep(1000);
3.构造器
五、用户线程与守护线程
Java线程主要分为和
理解这两个概念通过顾名思义的方式再适合不过了,用户线程是一个用户,守护线程是一个守护者时时刻刻守护用户,当用户程序执行完毕守护者就默默退出
最常见的:main线程退出后,里面的子线程还在运行,当开启守护线程后,子线程会随着main线程的结束而退出
创建守护线程只需通过该对象来调用setDaemon(true)实现即可
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户线程在运行");
}
});
thread.setDaemon(true); //开启守护线程
thread.start();
Thread.sleep(1000);
System.out.println("main线程结束~");
}
如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了要守护的人,守护线程没有工作可做,也就没有继续运行程序的必要了
守护线程开启前后对比:
六、线程安全
当多个线程同时操作同一个共享资源的时候可能出现业务安全问题,称为线程安全
生活中取钱就是一个经典的线程安全问题
懒羊羊、美羊羊同时去取沸羊羊的钱,两个线程同时进行都符合判断条件,结果银行还亏了十万,这就是线程安全引起的生产事故
线程安全问题出现的原因是因为存在多线程并发、同时访问共享资源存在修改共享资源的动作
因此我们需要使线程同步!
七、加锁实现线程同步(🚩)
1.同步代码块
:把出现线程安全问题的核心代码给上锁
:每次只能一个线程进入,执行完毕后自动解锁,其他线程进来执行
synchronized(同步锁对象){
操作共享资源的代码(核心代码)
}
2.同步方法
:把出现线程安全的核心方法给上锁
:每次只能一个线程进入,执行完毕后自动解锁,其他线程再来执行
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码(核心代码)
}
给对象上锁之后可以实现线程同步就不会出现多个线程同时访问同一个共享资源的情况(高速路上的收费站就可以抽象成一把锁)
:同步也有相应的局限性,会导致程序的执行效率降低,因为单位时间内启动的线程变少了
八、线程死锁
,但不肯相让,导致了线程死锁
模拟一个场景:
代码模拟:
class DeadLockDemo extends Thread {
static Object o1 = new Object();//资源1
static Object o2 = new Object();//资源2
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (o1) { //对象互斥锁, 下面就是同步代
System.out.println(Thread.currentThread().getName() + " 进入 1");
synchronized (o2) { // 这里获得 li 对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入 2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " 进入 3");
synchronized (o1) { // 这里获得 li 对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入 4");
}
}
}
}
- 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
- 如果线程 A 得不到 o2 对象锁,就会 Blocked
- 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
- 如果线程 B 得不到 o1 对象锁,就会 Blocked