一、如何解决线程安全问题
1.什么情况会出现线程安全问题
- 是否多线程环境
- 是否有共享资源
- 是否有多条语句操作共享资源
2.同步锁
2.1同步代码块
锁对象:任意对象
语法:
synchronized (obj){
//操作共享数据的代码
}
2.2同步方法
同步方法的锁对象是this
public synchronized void sellTicket(){
if(ticket>0){ // c1 c2 c3 ticket = 1
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"第"+ticket+"张票");
ticket--;// 0
}
}
2.3同步静态方法
同步方法的锁对象是当前类的字节码对象(反射会学习)
public synchronized static void sellTicket(){
if(ticket>0){ // c1 c2 c3 ticket = 1
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"第"+ticket+"张票");
ticket--;// 0
}
}
3.Lock锁
public void run() {
while (true){
lock.lock(); //上锁
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"第"+ticket+"张票");
ticket--;// 0
}
lock.unlock(); //释放锁
}
}
4.synchronized与lock的区别
synchronized jvm级别的锁,jvm自动上锁和解锁
Lock锁java代码写的锁,需要手动的加锁和释放锁
5.死锁
多线程产生死锁的四个必要条件:
-
互斥条件:一个资源每次只能被一个进程使用。
-
保持和请求条件:一个进程因请求资源而阻塞时,对已获得资源保持不放。
-
不可剥夺调用:进程已获得资源,在未使用完成前,不能被剥夺。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
public class MyThread extends Thread{
private boolean flag;
public MyThread(Boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (LockObject.objA) { //
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("我有锁A,需要锁B");
synchronized (LockObject.objB) {
System.out.println(Thread.currentThread().getName() + ":我有锁B");
}
}
}else{
synchronized (LockObject.objB) { //
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("我有锁A,需要锁B");
synchronized (LockObject.objA) {
System.out.println(Thread.currentThread().getName() + ":我有锁B");
}
}
}
}
}
public class LockObject {
public static Object objA = new Object();
public static Object objB = new Object();
}
public class Example02 {
public static void main(String[] args) {
MyThread thread = new MyThread(true);
MyThread thread2 = new MyThread(false);
thread.start();
thread2.start();
}
}
二、线程的生命周期图
-
线程的状态:
新建:创建线程对象
就绪:有执行资格,但是没有拿到执行权
运行:有执行资格,且有执行权
阻塞: 没有执行资格,没有执行权; 恢复阻塞回到就绪状态
死亡: 线程变成垃圾,等待回收
三、线程间的通信
正常思路:
生产者:
先看有没有数据,有就等待,没有就生产,生产后,通过消费者来消费数据
消费者:
先看有没有数据,有就消费,没有就等待,且通知生产都生产数据
为了实现上述的问题,Java提供一种机制,等待唤醒机制
案例:
一个创建学生对象,一个显示学生对象的信息
分析:
Student 对象 (name,age)
SetStudent对象 (生产者)
GetStudent对象(消费者)
StudentDemo(测试类)
线程间通信的内存图
常见情况:
- 新建 – 就绪 – 运行 – 死亡
- 新建 – 就绪 – 运行 – 就绪 – 运行 – 死亡
- 新建 – 就绪 – 运行 – 等待阻塞 – 同步阻塞 – 就绪 – 运行–死亡
- 新建 – 就绪 – 运行 – 其它阻塞 – 就绪 – 运行–死亡
- 新建 – 就绪 – 运行 – 同步阻塞 – 就绪 – 运行–死亡
四、线程池
使用线程池的思想,池化思想可以提高重用性和效率(Executors)
//Example01类
package com.Demo04;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author WavesWei
* @version 1.0
* @date:2023/11/27 15:43
* @abstract 关于这个源代码文件的一些基本描述。
*/
public class Example01 {
public static void main(String[] args) {
//创建一个固定大小的 线程池
ExecutorService service = Executors.newFixedThreadPool(5);
MyCallable myCallable1 = new MyCallable();
MyCallable myCallable2 = new MyCallable();
service.submit(myCallable1);
service.submit(myCallable2);
service.shutdown();
}
}
//MyCallable继承Callable
package com.Demo04;
import java.util.concurrent.Callable;
/**
* @author WavesWei
* @version 1.0
* @date:2023/11/27 15:46
* @abstract 关于这个源代码文件的一些基本描述。
*/
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i =0;i<5;i++){
System.out.println("callable "+i);
}
return null;
}
}