6.13 多线程(1)
内部类
所谓的内部类就是将一个类嵌套到另一个类的内部的操作,可以把内部类当成普通类看待。
现实生活中:心脏和人体的关系,心脏也有很多属性和方法,心脏之于人体就是一个内部类、发动机和汽车之间的关系,发动机有很多属性和方法,发动机之于汽车就是一个内部类。
把类套在另一个类内部原因:内部类的设计也是一种封装的思想,封装体现的就是保护性和易用性,心脏封装到人类的内部->保护,发动机封装到汽车的内部=》易用。
内部类存在的原因
-
内部类和外部类可以方便的访问彼此的私有域(属性和方法)
-
内部类可以对外部类的的外部完全隐藏(把内部类就当作外部类的属性来看待),内部类可以使用private修饰
-
内部类可以变相实现多继承(很少使用)
class A{} class B{} class C{ class InnerA extends A{} class InnerB extends B{} }
变相实现多继承,不推荐使用。
内部类种类
成员内部类
-
在外部类的内部不适用static关键字定义的内部类就是成员内部类,类比成员属性和成员方法,成员内部类需要依赖外部类对象,先有外部类对象,才有内部类对象。
需要依赖外部类对象才能存在。
关于外部类的内部类产生对象的两种方式:
-
在外部类的内部产生成员内部类对象:在Outter的内部产生Inner对象,就和普通类没啥区别
内部类名称 内部类引用 = new 内部类();
-
若内部类对外部可见(不是private修饰的内部类)
在外部类的外部创建内部类对象
外部类名称.内部类名称 内部类引用 = new 外部类().new 内部类();
Outter.Inner inner = new Outter().new Inner();
-
-
成员内部类需要依赖外部类的对象,先产生外部类对象,然后产生内部类对象 - 类比成员属性或成员方法,成员内部类可以直接访问外部类对象
-
成员内部类可以访问外部类中的静态域,不可以在当前内部类中自己定义静态域。(因为有成员内部类就一定有外部类的对象,静态域不需要对象,所以成员内部类中没有静态域)
静态内部类
使用static关键字定义在另一个类内部的类,=》静态方法或属性
和成员内部类最大的区别:静态内部类不需要外部类的对象,和外部类是一个相对独立的关系,只是套在外部类的内部定义而已
静态内部类只能访问外部类的静态域,没有外部类对象,无法直接访问外部类的成员域(需要在内部创建对象访问)
静态内部类中可以定义自己的成员域(静态内部类就是个普通类,只不过套在一个类的内部而已)
方法内部类
匿名内部类 (lambda表达式)
小结
成员内部类可以访问外部类的所有域(成员,静态),因为成员内部类依赖外部类的对象;不能拥有自己的静态域。
静态内部类可以拥有自己的成员域,但是不能直接访问外部类的成员域。
6.15 多线程(2)
内部类
成员内部类和静态内部类都是定义在类中。
方法内部类和匿名内部类都定义在方法中。
方法内部类
定义在方法中的类
-
不能使用任何权限修饰符(这个类出了方法就没了)
-
对外部完全隐藏(外部类的外部)
-
Test内部类要使用fun方法的形参或者局部变量,该变量必须为隐式的final声明
方法内部类要使用外部方法中的形参或者局部变量,只有读取权,无法修改
-
若方法内部类中读取了方法中的局部变量(形参或者局部变量),这个局部变量就无法修改(不仅在类中无法修改,在方法中也不能再改),如果在方法内部类中使用过一个该方法的局部变量,那么该变量为隐式的final声明(编译之后默认加上final)
-
在方法内部类中无法定义static域
匿名内部类
匿名内部类也是方法内部类的一种,最多用在方法形参中
定义在方法中(方法的形参或实参)-实参用到最多,没有任何权限修饰符,甚至连类名称都没有的内部类。
package inner_class;
/**
* 匿名内部类
* @author kurumi
* @date 2022/6/20 21:36
**/
public class NoNameClass {
public static void main(String[] args) {
//匿名内部类实现了IMsseage接口
fun(new IMessage() {
//覆写了getMsg抽象方法
@Override
public void getMsg(String msg) {
}
});
}
public static void fun (IMessage message) {
message.getMsg("测试匿名内部类");
}
}
interface IMessage {
void getMsg(String msg);
}
接口无法直接实例化对象,所以此处其实new的是IMessage接口的子类,只不过这个子类只在fun方法中使用一次。
Lambda表达式
-
JDK8 - 2008年,大数据最火热的几年
Hadoop Map Reduce 里面就是各种Lambda表达式的使用
函数式编程思想 =》Scala
Java这种面向对象的定义太繁琐,在处理各种数学运算中需要频繁定义类,对象,方法等
-
在接口中引入了default普通方法 =》JDK8之后接口才有的,JDK8之前,接口中只有全局常量和抽象方法
/** * @author kurumi * @date 2022/6/21 5:44 **/ public interface NewInterface { void test(); default void test1() { System.out.println("接口中的普通方法"); } } class InterfaceImpl implements NewInterface { @Override public void test() { } }
不常用,一般都是用来修正上古版本接口中拓展方法使用的。
default关键字在接口中表示普通方法,不能省略,不写default就是抽象方法。
-
函数式接口
一个接口有且只有一个抽象方法,这种接口称之为函数式接口。
使用@FunctionalInterface检查当前接口是否是函数式接口。
@FunctionalInterface //检查当前接口中是否只包含一个抽象方法(有且只有一个) public interface FuncInterface { void test(); default void test1() { System.out.println("这是普通方法");//普通方法不影响函数式接口的定义 } }
Lambda表达式的前身=》匿名内部类
@FunctionalInterface
interface FuncInterface {
void test();
}
public class LambdaTest {
public static void main(String[] args) {
//1.调用fun方法
//2.实参由一个匿名内部类实现了FunInterface接口
fun(new FuncInterface() {
//3.实现接口中的test方法
@Override
public void test() {
System.out.println("匿名内部类实现了FuncInterface接口");
}
});
}
public static void fun(FuncInterface fi) {
fi.test();
}
}
转换为Lambda表达式
public static void main(String[] args) {
fun(() -> {
System.out.println("Lambda表达式实现了FuncInterface接口");
});
}
()代表test(),-> 代表对test方法进行实现。
Lambda表达式把匿名内部类中多余的new方法定义等全部省略,只保留方法的参数和方法体的实现。
能使用Lambda表达式的前提是接口必须是函数式接口,只能有唯一的抽象方法。
Lambda表达式的四种情况
对应抽象方法的四种分支
-
无返回值无参数
public class AllLambda { //双无的对象 public static void main(String[] args) { // NoParaNoReturn doubleNo = () -> { // System.out.println("无返回值,无参数的接口对象"); // }; NoParaNoReturn doubleNo = () -> System.out.println("无返回值,无参数的接口对象"); doubleNo.test(); } } interface NoParaNoReturn { void test(); }
规则1:若方法体只有一行代码,可以省略{}
-
无返回值有参数
public class AllLambda { public static void main(String[] args) { //方法实现 方法体代码 // HasParaNoReturn hasParaNoReturn = (int x) -> { // x += 20; // System.out.println(x); // }; HasParaNoReturn hasParaNoReturn = x -> { x += 20; System.out.println(x); }; hasParaNoReturn.test(10); } } interface HasParaNoReturn { void test(int a); }
规则2:若方法的参数只有一个,则可以省略小括号
规则3:可以省略Lambda表达式中参数的类型,若省略则都需要省略
-
有返回值无参数
public class AllLambda { public static void main(String[] args) { // NoParaHasReturn noParaHasReturn = () -> { // int a = 10; // int b = 20; // return a + b; // }; NoParaHasReturn noParaHasReturn = () -> 10 + 20; System.out.println(noParaHasReturn.test()); } } interface NoParaHasReturn { int test(); }
规则4:若抽象方法存在返回值且覆写的方法体只有一行,此时方法体的大括号和return都能省
-
有返回值有参数
public class AllLambda { HasParaHasReturn hasParaHasReturn = (x,y) -> x + y; System.out.println(hasParaHasReturn.test(20,30)); } } interface HasParaHasReturn { int test(int a, int b); }
看见() -> 第一反应就是Lambda表达式,然后去找借口中抽象方法的定义,一一对应参数和内容即可。
进程和线程
程序:一系列有组织的文件,封装操作系统的各种API,实现不同的效果。
进程:程序在系统中的一次执行过程。进程是现代资源(CPU,内存等关键系统资源)的最小单位。
线程:就是进程一个独立的子任务。同一个进程的所有进程共享进程的资源,线程是操作系统任务执行(系统调度)的基本单位。
同时下载视频,同时听音乐=》下载任务就是一个线程,听音乐就是另一个线程。
进程和线程的区别
-
进程是os资源分配的基本单位,线程是os系统调度的基本单位。
-
创建和销毁进程的开销要远比创建和销毁线程大得多(创建和销毁一个进程的时间要比创建和销毁一个线程大得多),线程更加轻量化。
-
调度一个线程远比调度一个进程快得多。
-
进程包含线程,每个进程至少包含一个线程(主线程)
之前写的主方法main就是主线程
-
进程之间彼此相对独立,不同的进程不会共享内存空间,同一个进程的线程共享内存空间。
6.16多线程(3)
进程:操作系统资源分配(CPU和内存)的基本单位,不同进程之间互相隔离,内存空间独立,不共享。
线程:os任务调度的基本单位,线程存在于进程之内,每个进程至少存在一个线程(主线程,其他的子线程和主线程共享进程的资源)
线程就是进程中的一个个独立的子任务。
第一个多线程代码
之前写的代码入口都是main(主线程),所有的任务都在主方法中(主线程)进行。
Java中描述线程这个对象的类 - java.lang.Thread类线程的核心类,都是通过Thread类来启动一个新的线程。
/**
* 第一个多线程示例代码
* @author kurumi
* @date 2022/6/21 17:07
**/
public class FirstThreadDemo {
private static class MyThread extends Thread {
//run方法就是线程的核心工作方法,线程要干的所有事情都在run方法中进行定义
@Override
public void run() {
Random random = new Random();
while (true) {
//打印当前线程名称
System.out.println(Thread.currentThread().getName());
//当前线程随即暂停0~9秒
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public static void main(String[] args) {
//创建三个线程对象
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
MyThread m3 = new MyThread();
//启动三个子线程
m1.start();
m2.start();
m3.start();
Random random = new Random();
while (true) {
//打印当前线程名称
System.out.println(Thread.currentThread().getName());
//当前线程随即暂停0~9秒
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
main方法是主线程
run方法就是每个线程的核心执行流程方法,每个线程都是从run方法开始执行代码。
启动线程调用start方法,线程启动之后会由JVM自动执行每个线程的run方法。
JDK和JRE相比最大的区别,JDK提供了很多开发程序会用到的辅助工具。
jconsole命令,查看当前运行的JVM内部的线程情况=》jconsole.exe
创建线程的方式
一共有四种方式
第1种和第2种 =》 最终启动线程都是通过Thread类的start方法
-
继承Thread类,覆写run方法(线程的核心工作任务方法)
- 一个子类继承Thread类
- 覆写fun方法
- 产生当前这个子类对象,而后调用start方法启动线程
调用start方法启动线程,是由JVM产生操作系统的线程并启动,倒是啥时候真正启动,对于我们来说不可见,也没法控制
public class ThreadMethod extends Thread{
@Override
public void run() {
System.out.println("子线程输出结果");
}
}
public class Main {
public static void main(String[] args) {
//创建进程类对象
ThreadMethod mt = new ThreadMethod();
mt.start();
System.out.println("主线程的输出");
}
}
-
覆写Runnable接口,覆写fun方法
/** * 这个实现了RunnableMethod接口的子类,并不是直接的线程对象,只是一个线程的核心工作任务 * 线程的任务和线程实体的关系 * @author kurumi * @date 2022/6/21 20:05 **/ public class RunnableMethod implements Runnable{ @Override public void run() { System.out.println("这是Runnable方式实现的子线程任务"); } } public class Main { public static void main(String[] args) { //创建线程的任务对象 RunnableMethod runnableMethod = new RunnableMethod(); //创建线程对象,将任务对象传入线程对象 Thread thread = new Thread(runnableMethod); //启动线程 thread.start(); System.out.println("主线程的输出"); } }
推荐使用方式2,实现Runnable接口更加灵活,子类还能实现别的接口,继承别的类,方式1的话,只能继承Thread,单继承局限。
-
覆写Callable接口,覆写call方法
-
使用线程池创建线程
使用匿名内部类创建线程
方式1:使用匿名内部类创建Thread对象。
public class OtherMethod {
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println("匿名内部类继承Thread");
System.out.println(Thread.currentThread().getName());
}
};
t1.start();
System.out.println("这是主线程" + Thread.currentThread().getName());
}
}
方式2:使用匿名内部类创建Runnable对象。
public class OtherMethod {
public static void main(String[] args) {
//Thread类的有参构造, public Thread(Runnable target)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现Runnable接口");
}
});
t2.start();
System.out.println("这是主线程" + Thread.currentThread().getName());
}
}
使用Lambda表达式创建线程
public class OtherMethod {
public static void main(String[] args) {
Thread t3 = new Thread(() -> System.out.println("Lambda表达式实现Runnable接口"));
t3.start();
System.out.println("这是主线程" + Thread.currentThread().getName());
}
}
因为Runnable接口也是函数式接口,所以可以转为Lambda表达式,()指代run方法。
并发执行理论速度是顺序执行的二倍。
Thread类常见方法
无论是继承Thread类还是实现Runnable接口,最终启动线程的都是Thread类的start方法。
Thread类就是JVM描述管理线程的类,每个线程都对应唯一的一个Thread对象。
构造方法
/**
* 多线程常用方法
* @author kurumi
* @date 2022/6/22 9:17
**/
public class NormalMethod {
public static void main(String[] args) {
//一般搭配子类使用,需要有一个子类继承Thread类
Thread t1 = new Thread();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("传入Runnable对象");
}
});
Thread t3 = new Thread("dhl的线程");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("带有名字");
}
},"dhl的线程");
}
}
t4最常用,在传入线程对象同时也赋予线程名字。
Thread类的核心属性
start方法
在启动线程时调用Thread类的start方法,线程才开始按照run方法中的步骤去执行。
已启动的线程不能再次调用start方法,否则会抛出异常,说明该类已经启动过了,异常类为非法线程状态异常。
中断线程
线程之间通信的方式,中断一个正在执行的线程(run方法还没有执行结束),普通线程会在run方法执行结束之后自动停止。
中断线程有两种方式:
-
通过共享变量进行中断
/** * 通过共享变量中断线程 * @author kurumi * @date 2022/6/23 20:16 **/ public class TreadInterruptByVar { private static class MyThread implements Runnable { // 多个线程都会用到的变量加上volatile关键字 volatile boolean isQuit = false; @Override public void run() { while (!isQuit) { System.out.println(Thread.currentThread().getName() + "运行中"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(Thread.currentThread().getName() + "被中断了"); } } public static void main(String[] args) throws InterruptedException { MyThread mt = new MyThread(); Thread thread = new Thread(mt,"a线程"); thread.start(); Thread.sleep(3000); System.out.println("中断a线程"); mt.isQuit = true; } }
在主线程进行中断前,子线程持续输出,在主线程休眠3秒后中断子线程。
所有Thread类的静态方法都是在哪个线程中调用的,就生效在哪个线程。
-
使用Thread.interrupted()静态方法或
Thread对象的成员方法isInterrupted()
Thread类的内部包含了一个属性,当前线程是否被中断属性。
线程收到内置的中断通知有两种方式:
-
当线程调用sleep、wait、join等方法处在阻塞状态时,收到中断通知(thread.interrupt()),就会抛出一个中断异常InterruptedException,当抛出异常后,当前线程的中断状态会被清除,一般在捕获异常代码块决定是否
/** * 通过内置的属性中断线程 * @author kurumi * @date 2022/6/23 20:47 **/ public class ThreadInterruptedByMethod { private static class MyRunnable implements Runnable { @Override public void run() { // while (!Thread.interrupted()) { while (!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + "正在运行"); try { Thread.sleep(1000); } catch (InterruptedException e) { System.err.println("有人在打扰"); break; } } System.out.println(Thread.currentThread().getName() + "被打断了"); } } public static void main(String[] args) throws InterruptedException { MyRunnable mt = new MyRunnable(); Thread thread = new Thread(mt,"a线程"); System.out.println("a线程已启动"); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
-
线程没有调用以上三种方法时,处在正常运行状态,收到中断通知thread.interrupt(),
Thread.interrupted():判断当前线程是否被中断,若中断状态为true,清除中断标志
public class ThreadDoubleMethod { private static class MyRun implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.interrupted()); } } } public static void main(String[] args) { MyRun myRun = new MyRun(); Thread thread = new Thread(myRun); thread.start(); thread.interrupt(); } } //输出结果为true false false false false
Thread.currentThread.isInterrupted():判断指定线程对象是否状态为中断状态,若状态为true,不会清除中断标志。
public class ThreadDoubleMethod { private static class MyRun implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().isInterrupted()); } } } public static void main(String[] args) { MyRun myRun = new MyRun(); Thread thread = new Thread(myRun); thread.start(); thread.interrupt(); } } //结果为true true true true true
-
6.17多线程(4)
线程常用方法
等待另一个线程
也是多线程之间通信的方式
线程对象.join();
在哪个线程调用别的线程对象的join方法,意思就是这个线程要等待另一个线程执行完毕在继续执行本线程的后续代码。
例如:主线程中调用thread.join(),主线程就会进入阻塞状态,知道thread1执行结束,主线程才会继续向后执行。
/**
* 线程等待,join方法,成员方法
* @author kurumi
* @date 2022/6/24 14:15
**/
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"a线程");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"b线程");
t1.start();
//阻塞主线程,等待t1执行完后继续向后执行
t1.join();
t2.start();
t2.join();
System.out.println("执行完毕");
}
}
在哪个线程调用的join方法,就阻塞哪个线程。
比如在main里调用t2.join(),就阻塞主线程,直到t2完全执行结束后再恢复主线程执行。
获取当前正在执行的线程对象
Thread.currentThread() -> 获取正在执行的线程对象。
休眠当前线程
Thread.sleep(long millis):在哪调用,就休眠哪个线程。
运行状态转为就绪态
yield()方法:调用yield方法的线程会主动让出CPU资源,从运行态转为就绪态,等待被CPU继续调度。
使用不多,因为都不可控。
到底啥时候让出CPU,又啥时候被CPU再次调度,都是os调度的,我们无权选择:
-
让出之后立马又被调用了
-
让出之后很久都不调度
都有可能,就绪态就是指等待被服务的状态,已经准备好了,不需要别的资源
public class YieldTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(true) {
System.out.println(Thread.currentThread().getName());
Thread.yield();
}
},"a线程");
t1.start();
Thread t2 = new Thread(() -> {
while(true) {
System.out.println(Thread.currentThread().getName());
}
},"b线程");
t2.start();
}
}
线程的状态
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
NEW:新建状态,还没开始执行,新建线程对象就是new状态
RUNNABLE:下一个状态,可执行状态,就绪和运行(即将开始执行和真正在运行)都是这个状态
BLOCKED\WAITING\TIMED_WAITING:等待状态,当前线程暂停执行,等待其他任务或者资源。
TERMINATED:终止状态,表示当前线程已经执行结束了,或者抛出异常不正常执行完毕都会进入终止状态,可以被销毁
/**
* 线程的新建,运行和终止状态
* @author kurumi
* @date 2022/6/24 19:57
**/
public class NewAndRunnableState {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000_0000; i++) {
}
},"子线程");
System.out.println(thread.getName() + " : " + thread.getState());
thread.start();
while (thread.isAlive()) {
System.out.println(thread.getName() + " : " + thread.getState());
}
System.out.println(thread.getName() + " : " + thread.getState());
}
}
thread.isAlive()就是判断线程是否为存活状态,除了NEW和TERMINATED状态,都是存活状态。
三种阻塞状态:
BLOCKED\WAITING\TIMED_WAITING都属于线程的阻塞状态(该线程需要暂缓执行,这三个造成的暂缓执行的原因不同)
TIMED_WAITING:超时等待,需要等待一段时间后自动唤醒。
BLOCKED:锁等待,需要等待其他线程释放锁对象。
WAITING:等待被另一个线程唤醒(notify方法)
/**
* 三种阻塞状态
* @author kurumi
* @date 2022/6/24 20:14
**/
public class BlockedState {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (true) {
try {
//TIME_WAITING 等待时间到了,自动唤醒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (lock) {
//BLOCKED 等待获取锁对象
System.out.println("haha");
}
},"t2");
t2.start();
}
}
此时t1的状态为TIMED_WAITING,t2的状态为BLOCKED
public class BlockedState {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
// while (true) {
try {
//TIME_WAITING 等待时间到了,自动唤醒
// Thread.sleep(1000);
lock.wait();
System.out.println("被唤醒了");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// }
}
},"t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (lock) {
//BLOCKED 等待获取锁对象
System.out.println("haha");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//唤醒t1
lock.notify();
}
},"t2");
t2.start();
}
}
稍作改动可将t1变为wait状态,等待t2线程调用,搭配notify方法使用
线程安全问题
多线程最重要的问题,线程安全。
概念
什么是所谓的线程安全:
线程安全指的是代码若是串行执行和并行执行,结果完全一致,就称该代码是线程安全的。
多个线程串行执行的结果和并行结果不同,就称为线程不安全。
/**
* 观察多线程场景下的线程安全问题
* @author kurumi
* @date 2022/6/25 9:13
**/
public class ThreadUnsafeDemo {
private static class Counter {
int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
t1.start;
t1.join;
t2.start;
t2.join;
以上写法分别为并发执行和串行,结果不同,所以该线程不安全。
6.21多线程(5)
线程安全问题
在多线程并发的场景下,实际运行结果和单线程场景下预估结果不符的问题。
JMM
Java的内存模型,描述多线程场景下Java的线程内存(CPU的高速缓存和寄存器)和主内存之间的关系
(和后面的JVM内存区域划分(JVM实实在在的将内存划分为6大区域)不是一个概念)
Java Memory Model:描述线程的工作内存和主内存的关系。
每个线程都有自己的工作内存,每次读取变量(共享变量,不是线程的局部变量)都是先从主内存将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写回主内存。
类中的成员变量,静态变量,常量都属于共享变量,在堆上和方法区中存储的变量。
线程安全要满足的特性
一段代码要保证是线程安全的(多线程场景下和单线程场景下的运行结果保持一致),需要同时满足一下三个特性。
对于多线程操作同一个共享变量才会出现线程安全问题。
原子性
该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么都不执行,不会存在中间状态,这种操作是一个原子性操作。
int a = 10;//直接将常量10赋值给a变量,原子性,要么没赋值,要么赋值成功
a += 10;//=> a = a + 10; 先要读取当前变量a的值,再将a + 10计算,最后将计算得出的值重新赋值给a变量(对应三个原子性操作)
可见性
一个线程对共享变量的修改,能够及时的被其他线程看到,这种特性称为可见性。
public class ThreadUnsafeDemo {
private static class Counter {
int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
count值是Counter类的一个成员变量,这个属性属于共享属性,多个线程同时访问Count类的同一个对象,这个值属于线程共享变量。
count值在主内存中存储(堆上)
对于这段代码来说,第一不满足原子性,每次的操作为count++,有可能在t2读数据时,t1对count进行完++操作,但是还没赋值。
得二不满足可见性,因为count是线程共享变量,存在堆上,每个线程在使用的时候都需要将该变量从主内存中读到自己的工作内存中,由于线程之间的工作内存不可见,所以就有可能导致线程进行完++操作但是还没来得写回主内存,主内存的值又被另一个线程读取,每个线程跑完50000个循环后,因为有漏掉的++操作,所以最终结果要比10万小。
防止指令重排
了解即可,大部分代码的线程安全问题都是因为可见性和原子性。
-
为啥会有这么多内存?
其实只有一个内存,主内存(硬件中实实在在存在的),JVM讲的工作内存实际上是CPU的高速缓存和寄存器。
-
为啥CPU要搞个缓存和寄存器,为啥不直接读写内存
CPU的高速缓存和寄存器的读写速度基本上是内存的成千上万倍。而高速缓存和寄存器价格非常昂贵,造成不可能空间太大,所以还是需要内存。
指令重排:代码的书写顺序不一定就是最终JVM或者CPU的执行顺序。
在单线程场景下指令重排没啥问题,但是在多线程场景下就有可能因为指令重排导致错误,一般就是对象海内初始化完成就被别的线程给用了。
class Person {
int age;
String name;
public Preson(int age, String name) {
this.age = age;
this.name = name;
}
}
Thread1 ->Person per = new Person(18,"a");
Thread2 ->if(per != null) {
per.getName();
}
如果以上代码在执行时进行了指令重排则有可能在赋值完age属性后,就被Thread1调用了,但此时per对象的name属性还没赋值。
要确保一段代码的线程安全性,需要同时满足可见性,原子性,和防止指令重排。
解决线程安全问题
使用关键字synchronized就可以解决线程安全问题。
synchronized为监视器锁,monitor lock(对象锁)
什么是锁,锁的是什么东西。
具体锁的是什么东西需要看synchronized修饰的是什么。
互斥
所谓"互斥" mutex lock
某个线程获取到该对象的锁时,其他线程若也要获取同一个对象的锁,就会处在阻塞等待状态。
synchronized void increase() {
count++;
}
当给increase方法加上synchronized关键字,所有进入该对象的线程都需要获取当前counter对象的"锁",获取成功才能进入,获取失败就会进入阻塞态。
t1先执行increase方法,就会获取到counter这个对象的锁,然后执行increase方法。
t2要执行increase方法,需要获取counter对象的锁,这个锁被t1持有,因此t2线程就处于阻塞态,直到t1释放这个锁。
- 进入synchronized代码块就会尝试加锁操作
- 退出synchronized代码块就会释放这个锁
正因为increase方法上锁处理,多个线程在执行increase方法时其实是排队进入,同一时刻只有可能有一个线程进入increase方法执行对count属性的操作,保证的线程的安全性。
在Java内部,每个Java对象都有一块内存,描述当前对象"锁"的信息,锁的信息就表示当前对象被哪个线程持有。
若锁信息没有保存任何线程,则说明该对象没有被任何线程持有。
若所信息保存了线程id,其他线程要获取该锁,就处在阻塞状态。
等待队列不是FIFO队列,不满足先来后到的特点。
到底是不是互斥关系就看锁的是不是一个对象。
synchronized代码块刷新内存
因为synchronized保证互斥,同一时刻只有一个线程获取到这个对象的锁,就保证了可见性和原子性。因为从第1步到第5步只有一个线程能执行,其他线程都在等待,2,3,4三步对于其他线程就是天然的可见性和原子性。
线程执行synchronized代码块的流程:
- 获取对象锁
- 从主内存拷贝变量值到工作内存
- 执行代码
- 将更改后的值写回主内存
- 释放对象锁
6.23多线程(6)
synchronized关键字
线程上锁
-
互斥性:进入同一个synchronized的线程同一时间只可能有一个,其他线程若想进入synchronized代码块就需要等待(等待获取锁的线程释放锁),BOLCKED(获取锁失败进入的状态)。
-
刷新内存:天然保证原子性和可见性。同一时间只可能有一个线程进入代码块,当然是原子性(所有操作结束才会释放锁),可见(操作完毕对于共享变量的值写回主内存才释放锁,其他线程获取锁的时候,主内存中的值一定是更改后的)
-
可重入:Java中的线程安全锁都是可重入的(包括java.concurrent.lock)
什么是可重入:
获取到对象锁的线程可以再次加锁。=》这个操作就称为可重入
synchronized支持线程的可重入:
Java每个对象都有一个"对象头"(描述当前对象的锁信息 - 当前对象被哪个线程持有,以及一个"计数器", - 当前对象被上锁的次数)
-
若线程1需要进入当前对象的同步代码块(synchronized),此时当前对象的对象头没有锁信息,线程1是第一个获取锁的线程,进入同步代码块,对象头修改持有线程为线程1,计数器的值由0+1 = 1。当线程1在同步代码块中再次调用当前对象的其他同步方法可以进入,计数器的值再次加一,说明此时对象锁被线程1获取两次。
public class Reentrant { private class Counter { int val; synchronized void increase() { val ++; } synchronized void increase1() { //就相当于对同一个Counter对象加了两次锁 //若线程1进入了increase1方法,若不支持可重入,进入increase需要再次获取锁,无法进入increase //线程1拿到当前Counter对象锁的线程阻塞在这里,等待线程1自己释放锁之后再进入increase()-》这个程序永远不会停止 //线程1一直阻塞在这里 - 死锁 increase(); } } }
synchronized加锁和解锁是隐式的,JVM来进行加锁和解锁操作,每当进入一次同步代码块就会自动上锁并将计数器+1,每当带哦用结束一次同步块,自动开锁,并计数器-1,当任意时刻该对象的计数器为0且持有线程为null,说明该对象可以被占有。
-
若线程2需要进入当前对象的同步块,此时当前对象的对象头持有线程为线程1,且计数器值不为0,线程2就会进入阻塞状态,一直等到线程1释放锁为止(直到计数器的值为0,才叫真正释放锁)
-
synchronized锁的内容
synchronized是对象锁,必须得有个具体的对象让他锁。
-
synchronized修饰类中的成员方法,则锁的对象就是当前类的对象。
public class Reentrant { private class Counter { int val; //成员方法,锁的是当前Counter对象 synchronized void increase() { val ++; } } }
当在多个线程中使用两个对象调用该成员方法时不会互斥。
当前这个方法是通过哪个对象调用的,synchronized就锁的是哪个对象。
-
synchronized修饰类中的静态方法,锁的是当前这个类的class对象(全局唯一,相当于把这个类锁了),同一时刻只能有一个线程访问这个方法(无论是几个对象)
synchronized static void increase2() { while (true) { System.out.println(Thread.currentThread().getName() + "获取到了锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) throws InterruptedException { Counter counter1 = new Counter(); Counter counter2 = new Counter(); Counter counter3 = new Counter(); Thread t1 = new Thread(() -> {counter1.increase2();},"t1"); Thread t2 = new Thread(() -> {counter2.increase2();},"t2"); Thread t3 = new Thread(() -> {counter2.increase2();},"t3"); t1.start(); t2.start(); t3.start(); }
这是一个静态方法,锁的Counter.class对象。全局唯一,无论通过哪个Counter对象调用increase2(),同一时刻只能由一个线程获取到这个锁,其他线程都在等待。此时t1,t2,t3都是互斥关系。都需要拿到Counter.class这个全局唯一的对象。synchronized修饰静态方法需要谨慎。
-
synchronize代码块,明确锁的是哪个对象
在开发的时候使用的最多,因为锁的更“细”,只在有需要同步的若干代码上加上synchronized关键字,方法中其他代码仍然是多线程的。
void increase3() { System.out.println(val); System.out.println(val1); System.out.println("abc"); synchronized (this) { while (true) { System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁"); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
在同步代码块外的语句多个线程可以并发执行。
此时,同步代码块内部,若多个线程访问的是同一个Counter对象,则只有一个线程能进入同步代码块。this表示当前对象的引用。当多线程内不是使用同一个对象访问,则不构成互斥。
若同步代码块改成如下所示:
synchronized (Reentrant.class) { }
此时被上锁的是Reetrant.class,全局唯一,所以此时所有对象都互斥。
public class LockNormal { public static void main(String[] args) { Object lock = new Object(); Counter c1 = new Counter(); c1.lock = lock; Counter c2 = new Counter(); c2.lock = lock; Counter c3 = new Counter(); c3.lock = new Object(); Thread t1 = new Thread(() -> { c1.increase(); },"t1"); Thread t2 = new Thread(() -> { c2.increase(); },"t2"); Thread t3 = new Thread(() -> { c3.increase(); },"t3"); t1.start(); t2.start(); t3.start(); } private static class Counter { int val; Object lock; void increase() { synchronized (lock) { while (true) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } }
到底互锁与否,就看多个线程锁的是啥,只有锁的是同一个对象才互锁,不同对象就不互锁。
在此示例中,由于对lock进行上锁,c1和c2的lock是同一个对象,c3是单独new了一个对象,所以在多线程调用时,t1和t2互斥,都与t3并发。
volatile关键字
可见性,内存屏障。
-
volatile关键字可以保证共享变量可见性
相较于普通的共享变量,使用volatile关键字可以保证共享变量的可见性。
-
当线程读取单独是volatile关键字时,线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值)
-
当线程写的是volatile关键字的变量,将当前修改后的变量值(工作内存中)立即刷新到主内存中,==且其他正在读此变量的线程会等待(不是阻塞),直到写回主内存的操作完成,保证读的一定是刷新后的主内存。==对于同一个volatile变量,他的写操作一定发生在他的读操作之前,保证读到的数据一定是主内存中刷新后的数据。
public class Volatile { private static class Counter { volatile int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(() -> { while (counter.flag == 0) { } System.out.println(counter.flag + "退出循环"); },"t1"); t1.start(); Thread t2 = new Thread(() -> { Scanner scanner = new Scanner(System.in); System.out.println("请改变flag的值"); counter.flag = scanner.nextInt(); }); t2.start(); } }
在变量flag前加volatile修饰可以保证变量的可见性。volatile只能保证可见性,无法保证原子性,因此若代码不是原子性操作,任然不是线程安全的,volatile != synchronized
-
-
使用volatile修饰的变量,相当于一个内存屏障。
class { int x = 1;// 1 int y = 2;// 2 boolean z = true;// 3 x = x + 1;// 4 y = y + 1;// 5 }
CPU会在不影响结果的前提下,执行时不一定按照书写顺序执行。
1 2 3 4 5或1 3 4 2 5都有可能。
class { int x = 1;// 1 int y = 2;// 2 volatile boolean z = true;// 3 x = x + 1;// 4 y = y + 1;// 5 }
现在如果加上volatile修饰,CPU在执行到第三行时,一定保证1和2已经执行结束,且1和2的结果对后面的结果可见,此时4和5行一定还没开始。但1和2行还是有可能打乱,执行完第三行后,4和5也肯能打乱。
6.25多线程(7)
线程间等待与唤醒机制
wait和notify是Object类的方法,用于线程的等待与唤醒,必须搭配synchronized锁来使用。
多线程并发的场景下,有时需要某些线程先执行,这些线程执行结束其他线程再继续执行。
等待方法:
-
死等,线程进入阻塞态(WAITING),直到有其他线程调用notify方法唤醒。
-
等待一段时间,若在该时间内线程被唤醒,则继续执行,若超过相应时间还没其他线程唤醒此线程,此线程就不再等待,恢复执行。
唤醒方法
notify():随机唤醒一个处在等待状态的线程。
notifyAll():唤醒所有处在等待状态的线程。
无论是wait还是notify方法,都需要搭配synchronized锁来使用(等待和唤醒,也是需要对象)
使用
等待方法:
调用wait和notify方法时没有获取锁,就会抛出异常,都需要在同步代码块中
private static class WaitTask implements Runnable {
private Object lock;
public WaitTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
}
}
}
调用wait方法的线程就会进入Waiting状态,等待被其他线程唤醒(lock.notify()),同一个对象的notify。
被唤醒之后,线程从wait方法之后开始继续执行。
private static class NotifyTask implements Runnable {
private Object lock;
public NotifyTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("准备唤醒");
lock.notify();
System.out.println("唤醒结束");
}
}
}
调用用notify方法也许亚奥获取一个锁,必须在synchronized同步块中使用,随机唤醒一个处在lock对象上的wait的线程
notify方法的线程必须自己先执行结束run方法,其他的线程才会执行。
对于wait和notify方法,其实有一个阻塞队列还有一个等待队列。
阻塞队列,表示同一时间只有一个线程能获取到锁,其他线程进入阻塞队列
等待队列,表示调用wait(首先此线程要获取到锁,进入等待队列,释放锁)
调用wait方法的前提首先是要获取该对象的锁(synchronized对象锁
调用wait方法会释放锁,本线程进入等待队列等待被唤醒,被唤醒后不是立即恢复执行,进入阻塞队列,竞争锁。
wait方法和sleep方法的区别
遇到这种问题,若这两个方法有联系,就先答共性,再答区别
若这两方法毫无关系,就分别介绍即可
- wait方法是Object类提供的方法,需要搭配synchronized锁来使用,调用wait方法会释放锁,线程进入WAITING状态,等待被其他线程唤醒或者超时自动唤醒,唤醒之后的线程需要再次竞争synchronized锁才能继续执行。
- sleep方法是Thread类提供的方法,调用sleep方法的线程进入TIME_WAITING状态,不会释放锁,时间到自动唤醒
单例模式
校招中考察频率非常高的一个设计模式(32种设计模式 - 编程思想,不同场景下该如何设计和实现代码的固定套路)
所谓的单例模式保证某个类在程序中有且只有一个对象。
现实生活中的单例:一个类只有一个对象,地球类 - 地球只有这一个对象,太阳类 - 太阳这一个对象
如何控制某个类只有一个对象? =》要创建类的对象,通过构造方法产生对象 =》构造方法若是public权限,对于类的外部,随意创建对象,无法控制对象的个数 =》构造方法私有化,类的外部彻底没法产生对象,一个对象都没有。
public class SingLeTon {
private SingleTon(){}
}
=》构造方法私有化之后,对于类的外部而言就一个对象都没有如何构造这唯一的对象(私有化的构造方法只能在类的内部调用)只调用一次构造方法即可)
public class SingleTon {
public static SingleTon singleTon = new SingleTon();
private SingleTon(){}
}
这个唯一的对象应该使用静态变量,因为如果是成员变量需要使用对象才能调用,但是此时在外部无法获取到该类对象。
在Singleton类的外部访问这个唯一的对象直接通过getSingleTon方法获取这个唯一对象。
单例三步走:
- 构造私有化(保证对象的的生产个数)
- 单例的内部提供这个唯一的对象(static)
- 单例类提供返回这个唯一的对象的静态方法供外部使用
饿汉式单例:类加载就产生这个唯一的对象,也不管外部是否调用该对象
/**
* 饿汉式单例,饥不择食,这个类一加载就把唯一的对象产生了,
* 不管外部用不用这个对象,只要这个类加载到JVM,唯一对象就会产生
* @author kurumi
* @date 2022/7/3 22:37
**/
public class SingleTon {
//唯一的这一个对象
public static SingleTon singleTon = new SingleTon();
private SingleTon(){}
public static SingleTon getSingleTon() {
return singleTon;
}
}
懒汉式单例:只有第一次调用getSingleTon方法,表示外部需要获取这个单例对象时才产生对象
/**
* 懒汉式单例:第一次调用getDingleTon方法才实例化对象
* @author kurumi
* @date 2022/7/4 10:48
**/
public class LazySingleTon {
private static LazySingleTon singleTon;
private LazySingleTon() {}
public static LazySingleTon getSingleTon() {
if (singleTon == null) {
singleTon = new LazySingleTon();
}
return singleTon;
}
}
系统初始化时,外部不需要这个单例对象,就先不产生,只有当外部需要此对象才实例化对象。这种操作称为懒加载。
在线程安全方面,饿汉式单例时天然线程安全的,因为在系统初始化JVM加载类的过程中就创造了这个唯一的对象。
但是在懒汉式单例时,如果三个线程并行调用get方法,此时singleTon三个线程看到的就都是null每个线程都创建了一个对象。
解决懒汉式的线程安全问题
最简单粗暴的方式,直接在静态方法上加锁。
public synchronized static LazySingleTon getSingleTon() {
if (singleTon == null) {
singleTon = new LazySingleTon();
}
return singleTon;
}
保证了同一时间只有一个线程进入此方法。
此时getSingleTon方法内部所有都是单线程操作,其他线程要进入此方法都需要获取锁(锁的粒度太粗)
若此时优化刚才的方法锁如下
public static LazySingleTon getSingleTon() {
if (singleTon == null) {
synchronized (LazySingleTon.class) {
singleTon = new LazySingleTon();
}
}
return singleTon;
}
当t1先进入同步代码块之后,t2和t3卡在获取锁的位置,t1产生对象之后,锁释放,t2和t3还是从获取锁的位置继续执行,t2和t3就会再次new对象。
所以最后使用double - check,双重检查
public static LazySingleTon getSingleTon() {
if (singleTon == null) {
synchronized (LazySingleTon.class) {
if (singleTon == null) {
singleTon = new LazySingleTon();
}
}
}
return singleTon;
}
在同步代码块的内部需要再次检查singleTon是否为空,防止其他线程恢复执行后多次创建单例对象。
此时的单例只是最核心的代码,单例模式还有很多其他操作,为了保证其他操作尽可能的并发执行,使用双重检查。
双重加锁:使用volatile关键字保证单例对象的初始化不被中断
public class LazySingleTon {
private static volatile LazySingleTon singleTon;
private LazySingleTon() {}
public static LazySingleTon getSingleTon() {
if (singleTon == null) {
synchronized (LazySingleTon.class) {
if (singleTon == null) {
singleTon = new LazySingleTon();
}
}
}
return singleTon;
}
}
相当于在singleTon = new LazySingleTon时,有一个内存屏障,其他线程获取的单例对象一定是初始化完成的对象。
如果在构造方法中有初始化语句,在多线程场景下,如果t1在构造方法还没有完全结束时,t2检测到此时singleTon已经 != null,直接返回了,但是返回的单例对象是一个尚未完全初始化的对象。
加了volatile关键字后,其他线程要能执行到return操作JVM一定要保证new操作完全结束之后才能执行return语句。
如果以后让写单例模式
- 如果对double - check的理解到位,直接写懒汉单例
- 如果感觉不太行就写饿汉单例
阻塞队列
和普通队列最大的区别在于入队和出队会阻塞
入队时,若队列已满,则入队操作会“阻塞”,直到有其他线程从队列中取出元素,
出队时,若队列为空,则出队操作会”阻塞”,直到有其他线程想队列中添加元素。
可以将支付请求交给队列,服务器不直接处理支付逻辑,专门处理支付逻辑的程序从队列中取请求,依次处理。
JDK中的阻塞队列
BlockingQueue,入队方法put()阻塞式入队方法,出队方法take()阻塞式的入队。
常用子类
ArrayBlockingQueue
LinkedBlockingQueue
通过锁 + wait和notify机制实现阻塞队列
定时器
现实生活中的闹钟。
设置一个时间以及一个相应的任务“小爱同学,三分钟后播放最后的战役”
在web编程部分,检测客户端的连接,500ms之后没有收到数据,断开连接。
LRU缓存 希望某个键值对3s之后就过期(删除)
JDK中使用Timer类描述定时器,核心方法就是schedule(指定时间到了要执行的任务,等待时间 - ms)
JDK中使用Timer类描述定时器,核心方法就是schedule(指定时间到了要执行的任务,等待时间 - ms,每隔多久执行一次)
延迟三秒之后执行TimerTask任务
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000);
}
}
延迟三秒后开始执行任务,该任务启动之后每隔一秒就会再次执行。
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000,1000);
}
}
线程池
之前创建线程的两种方式:继承Thread类,实现Runnable接口。
最终启动线程都是通过Thread类的start方法,调用Thread类的构造方法产生Thread对象后,线程的状态为new
所谓的new状态,就是操作系统要准备重新开启一个线程
调用start方法后,线程的状态变为runnable
启动线程后,JVM调用线程的run方法来执行线程任务,当run方法执行结束之后,线程的状态为Terminzted
虽然创建和销毁线程开销比较小(和进程相比),但是当系统中的线程数量比较多时,这个开销就比较可观
“池”:数据库连接池,创建和销毁数据库的连接就是一个比较耗时的操作,每当一个连接调用close方法终止后,表示当前用户不再使用此连接,就回收到连接池中(同一个连接可以被多个用户使用多次,减少了每次创建连接和销毁连接的系统开销)
“池”:目的就是让某些对象多次重复利用,减少频繁创建和销毁对象带来的开销问题(这些对象一定是可以复用的)
同样的,不同线程只是run方法的内容不同,线程的大致流程都是一样的,因此为了避免重复创建和销毁线程带来的开销,可以让线程“复用”起来
线程池:内部创建好了若干个线程,这些线程都是runnable,只需要从系统中取出任务,就可以立即开始执行。
线程池最大的好处就减少每次启动和销毁线程的损耗(提高时间和空间利用率)
JDK中线程池的使用
描述线程池的类,最常用的一个子类——TheadPoolExecutor,这个类的构造方法就是创建一个线程池的所有核心参数。
线程池的核心父接口,ExecutorService接口
提交任务
推荐使用submit
-
执行任务,线程接受一个runable对象并执行任务
-
提交一个任务(线程的run方法或call方法)到线程池,线程池就会派遣空闲的线程执行任务。
销毁线程池
作用都是终止线程池中的所有线程,销毁线程池。
-
停止所有处在空闲状态的线程,其他正在执行任务的线程,等待任务执行结束再停止。
-
立即尝试终止所有线程池中的线程(无论是否空闲)
Executors类
Java中类的命名规律,凡是类s =》 工具类,例如Arrays(数组工具类,copyOf,sort等)
使用这个类就可以创建JDK内置的四大线程池。
public static void main(String[] args) {
// 1.固定大小线程池。
ExecutorService pool = Executors.newFixedThreadPool(10);
// 2.数量动态变化的缓存池
ExecutorService pool1 = Executors.newCachedThreadPool();
// 3.只包含一个线程的单线程池
ExecutorService pool2 = Executors.newSingleThreadExecutor();
// 4.实现线程池,可以设置任务的延时自动时间(Timer类的线程池实现)
ExecutorService pool3 = Executors.newScheduledThreadPool(1000);
}
pool.submit(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "hello" + i);
}
});
pool3.schedule(() -> {
System.out.println(Thread.currentThread().getName() + "三秒后执行");
},3000, TimeUnit.MILLISECONDS);
ThreadPoolExecutor子类的核心构造方法参数
corePoolSize:核心池线程数量,——正式员工数
maximumPoolSize:线程池的最大线程数量,——正式工+临时工
keepAliveTime,unit:临时线程空闲以unit为单位空闲时间keepAliveTime后销毁
workQueue:阻塞队列,任务都在队列中存储,线程从队列中取出要执行的任务。
handler:拒绝策略,当任务数量超出线程的负荷咋办。
线程池的工作流程
当调用submit提交一个新的任务时,线程池内部的执行流程:
-
判断核心池corePoolSize是否到达最大数量(正式工人数量是否到达最大数量)
F:没达到corePoolSize最大数量,无论当前是否有空闲线程都要创建一个新线程执行任务,然后将该线程保存到核心池中(招聘一个新的正式工)
T:达到当前核心池corePoolSize最大数量 =》2
-
判断工作队列是否已满(BlockingQueue)
F:若队列未满,将任务入队列,排队等待线程调度
T:若队列已满 =》3
-
判断当前线程数量是否达到maxiumSize(临时工 + 正式工)
F:若没达到上限,创建新的线程(招募临时工)处理此任务
T:若达到上限,说明线程池已经满负荷运行 =》 4
-
执行拒绝策略
四种拒绝策略
- AbortPolicy:超出负荷的任务直接拒绝,抛出异常。
- CallerRunsPolicy:返回给线程池的调用者处理
- DiscardOldestPolicy:丢弃队列中最古老的任务(排队时间最长的)
- DisCardPolicy:丢弃新来的任务。
固定大小线程池
此时固定大小线程池中,没有临时工,最大线程数就是核心池的线程数。
因为没有临时线程,所以没有空闲时间。
基于链表的阻塞队列,无界队列。
数量动态变化的缓存池
此时没有正式工,全是临时工。
当临时线程空闲60s销毁
队列中一个元素都保存不了,take元素必须等待另一个线程put元素。
单线程池
只有一个线程,没有临时工。
没有空闲时间。
基于链表实现的无限队列。
定时器线程池
核心数
最大数
当临时工一空闲就销毁
单线程池的意义
和只创建一个线程区别:只创建一个线程代表只能执行一个任务,一个任务执行完了就销毁了。
单线程池:虽然同一时间只能执行一个任务,当这个任务执行结束之后,继续在工作队列调度一个新的任务继续执行。
例如现在有10个任务,只创建一个线程就会不断创建和销毁线程,创建和销毁线程10次,执行10个任务。
如果是单线程池,只会创建和销毁一个线程,这10个任务就会进入队列一次调度进行。
阿里编码规约
尽量不要使用内置线程池,最好根据实际的业务需求,定制线程池。自己new ThreadPoolExector对象,传递相应的参数。
常见的锁的策略
概念不只局限于Java,MySQL,Go,C++等等都有类似的锁的策略。
所谓的锁策略就是指,到底如何实现锁。
乐观锁和悲观锁
-
乐观锁
每次读写数据都认为不会发生冲突,线程不会阻塞,一般来说,只有在数据更新时才会检查是否发生冲突,若没有冲突,直接更新,只有冲突(多个线程都在更新数据)了才解决问题。
乐观锁会直接访问,若有应答则避免了加锁和解锁操作,若无应答则不会阻塞,线程会去忙别的事。
-
悲观锁
每次取读写数据都会冲突,每次在进行数据读写时都会上锁(互斥),保证同一时间只有一个线程在读写数据。
悲观锁会先尝试加锁,若无相应则线程阻塞,等待被唤醒重新加锁。
两种策略都有相应的应用场景。
乐观锁场景:当线程冲突不严重时候,可以采用乐观锁策略来避免多次的加锁解锁操作。
悲观锁场景:当线程冲突严重时,就需要加锁来避免线程频繁访问共享数据失败带来的CPU空转问题。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 “版本号” 来解决。
读写锁
适用于线程基本都在读数据,很少有写数据的情况。synchronized不是读写锁,JDK内置了另一个ReentrantReadWriteLock实现读写锁。
多线程访问数据时,并发读取数据并不会有线程安全问题,只有在更新数据(增删改)时会有线程安全问题,将锁分为读锁和写锁。
- 多个线程并发访问读锁(读数据),则多个线程都能访问到读锁,读锁和读锁是并发的,不互斥。
- 两个线程都需要访问写锁(写数据),则这两个线程互斥,只有一个线程能成功获取到写锁,其他写线程阻塞
- 当以一个线程读锁,另一个线程写(也互斥,只有当写线程结束,读线程才能继续执行)
重量级锁和轻量级锁
重量级锁:需要操作系统和硬件支持,线程获取重量级锁失败进入阻塞状态(os,用户态切换到内核态,开销非常大)
例如去银行办业务,
用户态:在窗口外,自己处理的业务属于用户态
内核态:窗口内部,需要工作人员协助。
某个任务需要频繁在用户态和内核态切换,非常耗时。
轻量级锁:尽量在用户态执行操作,线程不阻塞,不会进行状态切换。
轻量级锁的常用实现就是采用自旋锁
之前方式,获取锁失败的线程都会进入Blocked状态,线程置入阻塞队列,等待锁被释放,由CPU唤醒(这个时间一般来说比较长,用户态到内核态的切换)
自旋锁:自选就是循环
while(获取lock == false){// 循环} 线程获取失败并不会让出CPU,线程也不会阻塞,不会从用户态切到内核态,线程在CPU上空跑,当锁被释放,当锁被释放,此时这个线程会很快获取到锁。
公平锁和非公平锁
获取锁失败的线程进入阻塞队列,当锁被释放,第一个进入队列的线程首先获取到锁(等待时间最长的线程获取到锁)——公平锁
获取锁失败的线程进入阻塞队列,当锁被释放,所有在队列中的线程都有机会获取到锁,获取到锁的线程不一定就是等待时间最长的线程——非公平锁。
synchronized就是非公平锁。
CAS
乐观锁的一种实现,程序不会阻塞,不断重试。
实现原子类
Java.util.concuttent(juc包,并发工具包,原子类,线程安全集合,ConcurrentHashMap,CopyOnWriteArrayList)
int i = 0; i++,I–都是非原子性操作,多线程并发会有线程安全问题。
使用原子类来保证线程安全性。
AtomicInteger i = new AtomicInteger();// 无参构造就是默认为0
AtomicInteger i = new AtomicInteger(10);// 默认从10开始计数
incrementAndGet();// ++i;
使用CAS来实现自旋锁
乐观锁的一种实现
自旋锁指的是获取锁失败的线程不进入阻塞态,而是在CPU上空转(线程不让出CPU,而是跑一些无用指令)不断查询当前锁的状态
锁的升级
当竞争流程越来越激烈时,
会从无锁状态 =》虽然有synchronized,但是没有线程调用此方法,对象就是无锁状态,没有任何线程尝试获取该锁
转变为偏向锁 =》 当有第一个线程(t1)尝试获取锁时,JVM分配偏向锁给该线程,当这个线程再次获取锁时(重入或者再次执行),没有加锁和解锁过程,就验证一下锁的持有线程是否为t1,若是就直接运行。
当有第二个线程(t2)在t1执行后尝试获取锁,JVM就会取消偏向锁的状态,将锁升级为轻量级锁 =》 使用CAS(自旋锁)来进行轻量级锁的获取,有枷锁和解锁的过程,就是通过自旋来进行的。t2尝试通过自旋的方式来获取轻量级锁,还有其他线程t3,t4等,在t2还没结束的售后尝试获取锁,都在等待t2执行结束。
到此还是Java语言层面,只是进行循环。
当竞争非常激烈时,多个线程同时在竞争轻量级锁(一般来说时当前线程数为CUP核数的一半),就会将轻量级锁膨胀为重量级锁,或者自旋次数超过10次(默认值)以上,也会将轻量级锁膨胀为重量级锁(竞争比较激烈,为了避免CPU空跑,就将线程阻塞),或者程序中调用了Object.wait()方法,直接会将锁膨胀为重量级锁,无论当前竞争激烈与否,wait方法实际上需要monitor实现。
只要线程阻塞就是悲观锁,依赖操作系统提供mutex来实现重量级锁,用户态切换到内核态。
创建线程的方式
一共有以下四种:
- 继承Thread类
- 实现Runnable接口 =》 不带返回值的接口,覆写run(线程的核心工作任务方法)
- 实现Callable接口 =》 带返回值的接口,覆写call方法(线程的核心工作方法,有返回值)
- 线程池
1,2,3的方法最终启动线程,都需要传递给Thread类,start方法启动线程。
线程池的方法要调用submit(Runnable | Callable)
创建线程来计算1 + 2 + 3 + … + 1000,因为runable没有返回值。
主线程等待子线程执行完毕,获取结果
private static class Count {
int sum = 0;
Object lock = new Object();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Count count = new Count();
Callable<Integer> callable = new Callable<Integer>() {
int sum = 0;
@Override
public Integer call() throws Exception {
for (int i = 0; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
int result = futureTask.get();
System.out.println(result);
}
- Callable接口的返回值使用FutureTask子类来接收
- Callable接口对象最终也是通过Thread类的start方法启动线程,向Thread类传入FutureTask对象(包含了call方法)
- 调用FutureTask的get获取call方法的返回值,调用get方法的线程会一直阻塞,直到call方法执行结束,有返回值再继续执行
Callable就是带返回值的工作方法接口,县城核心方法call方法,
FutureTask类接收Callable的返回值,启动线程将FutureTask类传入Thread启动线程
获取相应的call方法返回值,调用FutureTask的get方法。
juc下的常用子类
对象锁juc.lock
在Java中除了synchronized关键字可以实现对象锁之外,java.util.concurrent中的Lock接口也可以实现对象锁
synchronized从JDK1.0就有,需要JVM借助操作系统提供的mutex系统原语实现
Lock接口是JDK1.5之后,Java语言自己实现的互斥锁,不需要借助操作系统的monitor机制。
void lock(); // 加锁,获取锁失败的线程进入阻塞状态,知道其他线程释放锁,再次竞争,死等
boolean trylock(long time,TimeUnit unit) throws InterruptedException;
// 加锁,获取锁失败的线程进入阻塞态等待一段时间,时间过了还未获取到锁恢复执行,放弃加锁,执行其他代码。
void unlock();// 解锁
了
synchronized和lock的区别:
- synchronized是Java的关键字,由JVM实现,需要依赖操作系统提供的线程互斥原语(metux);lock标准库的类和接口,其中一个最常用的子类(ReentrantLock,可重入锁),由Java本身实现,不需要以来操作系统。
- synchronized隐式的加锁和解锁,lock需要显示进行加锁和解锁
- synchronized在获取锁失败的线程时,死等;lock可以使用tryLock等待一段时间后自动放弃加锁,线程恢复执行
- synchronized是非公平锁,ReentrantLock默认是非公平锁,可以在构造方法中传入true开启公平锁
- synchronized不支持读写锁,Lock子类ReentrantReadWriteLock支持读写锁。
一般场景synchronized足够用,需要用到超时等待锁,公平锁,读写锁再考虑使用juc.lock
死锁
锁对象的循环等待问题
可以使用jconsole来检查死锁
信号量
Semaphore就是一个计数器,表示当前可用资源个数
关于信号量Semaphore有两个核心操作
P:申请资源操作
V:释放资源操作
Semaphore的PV操作都是原子性的,在多线程场景下可以直接使用
public static void main(String[] args) {
// 在构造参数传入可用资源的个数
// 可用资源为5个
Semaphore semaphore = new Semaphore(5);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "准备申请资源");
// P操作,默认申请一个资源
// 若可用资源为零,会在这阻塞
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "获取资源成功");
Thread.sleep(1000);
// V操作,默认释放一个占有的资源
semaphore.release();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable,String.valueOf(i + 1));
t.start();
}
}
当前可用资源为5,此时只有5个线程获取资源成功,其他线程都会阻塞在acquire方法处,等待占有线程释放资源。
如果需要多个资源,在P,V的重载方法中传入参数即可,保证获取和释放的资源一致。
计数器
CountDownLatch,大号的join方法,调用await方法的线程需要等待其他线程将计数器减为0才能继续恢复执行
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "到达终点");
// 计数器 - 1
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable,"运动员" + i + 1);
thread.start();
}
// main线程是裁判线程,需要等待所有的运动员到达终点后再恢复执行
// 直到所有线程调用countdown方法将计数器减为0继续执行
latch.await();
System.out.println("比赛结束~");
}
juc包下一共有四个常用工具类
信号量 - Semaphore
计数器 - CountDownLatch
循环栅栏 - CyclicBarrier
两个线程之间的交换器 - Exchanger
类ReentrantReadWriteLock支持读写锁。
一般场景synchronized足够用,需要用到超时等待锁,公平锁,读写锁再考虑使用juc.lock
死锁
锁对象的循环等待问题
可以使用jconsole来检查死锁
信号量
Semaphore就是一个计数器,表示当前可用资源个数
关于信号量Semaphore有两个核心操作
P:申请资源操作
V:释放资源操作
Semaphore的PV操作都是原子性的,在多线程场景下可以直接使用
public static void main(String[] args) {
// 在构造参数传入可用资源的个数
// 可用资源为5个
Semaphore semaphore = new Semaphore(5);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "准备申请资源");
// P操作,默认申请一个资源
// 若可用资源为零,会在这阻塞
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "获取资源成功");
Thread.sleep(1000);
// V操作,默认释放一个占有的资源
semaphore.release();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable,String.valueOf(i + 1));
t.start();
}
}
当前可用资源为5,此时只有5个线程获取资源成功,其他线程都会阻塞在acquire方法处,等待占有线程释放资源。
如果需要多个资源,在P,V的重载方法中传入参数即可,保证获取和释放的资源一致。
计数器
CountDownLatch,大号的join方法,调用await方法的线程需要等待其他线程将计数器减为0才能继续恢复执行
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "到达终点");
// 计数器 - 1
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable,"运动员" + i + 1);
thread.start();
}
// main线程是裁判线程,需要等待所有的运动员到达终点后再恢复执行
// 直到所有线程调用countdown方法将计数器减为0继续执行
latch.await();
System.out.println("比赛结束~");
}
juc包下一共有四个常用工具类
信号量 - Semaphore
计数器 - CountDownLatch
循环栅栏 - CyclicBarrier
两个线程之间的交换器 - Exchanger