文章目录
1.线程的状态
- 可运行态:就绪态+可运行态
- java进程无法决定线程在某个时间点一定处于运行态(由系统调度决定)
- 有三个阻塞态,线程表现得特性都是相同得,线程暂停/挂起来, 划分为三种状态,只是jdk/底层实现有区别。
- Java线程得状态:
- NEW
- RUNNABLE 可运行态:就绪态+运行态
- BLOCKED
- WAITING
- TIMED——WAITING
- TERMINATED
2.线程安全
- 线程安全的概念:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
- 多线程是同时并发并行执行多行代码指令,要考虑所有情况都符合我们的预期,才是安全的。
2.1线程不安全原因
- 运行一个 java进程(系统分配java进程的内存空间),会启动main线程执行main函数,加载class字节码到java进程的内存,java程序(java虚拟机),执行字节码指令(一行一行的翻译到机器码,CPU执行机器码)
- 内存(栈,堆,方法区)
- 栈:(线程私有)的包含局部变量表(局部变量+基础类型的值)
- 堆:(线程共享的),包含对象和数组(虽然是共享,但是线程能不能用,取决于线程是否能够获取到对象的引用)
- 方法区(线程共享):方法区和Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(字符串常量池+静态变量,jdk1.7之前在方法区里面,在1.7之后在堆里面)
2.1.1线程不安全从代码层面上
- 代码层面的:多个变量对同一个变量进行操作(读,写),有一个写操作,就有线程安全的问题。
public class UnsafeThread {
private static int n =0;
public static void main(String[] args) {
for(int i =0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i =0;i<1000;i++) {
n++;
}
}
}).start();
}
//等待子线程执行完毕,在打印多个变量++计数的值
while(Thread.activeCount()>1) Thread.yield();
System.out.println(n);
}
}
2.1.2原子性(从原理上考虑)
- 原子性:多行代码指令,执行时,是一组不可再分的最小执行单位,多个线程同时并发并行的执行代码指令,可能在一个线程操作一个共享变量时,是由前后的依赖关系,指令间有其他线程的操作,就会导致线程不安全。
- 原子性表达出一组操作需要具有不可拆分(其他线程并发并行在中间插入指令),这种现象也叫做同步互斥,表示在操作的时候是互相排斥的。
- 注意特殊的非原子行操作(java代码表示为一行代码,可能在真正运行的时候,是多行指令)
- n++,n–,--n,++n
- 分解为三步:
- 从主存读取变量值(int tmp =从主存读取的值)
- 修改(tmp=tmp+1)
- 写回主存
- Object o=new Object();
- 分解为3步:
- 分配对象的内存空间(java进程)
- 对象的初始化(成员变量、实例代码块、构造方法,这些会编译成初始化指令)
- 赋值给变量
- long等等64位的变量,不具有原子性。
2.1.3可见性
- 主存:线程都使用的共享区域,对其中变量/对象的操作
- 工作内存:线程之间互相不可见,CPU执行线程中代码指令,是从主存复制到CPU高速缓存。
2.1.4代码顺序性
- 代码重排序:java代码顺序是固定的,但是jvm执行的字节码或cpu执行机器码,都可能重排序指令顺序,目的是提高运行效率。
- 但是重排序是会考虑我们指令的前后的依赖关系。(站在一个线程的角度,看线程的指令中:都是有序的(其实表达的意思,是指运行结果符合预期结果的,内部还是有指令的重排序),站在其他线程的角度,看某个线程的指令执行:都是无序的)
2.2如何解决线程安全问题
- 一组代码,如果存在多线程对共享变量的操作,都需要考虑线程安全问题,一般把多线程操作的共享变量,称为临界资源。一组代码,称为临界区
- 思路:把临界区加锁,多个线程执行临界区的代码,最终的表现是,就是一个线程申请加锁,执行代码,释放锁,其他线程申请失败,需要等待(不一定是阻塞态,也可以是运行态)
- 一个线程依次的执行临界区代码
- 同一个对象:
public static void main(String[] args) {
Person p1 = new Person();
p1.name="ABC";
Person p2 = new Person();
p2.name="ABC";
//判断两个业务是否相等(业务上的)
System.out.println(p1.equals(p2));
//判断是否是同一个对象(引用的堆里面的对象,是不是同一个——
System.out.println(p1==p2);
//判断是否为同一个对象==
}
- Class和类加载的关系
//类加载:new 对象,静态方法调用,类,静态变量,如果没有类加载,先进行类加载
//类加载是在运行期执行的,并且只执行一次
//类加载做的事情:1.把class字节码加载到java进程的方法区
//2.在堆里面,生成一个Class对象(类对象)
Class<Person> c2 = Person.class;
Class<Person> c1 = Person.class;
System.out.println(c1==c2);//相等true
2.3解决多线程安全问题
- synchronized关键字,同步的关键字:依次执行某段代码
- 作用:对同一个对象加锁的线程,造成同步互斥的作用(多个线程依次执行临界区代码),注意:如果不是使用同一个对象加锁,意味着能同时执行(并发并行)
2.3.1在方法上
- 在静态方法上:加锁整个方法,所对象为当前的类对象(Class<当前类型>为类对象)
public class SynchronizedThread {
private static int COUNT;
public static void main(String[] args) {
for(int i =0;i<9;i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i =0;i<1000;i++) {
increment1();
}
}
}).start();
}
//等待子线程执行完毕,在打印多个变量++计数的值
while(Thread.activeCount()>1) Thread.yield();
System.out.println(COUNT);
}
private synchronized static void increment1() {
COUNT++;
}
}
- 在实例方法上:加锁整个方法,锁对象为this(this指的是哪个对象调用该实例方法,this就指谁)
2.3.2同步代码块
- synchronized(锁对象){}
private static void increment2() {
synchronized (SynchronizedThread.class){
COUNT++;
}
}
- 锁对象为:SynchronizedThread.class
- 不同的对象加锁,不会产生同步互斥的效果;
private static void increment3() {
Object o = new Object();
synchronized (o){
COUNT++;
}
}
- 应该改为
static Object o = new Object();
private static void increment3() {
synchronized (o){
COUNT++;
}
}
2.4 synchronized加锁的部分原理
- 本质上是对对象头进行加锁,对同一个对象加锁的线程,是同步互斥的。(一个对象加锁只能由一个线程加锁后,必须释放锁,其他线程才能获取到)。
- 底层实现原理是基于操作系统的锁实现。
- synchronized申请是可重入的。