多线程基础---线程安全(原因及解决方法),syncronized,volatile关键字
作者:互联网
目录
1. 中断线程
1) 使用标志位的方式
isStop变量.
public class FlagStop {
private static volatile boolean isStop;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
try {
while (!isStop) {
System.out.println("转账....");
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}).start();
//停止转账
Thread.sleep(3000);
isStop=true;
}
}
2) 线程中断的API
前置: 线程Thread中,存在一个中断标志位,默认值=false(没有被中断)
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()){
System.out.println("转账...");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
2. 线程等待
1) API
线程引用对象.join() / 线程引用对象.join(long)
当前线程阻塞等待,直到满足以下条件:
① 无参: 线程引用执行完毕
② 有参: 等待指定时间,或者线程引用执行完毕,任意一个条件满足,当前线程继续向下执行.
2) 示例
public class LearnJoin {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
try{
System.out.println("t start");
Thread.sleep(5000);
System.out.println("t end");
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
t.join(2000);
System.out.println("main");
}
}
3. 守护线程
1) API
参数为:
true: 设置为守护线程;
false: 设置为用户线程.
2) 一个Java进程,至少有一个用户线程存在,才会运行,否则就结束.
4. Java中的线程状态
这里的等待,超时等待,阻塞状态,和之前学习的进程状态转换图,阻塞状态,表现是一样的,线程暂停/挂起.
5. 线程安全
1) 背景/原因
代码层面上: 多个线程对同一个共享数据的操作(读,写),如果有写操作,就存在线程安全问题.
底层的原因: 原子性,可见性,有序性.
① 原子性
- 一系列的操作(多行代码指令的执行),需要具有不可分割的特性,就是原子性.
- 如果可以被分割,指令之间,可能有其他线程并发并行执行的操作,对同一个共享变量操作就会产生影响.
特殊的非原子性操作:
a) n++,n--,++n,--n
从主存把数据读取到CPU (读)
进行数据的更新 (改)
把数据写回到主存 (写回)
b) new一个对象
一行new对象的代码,分解为:
I. 分配内存空间(Java虚拟机)
II. 执行构造方法(字节码层面的<init>构造方法: 收集成员变量,实例代码块,Java代码中的构造方法)
III. 把new的对象赋值给变量
② 可见性
线程对主存数据的操作,需要先加载到线程私有的工作内存,操作完再写回到主存,线程之间的工作内存,是互相不可见的.
共享变量,存储于主存(线程共享区域),线程对变量的操作:
a) 加载到工作内存
b) 操作(赋值,修改)
c) 写回主存
③ 重排序
多行指令(字节码指令在Java虚拟机执行,及机器码指令在CPU执行)在执行时,可能进行优化(目的是提高执行效率),只是不能重排序有关联的指令.
比如洗苹果,吃苹果,这两条指令由于具有前后关系,所以就不能重排序.洗苹果,洗梨,就可以进行重排序.
2) 线程安全的解决
线程安全,是由于多个线程对共享变量的操作,并发并行执行的结果.(共享变量称为临界资源,这种代码(多行),称为临界区)
思路: 临界区代码执行时,先申请jvm加锁,然后再执行. 申请锁,是需要同一把锁来保证线程安全;申请失败,线程则需要等待(可以是阻塞式的,也可以是非阻塞式)
这种操作,在多个线程执行临界区都申请同一把锁的情况下,多个线程运行的结果,就表现为: 多个线程,依次执行临界区代码.
假如现在我(线程)----申请把餐厅101包间加锁---吃饭,喝水(临界区: 此时作为不可分割的最小执行单位)----归还锁---通知其他要在101做事的人(其他线程)---重复以上流程.
6. syncronized 同步关键字
1) 作用:
基于对象头加锁的方式,只要申请同一把锁加锁的线程,都有同步互斥的作用.
- 对象: 某个对象加锁(本质是对象在内存中的对象头加锁)
- 同一把锁: 是否为同一个对象加锁.
- 同步互斥: 多个线程之间执行synchronized作用域范围的临界区代码,是一个一个依次执行.
2) 语法:
- ①静态同步方法: 使用的锁为当前的类对象
- ②实例同步方法: 使用的锁为当前调用的实例对象(this)
- ③同步代码块: 使用的锁,为小括号中的对象
语法上对应申请锁-----加锁----释放锁
synchronized代码行: 线程申请某个对象锁
synchronized作用域结束(花括号结束): 自动释放对象锁.
3) 特别注意的事项:
多线程只有申请同一个对象的加锁,才具有同步互斥的效果.
不是对同一个对象加锁,并发并行的执行.(部分A加同一把锁,部分B不加锁,A中的多个线程同步互斥,但是B和A,B内部的多个线程,都是并发并行)
7. volatile
1) 作用
①保证变量(分解为字节码以后的)的可见性.
②建立一个内存屏障,禁止指令重排序.
2) 使用场景:
代码行本身保证原子性的前提下,变量使用volatile修饰,可以保证线程安全.
关于代码行本身保证原子性的理解:
①读(从主存读取到线程的工作内存)
②修改为一个常量值(把常量值写回主存)
标签:加锁,Thread,对象,---,线程,new,多线程,public,syncronized 来源: https://blog.csdn.net/weixin_43939602/article/details/117441171