JAVA面试高级技术栈-01多线程编程
想要了解更多?:
JAVA面试高级技术栈-01-多线程编程
JAVA面试高级技术栈-02Linux基本指令
JAVA面试高级技术栈-03JVM(Java虚拟机)
JAVA面试高级技术栈-04-MySql优化
JAVA面试高级技术栈-05-Redis持久化
JAVA面试高级技术栈-06-Spring
文章目录
一些要了解的概念
- 线程上下文切换: 当一个线程被剥夺cpu使用权时,切换到另外一个线程执行
多线程编程
首先,了解线程之前我们要先了解什么是进程:
进程
进程就是正在运行中的程序(进程是驻留在内存中的)
- 是系统执行资源分配和调度的独立单位
- 每一进程都有属于自己的存储空间和系统资源
- 注意:进程A和进程B的内存独立不共享。
进程的组成:
守护进程 : 也称精灵进程,是运行在后台的一种特殊进程。守护进程独立于控制终端并且周期性的执行某种任务或者等待处理某些打算的事件。生存周期长,常常在系统引导装入的时候启动,
线程
线程就是进程中的单个顺序控制流,也可以理解成是一条执行路径
- 单线程:一个进程中包含一个顺序控制流(一条执行路径)
- 多线程:一个进程中包含多个顺序控制流(多条执行路径)
线程的组成:
-
线程控制块(Thread Control Block,TCB):每个线程都有一个独立的线程控制块,用于存储线程的状态信息,如程序计数器、寄存器等。
-
程序计数器(Program Counter):用于记录线程当前执行的指令位置,当线程被中断时,可以恢复到中断前的执行位置。
-
栈(Stack):每个线程都有自己的栈空间,用于存储方法调用和局部变量等信息。栈是线程私有的,保证了线程之间的数据隔离。
-
寄存器(Registers):用于存储线程执行过程中的临时数据和计算结果。
-
堆(Heap):堆是Java虚拟机管理的内存区域,用于存储对象实例和数组等动态分配的内存。
-
同步器(Synchronizers):用于实现线程之间的同步和互斥操作,如锁、信号量、条件变量等。
-
线程优先级(Thread Priority):每个线程都有一个优先级,用于指定线程在竞争CPU资源时的调度顺序。
-
线程状态(Thread State):线程可以处于不同的状态,如新建、就绪、运行、阻塞和终止等。
PS:
堆:堆是用来存放对象的内存空间,
几乎
所有的对象都存储在堆中。(通俗来讲 只要是new出来的都在堆中)方法区存放的信息:
- 已经被虚拟机加载的类信息
- 常量
- 静态变量
- 即时编译器编译后的代码
在java语言中 线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发
进程与线程的关系
- 联系:
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
- 区别:
进程:有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程:是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
总结
进程:系统运行的基本单位,进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。
线程:独立运行的最小单位,一个进程包含多个线程且它们共享同一进程内的系统资源
进程间通过管道、 共享内存、信号量机制、消息队列通信
java中多线程的生命周期
就绪状态:就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权力(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。
运行状态:run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行。
阻塞状态:当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片。之前的时间片没了需要再次回到就绪状态抢夺CPU时间片。
锁池:在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片。(这个进入锁池,可以理解为一种阻塞状态)。
创建线程的方式-01
继承Thread类
-
自定义一个MyThread类,用来继承与Thread类
-
在MyThread类中重写run()方法
-
在测试类中创建MyThread类的对象
-
启动线程
package org.example;
/**
* @author TFY
*/
public class MyThread_1 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread("线程3");
//开启线程
// t01.run();
// t02.run();
// t03.run();
myThread1.start();
myThread2.start();
myThread3.start();
/** run不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。*/
//设置线程名(补救的设置线程名的方式)
myThread1.setName("线程1");
myThread2.setName("线程2");
//Thread.currentThread() 获取当前正在执行线程的对象(主线程)
//这里的主线程在Java虚拟机(JVM)启动时,它会创建一个名为“main”的线程,该线程成为主线程。
// 主线程负责执行应用程序的 main() 方法。
//一旦 main() 方法执行完毕,主线程就会终止,主线程负责执行应用程序的 main() 方法。
//主线程与myThread1、myThread1、myThread1线程之间是父线程和子线程的关系。
Thread.currentThread().setName("主线程"); //这里可以为主线程重新命名
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
//类继承thread
class MyThread extends Thread{
public MyThread(){
}
public MyThread(String name){
super(name);
}
//run方法是每个线程运行过程中都必须执行的方法
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(this.getName() +":" + i);
}
}
}
此处最重要的为start()方法。单纯调用run()方法不会启动线程,不会分配新的分支栈。
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法(由JVM线程调度机制来运作的),并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
单纯使用run()方法是不能多线程并发的。
多线程的实现方式-02
实现Runable接口
-
自定义一个MyRunnable类来实现Runnable接口
-
在MyRunnable类中重写run()方法
-
创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去
-
启动线程
package org.example;
/**
* @author TFY
*/
public class MyThread_2 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();//将一个任务提取出来,让多个线程共同去执行
//封装线程对象
Thread thread1 = new Thread(myRunnable,"线程1");
Thread thread2 = new Thread(myRunnable,"线程2");
Thread thread3 = new Thread(myRunnable,"线程3");
//开启线程
thread1.start();
thread2.start();
thread3.start();
//通过匿名内部类的方式创建线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
},"线程4").start();
}
}
//自定义线程类,实现Runnable接口
//这并不是一个线程类,是一个可运行的类,它还不是一个线程。
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
Runnable 和 Thread 在 Java 中都是用于创建多线程的接口和类,但它们之间存在一些关键的区别:
Runnable
- Runnable 是一个接口,定义了 run() 方法,它指定了线程执行的任务。
- Runnable 对象本身不是线程;它需要由 Thread 类实例化才能实际运行。
- Runnable 对象可以由多个线程共享,从而允许并行执行任务。
Thread
- Thread 是一个类,它实现了 Runnable 接口并提供了创建和管理线程所需的所有功能。
- Thread 对象本身就是一个线程,它具有状态、优先级和堆栈跟踪等信息。
- Thread 对象不能被共享,因为它代表一个特定线程的执行流。
通常,对于简单的任务,使用 Runnable 已经足够。但是,对于更复杂或需要对线程进行更精细控制的情况,Thread 是更好的选择。
多线程的实现方式-03
实现Callable接口( java.util.concurrent.FutureTask; /JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。)
-
自定义一个MyCallable类来实现Callable接口
-
在MyCallable类中重写call()方法
-
创建FutureTask,Thread对象,并把MyCallable对象作为FutureTask类构造方法的参数传递进去,把FutureTask对象传递给Thread对象。
-
启动线程
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
package org.example;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author TFY
*/
public class MyThread_3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第一步:创建一个“未来任务类”对象
// 参数非常重要,需要给一个Callable接口实现类对象
FutureTask futureTask = new FutureTask(new Callable() {
// call()方法就相当于run方法。只不过这个有返回值
@Override
public Object call() throws Exception {
System.out.println("call方法开始");
Thread.sleep(1000);
System.out.println("call方法结束");
int a = 100;
int b = 300;
return a + b; //自动装箱(int变成Integer)
}
});
//创建线程对象
Thread thread = new Thread(futureTask);
//开启线程
thread.start();
// 这里是main方法,这是在主线程中。
// 在主线程中,怎么获取t线程的返回结果?
// get()方法的执行会导致“当前线程阻塞”
Object object = futureTask.get();
System.out.println("线程执行结果:" + object);
// main方法这里的程序要想执行必须等待get()方法的结束
// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
// 另一个线程执行是需要时间的。
System.out.println("hello world!");
}
}
线程控制
方法名 | 说明 |
---|---|
void yield() | 使当前线程让步,重新回到争夺CPU执行权的队列中 |
static void sleep(long ms) | 使当前正在执行的线程停留指定的毫秒数 |
void join() | 当前线程等待另一个调用join()方法的线程执行结束后再往下执行 |
void interrupt() | 终止线程睡眠 |
yield()方法
暂停当前正在执行的线程对象,并执行其他线程yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。注意:在回到就绪之后,有可能还会再次抢到。
package org.example;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author TFY
*/
public class MyThread_4 {
public static void main(String[] args){
MyYield myYield1 = new MyYield("线程1");
MyYield myYield2 = new MyYield("线程2");
MyYield myYield3 = new MyYield("线程3");
//开启线程
myYield1.start();
myYield2.start();
myYield3.start();
}
}
class MyYield extends Thread{
public MyYield(){
}
public MyYield(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
if (i == 2){
Thread.yield();//当循i环到2时,让线程让步
//1. 回到抢占队列中,又争夺到了执行权
//2. 回到抢占队列中,没有争夺到执行权
}
System.out.println(this.getName() + ":" + i);
}
}
}
sleep()方法
谁执行谁就是当前线程
package org.example;
/**
* @author TFY
*/
public class MyThread_5 {
public static void main(String[] args){
MySleep mySleep1 = new MySleep("线程1");
MySleep mySleep2 = new MySleep("线程2");
MySleep mySleep3 = new MySleep("线程3");
//开启线程
mySleep1.start();
mySleep2.start();
mySleep3.start();
}
}
class MySleep extends Thread{
public MySleep(){}
public MySleep(String name){
super(name);
}
// 重点:run()当中的异常不能throws,只能try catch
// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(this.getName() + ": " + i);
try {
Thread.sleep(1000);//让当前正在执行的线程睡眠指定毫秒数
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
join()方法
package org.example;
/**
* @author TFY
*/
public class MyThread_7 {
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程线程A开始运行...");
ThreadB threadB = new ThreadB();
threadB.setName("ThreadB");
threadB.start();
ThreadC threadC = new ThreadC();
threadC.setName("ThreadC");
threadC.start();
threadB.join();
System.out.println("主线程线程A结束运行");
}
}
class ThreadB extends Thread {
@Override
public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + "休眠:5s");
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + "休眠结束");
}
}
class ThreadC extends Thread{
@Override
public void run() {
System.out.println("线程:" + Thread.currentThread().getName() + "休眠:1s");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + "休眠结束");
}
}
interrupt()方法和stop()方法
package org.example;
import javax.imageio.plugins.tiff.TIFFImageReadParam;
/**
* @author TFY
*/
public class MyThread_6 {
public static void main(String[] args){
Thread thread = new Thread(new MyRunnable2());
thread.start();
thread.setName("线程1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制)
thread.interrupt();
//thread.stop();//强行终止线程,缺点:容易损坏数据 线程没有保存的数据容易丢失
}
}
class MyRunnable2 extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin <---");
try {
//睡眠365秒
Thread.sleep(1000*365);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "---> end <---");
}
}
推荐的终止线程
package org.example;
/**
* @author TFY
*/
public class MyThread_9 {
public static void main(String[] args) {
MyRunable3 myRunable3 = new MyRunable3();
Thread thread = new Thread(myRunable3);
thread.setName("t");
thread.start();
// 模拟5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程
// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
myRunable3.run = false;
}
}
class MyRunable3 implements Runnable {
// 打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// return就结束了,你在结束之前还有什么没保存的。
// 在这里可以保存呀。
//save....
//终止当前线程
return;
}
}
}
}
线程的调度
-
线程调度模型
- 均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。
Java中JVM使用的就是抢占式调度模型
-
getPriority()
:获取线程优先级 -
setPriority
:设置线程优先级
package org.example;
/**
* @author TFY
*/
public class MyThread_8 {
public static void main(String[] args) throws InterruptedException {
//创建线程
MyPriority thread1 = new MyPriority("线程1");
MyPriority thread2 = new MyPriority("线程2");
MyPriority thread3 = new MyPriority("线程3");
//获取线程优先级,默认是5
System.out.println("thread1的优先级"+ thread1.getPriority());
System.out.println("thread2的优先级"+ thread2.getPriority());
System.out.println("thread3的优先级"+ thread3.getPriority());
//设置线程优先级;
thread1.setPriority(Thread.MIN_PRIORITY); //低
thread2.setPriority(Thread.NORM_PRIORITY); //中
thread3.setPriority(Thread.MAX_PRIORITY); //高
//获取线程优先级
System.out.println("--------------------");
System.out.println("thread1的优先级"+ thread1.getPriority());
System.out.println("thread2的优先级"+ thread2.getPriority());
System.out.println("thread3的优先级"+ thread3.getPriority());
//开启线程
thread1.start();
thread2.start();
thread3.start();
}
}
class MyPriority extends Thread{
public MyPriority(){}
public MyPriority(String name){
super(name);
}
// 重点:run()当中的异常不能throws,只能try catch
// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(this.getName() + ": " + i);
}
}
}
线程的安全
因为多个线程时共享地址空间的,也就是很多资源是共享的。优点是线程间的通信非常方便,缺点是缺乏访问的控制。因为一个线程的操作问题,给其他线程造成了不可控或者引起崩溃异常等,这种现象称之为线程安全。
产生线程安全的原因是多个线程可以同时共享一些资源,比如堆等。想避免线程安全问题就需要对资源进行访问控制。
- 是否具备多线程的环境
- 是否有共享数据
- 是否有多条语句操作共享数据
- 例如:我和小明同时取一个账户的钱,我取钱后数据还没来得及返回给服务器,小明又取了,这个时候小明的余额还是原来的。
- 如何解决?线程排队执行(不能并发),线程同步机制。
例如
package org.example;
/**
* @author TFY
*/
public class MyThread_11 {
public static void main(String[] args) {
Account account = new Account(1000);
// 创建两个线程
MyAccont myAccont1 = new MyAccont(account);
MyAccont myAccont2 = new MyAccont(account);
myAccont1.setName("线程1");
myAccont2.setName("线程2");
myAccont1.start();
myAccont2.start();
}
}
//类继承thread
class MyAccont extends Thread{
private Account account;
public MyAccont(Account account){
this.account = account;
}
// run方法的执行表示取款操作。
// 假设取款200
@Override
public void run() {
double money = 200;
account.getMoney(money);
System.out.println(this.getName() + "取走了200" + "还剩" + account.getMoney());
}
}
//账户信息
class Account {
private double money;
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void getMoney(double money){
double before = this.getMoney(); //取钱之前余额
double after = before - money; //取钱之后余额
//模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.setMoney(after);
}
}
输出:
可以看出 线程2 取了200后剩800, 那 线程1 取了200后应该还剩600才对. 原因是因为 线程2在网络延迟时线程1已经开始run,此时线程2虽然取出了200但还来得及将取出的信息传达给setMoney, 此时的money还是1000,于是线程1还是从1000上取200
互斥与同步
要保证线程安全,就需要线程之间是互斥和同步的。下面介绍几个概念来引出互斥和同步的概念。
临界资源:凡是被线程共享访问的资源都是临界资源(多线程,多进程都有临界资源。比如多个进程向显示器打印数据,显示器就是临界资源)
临界区:代码中访问临界资源部分的代码。因此,对临界区的保护本质上就是对临界资源的保护。(通过互斥和同步实现)
互斥:在任意时刻,只允许一个执行流访问某段代码(访问某部分资源),称之为互斥!
原子性:一段代码要么不执行,要么执行完毕。称这段代码具有元祖性
同步:一般而言,让访问临界资源的过程在安全的前提下(互斥并且原子的),让访问的资源具有一定的顺序性。
同步机制有4种实现方式:(部分引用网上资源)
- ThreadLocal
- synchronized( ) :线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时(正常返回,或者是异常退出)会自动释放锁。
- wait() 与notify()
- volatile(): 具体可参考 [Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)
synchronized( )
同步语句块:synchronized(this){方法体} (synchronized括号后的数据必须是多线程共享的数据,才能达到多线程排队)
package org.example;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author TFY
*/
public class MyThread_11 {
public static void main(String[] args) {
Account account = new Account(1000);
// 创建两个线程
MyAccont myAccont1 = new MyAccont(account);
MyAccont myAccont2 = new MyAccont(account);
myAccont1.setName("线程1");
myAccont2.setName("线程2");
myAccont1.start();
myAccont2.start();
}
}
//类继承thread
class MyAccont extends Thread{
private Account account;
public MyAccont(Account account){
this.account = account;
}
// run方法的执行表示取款操作。
// 假设取款200
@Override
public void run() {
double money = 200;
account.getMoney(money);
System.out.println(this.getName() + "取走了200" + "还剩" + account.getMoney());
}
}
//账户信息
class Account {
private double money;
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void getMoney(double money){
// 以下代码的执行原理?
// 1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
// 2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
// 找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
// 占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
// 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
// 共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
// 直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
// t2占有这把锁之后,进入同步代码块执行程序。
//
// 这样就达到了线程排队执行。
// 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
// 执行的这些线程对象所共享的。
synchronized (this){
double before = this.getMoney();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setMoney(after);
}
}
//或者 静态同步方法:修饰符 synchronized static 返回值类型 方法名(形参列表){方法体}
//缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,
// 可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。
// public synchronized void getMoney(double money) {
// double before = this.getMoney();
// // 取款之后的余额
// double after = before - money;
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// // 更新余额
// this.setMoney(after);
// }
}
Synchronized锁原理和优化
Synchronized是通过对象头的markwordk来表明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,成本很高,此时这种锁叫重量级锁,在JDK1.6以后引入了偏向锁、轻量级锁、重量级锁
-
偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁
-
轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会升级为轻量级锁。线程会通过CAS方式获取锁,不会阻塞,提高性能,
-
重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低
注意,锁只能升级不能降级
wait()和notify()方法
wait和notify方法不是线程对象的方法,是java中任何一个java对象
wait()方法作用:
Object o = new Object();
o.wait(); 表示:
让正在o对象上活动的线程进入等待状态,无期限等待,
直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上
活动的线程)”进入等待状态。notify()方法作用:
Object o = new Object();
o.notify(); 表示:
唤醒正在o对象上等待的线程。
还有一个notifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。
注意:wait方法和notify方法需要建立在synchronized线程同步的基础之上。
重点:o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁
o.notify()方法只会通知,不会释放之前占有的o对象的锁。
生产者与消费者模式是并发、多线程编程中经典的设计模式,通过wait和notifyAll方法实现。
例如:生产满了,就不能继续生产了,必须让消费线程进行消费。
消费完了,就不能继续消费了,必须让生产线程进行生产。
package org.example;
/**
* @author TFY
*/
public class MyThread_13 {
public static void main(String[] args) {
Box box = new Box();
Producer producer = new Producer(box); //生产者对象
Customer customer = new Customer(box); //消费者对象
Thread thread1 = new Thread(producer); //创建生产者线程
Thread thread2 = new Thread(customer); //创建消费者线程
thread1.start();
thread2.start();
}
}
//牛奶箱
class Box{
private int milk; //牛奶数
private boolean state = false; //默认奶箱为空
public void put(int milk){
synchronized (this){
if (state){ //奶箱中有牛奶
try {
wait(); //等待,需要被唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.milk = milk;
System.out.println("李逵放入" + this.milk + "瓶牛奶");
this.state = true;
notifyAll(); //唤醒所有等待的线程
}
}
public void get(){
synchronized (this){
if (!state){ // 奶箱中没有奶时
try {
wait(); //等待,需要被唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("林冲拿走" + this.milk + "瓶牛奶");
this.state = false; //奶箱没有牛奶
notifyAll(); 唤醒所有等待的线程
}
}
}
//生产者:生产力牛奶
class Producer implements Runnable{
private Box box;
public Producer(Box box) {
this.box = box;
}
@Override
public void run() {
//放入牛奶
for (int i = 1; i < 9; i++) {
box.put(i);
}
}
}
//消费者:消费牛奶
class Customer implements Runnable{
private Box box;
public Customer(Box box){
this.box = box;
}
@Override
public void run() {
while (true){
box.get(); //消费者取牛奶
}
}
}
sleep、join、yield、wait区别
- sleep 不释放当前对象监视器的锁、释放cpu
- join 释放对象监视器的锁、抢占cpu
- yield 不释放对象监视器的锁、释放cpu
- wait 释放对象监视器的锁、释放cpu
记住一句话:cpu是非常宝贵的,所以只有running的时候才会获取CPU时间片。
join底层还是wait()实现的
,会释放锁,进入waiting状态(简单说,在哪个线程的线程体中调用join,哪个线程就会进入等待状态,并且释放锁)
补充:
sleep()和wait()的区别
(1)wait()是Object的方法,sleep()是Thread类的方法
(2)wait()会释放锁,sleep()不会释放锁
(3)wait()要在同步方法或者同步代码块中执行,sleep()没有限制
(4)wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒
yield()和join()区别
yield()调用后线程进入就绪状态
A线程中调用B线程的join() ,则B执行完前A进入阻塞状态
变量对线程安全的影响
-
实例变量:在堆中。
-
静态变量:在方法区。
-
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈。)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
守护线程
- 用户线程(User thread)
平常创建的普通线程。
- 守护线程(Daemon thread)
守护线程就是运行在程序后台的线程,程序的主线程Main(main方法)不是守护线程,必须等所有的Non-daemon线程都运行结束了,只剩下daemon的时候,JVM才会停下来。通过在一个线程对象上调用setDaemon(true),可以将user线程创建的线程明确地设置成Daemon线程。
package org.example;
/**
* @author TFY
*/
public class MyThread_10 {
public static void main(String[] args) {
MyGuard myGuard = new MyGuard();
myGuard.setName("守护线程" );
///设置为守护线程
myGuard.setDaemon(true);
myGuard.start();
//主线程(main)/用户线程
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() +"---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class MyGuard extends Thread {
int i = 0;
@Override
public void run() {
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while (true){
System.out.println(this.getName() +"---->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
锁(Lock)
介绍锁(Lock)之前我们先介绍AQS(Lock是通过AQS实现的)
什么是AQS锁
AQS是一个抽象类,可以用来构造锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。
AQS的原理是,AQS内部有三个核心组件,一个是state代表加锁状态初始值为0,一个是获取到锁的线程,还有一个阻塞队列。当有线程想获取锁时,会以CAS的形式将state变为1,CAS成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state是不是0,不是0再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的
可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程
PS: CAS介绍
CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是
(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
(2)CAS频繁失败导致CPU开销大
为什么AQS使用的双向链表 :因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点
常见的AQS锁
AQS分为独占锁和共享锁
-
ReentrantLock(独占锁):可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队
-
Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。
应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票
- CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。
应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
- CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值
应用场景:多线程计算数据,最后合并计算结果的应用场景
Lock定义
Java的锁是一种同步机制,用于在多个执行线程的环境中强制对资源的访问限制。锁为同步处理的常见方法。
- 虽然叫做锁,但是其实相当于
临界区大门的一个钥匙
,那把钥匙就放到了临界区门口,有人进去了就把钥匙拿
走揣在了身上,结束之后会把钥匙还回来
。只有拿到了指定临界区的锁,才能够进入临界区,访问临界区资源
,当离开临界区时释放锁
,其他线程才能够进入临界区 - 而对于锁本身,也是一种临界资源,是不允许多个线程共同持有的,同一时刻,只能够一个线程持有;
Java中任何一个对象都可以被当做锁
在Java对象头中有一部分数据用于记录线程与对象的锁之间的关系,通过这个对象锁,进而可以控制线程对于对象的互斥访问,在JVM中每个对象中都拥有这样的数据
- 如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(也就是在指定的内存区域中进行一些数据的写入)
一个线程拥有了一个对象的锁之后,他就可以再次获取锁,也就是经常说的可重入
package org.example;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author TFY
*/
public class MyThread_11 {
public static void main(String[] args) {
Account account = new Account(1000);
// 创建两个线程
MyAccont myAccont1 = new MyAccont(account);
MyAccont myAccont2 = new MyAccont(account);
myAccont1.setName("线程1");
myAccont2.setName("线程2");
myAccont1.start();
myAccont2.start();
}
}
//类继承thread
class MyAccont extends Thread{
private Account account;
public MyAccont(Account account){
this.account = account;
}
// run方法的执行表示取款操作。
// 假设取款200
@Override
public void run() {
double money = 200;
account.getMoney(money);
System.out.println(this.getName() + "取走了200" + "还剩" + account.getMoney());
}
}
//账户信息
class Account {
private double money;
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
private Lock lock = new ReentrantLock();
public void getMoney(double money){
lock.lock(); //上锁
double before = this.getMoney(); //取钱之前余额
double after = before - money; //取钱之后余额
//模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.setMoney(after);
lock.unlock();//解锁
}
}
线程阻塞和线程等待的区别
-
线程阻塞(BLOCKED) : 一个处于
就绪状态(Running)
的线程尝试去获取锁,但锁已经被其他线程占用
, 导致当前线程被阻塞的状态(synchronize
关键字产生的状态)进入BLOCKED状态的只有
synchronize
关键字,ReentrentLock.lock()底层调用的是LockSupport.park()
,因此ReentrentLock.lock()进入的是WAITING
状态 -
线程等待(WAITING) : 一个线程
已经获取到了锁
,但是需要等待其他线程执行某些操作。时间不确定
当钱线程调用wait,join,park
方法时,进入WAITING状态。(前提是这个线程已经拥有锁
) -
超时等待(TIMED_WAITING) : 一个线程
已经获取到了锁
,但是需要等待其他线程执行某些操作。时间确定
通过sleep(int timeout)
或Wait(int timeout)
方法进入的限时等待的状态)
实际上可以不用区分两者, 因为两者都会暂停线程的执行
.
两者的区别是:
- 进入WAITING状态是线程
主动
的, 而进入BLOCKED状态是被动
的.- 更进一步的说, 进入BLOCKED状态是在
同步代码块之外
的, 而进入WAITING状态是在同步代码块之内
.
死锁
- 线程死锁是指由于两个或者两个以上的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
避免死锁的原则: 顺序上锁,反向解锁,不要回头
package org.example;
/**
* @author TFY
*/
public class MyThread_12 {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
//进入同步代码块之后线程1获取先获取到o1的对象锁,
//接下来线程1想要获取o2的对象锁,但是线程2已经获取了o2的对象锁,所以会出现死锁
synchronized (o1){
System.out.println(this.getName()+ "已经获取到o1,正在获取o1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("看看我执行了吗?");
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
//进入同步代码块之后线程2获取先获取到o2的对象锁,
//接下来线程2想要获取o1的对象锁,但是线程1已经获取了o1的对象锁,所以会出现死锁
synchronized (o2){
System.out.println(this.getName()+ "已经获取到o2,正在获取o1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("看看我执行了吗?");
}
}
}
}
线程池
概念
线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务
工作机制
在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交个某个空闲线程。
为什么使用线程池
多线程运行时,系统不断创建和销毁新的线程,成本非常高,会过度的消耗系统资源,从而可能导致系统资源崩溃,使用线程池就是最好的选择。
线程池七大参数
-
核心线程数:线程池中的基本线程数量
-
最大线程数:当阻塞队列满了之后,逐一启动
-
最大线程的存活时间:当阻塞队列的任务执行完后,最大线长的回收时间
-
最大线程的存活时间单位
-
阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列
-
线程工厂:用于生产线程
任务拒绝策略:阻塞队列满后,拒绝任务有四种策略
(1)抛异常
(2)丢弃任务不抛异常
(3)打回任务
(4)尝试与最老的线程竞争
可重用线程
方法名 | 说明 |
---|---|
Executors.newCacheThreadPoll(); | 创建一个可缓存的线程池 |
execute(Runnable run) | 启动线程池中的线程 |
package org.example;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
/**
* @author TFY
*/
public class MyThread_14 {
public static void main(String[] args) {
//1.创建一个大小为5的线程池
// ExecutorService threadPool= Executors.newFixedThreadPool(5);
//创建单个线程的线程池?为啥不直接创个线程?
//
//线程池的优点:
//1.复用线程:不必频繁创建销毁线程
//2.也提供了任务队列和拒绝策略
//newSingleThreadScheduledExecutor:创建执行定时任务的单个线程的线程池
//ScheduledExecutorService service= Executors.newSingleThreadScheduledExecutor();
//newScheduledThreadPool:创建执行定时任务的线程池
//ScheduledExecutorService service = Executors.newScheduledThreadPool(5);//5个线程
//newWorkStealingPool:根据当前设备的配置自动生成线程池
//ExecutorService service= Executors.newWorkStealingPool();
//带缓存的线程池,适用于短时间有大量任务的场景,但有可能会占用更多的资源;线程数量随任务量而定。
ExecutorService threadPoll = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
//如果不睡眠,那么第一个执行完的线程无法及时成为空闲线程,那么线程池就会让一个新的线程执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//每次循环开启一个线程
threadPoll.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在被执行~");
}
});
}
threadPoll.shutdown();// 关闭线程池
//线程池是无限大,当执行当前任务时,上一个任务已经完成,会重复执行上一个任务的线程,而不是每次使用新的线程
}
}
根据 CPU 核心数设计线程池线程数量
IO 密集型:线程中十分消耗Io的线程数*2
CPU密集型: cpu线程数量
多线程并发的线程安全问题
package org.example;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author TFY
*/
public class MyThread_15 {
//定义静态变量
static int a=0;
static int count=2000;
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<count;i++){
service.execute(new Runnable() {
@Override
public void run() {
a++;
}
});
}
// 关闭线程池
service.shutdown();
System.out.println(a);
}
}
以上a运行并没有达到预期的2000,此处多线程并发,a共享,所以没达到2000
package org.example;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author TFY
*/
public class MyThread_15 {
static int a = 0;
static int count = 2000;
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
//闭锁 在一些条件下可放开 参数:加多少把锁
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
service.execute(new Runnable() {
@Override
public void run() {
a++;
//解一把锁
countDownLatch.countDown();
}
});
}
service.shutdown();
//会进入阻塞状态 什么时候把锁全解了 阻塞状态才会解除
countDownLatch.await();
System.out.println(a);
}
}
CountDownLatch
仅用于跟踪线程的完成情况,而线程中的代码并没有同步访问共享变量 a
。因此,多个线程可以同时递增 a
,导致最终结果小于预期的 2000。
解决方法
package org.example;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author TFY
*/
public class MyThread_15 {
static int a = 0;
static int count = 2000;
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
//闭锁 在一些条件下可放开 参数:加多少把锁
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
service.execute(new Runnable() {
@Override
public void run() {
//同步机制
synchronized (MyThread_15.class) {
a++;
//解一把锁
countDownLatch.countDown();
}
}
});
}
service.shutdown();
//会进入阻塞状态 什么时候把锁全解了 阻塞状态才会解除
countDownLatch.await();
System.out.println(a);
}
}
Synchrpnized和lock的区别
(1)synchronized是关键字,lock是一个类
(2) synchronized在发生异常时会自动释放锁,lock需要手动释放锁
(3)synchronized是可重入锁、非公平锁、不可中断锁,lock的ReentrantLock是可重入锁,可中断锁,可以是公平锁也可以是非公平锁
(4)synchronized是JVM层次通过监视器实现的,Lock是通过AQS实现的