目录
4️⃣:synkronized关键字-监视器锁monitor lock
1️⃣:线程的状态
1.1:观察线程所有的状态
🍕NEW: 把Thread对象创建好,但是还没有调用start
🍔RUNNABLE:就绪状态,处于这个状态的线程随时可以被调度到CPU上,如果代码中没有进行sleep或者其他的阻塞的操作,代码大概率是处于RUNNABLE状态的
🍣BLOCKED: 当前线程在等待锁(synkronized),导致了阻塞
🍥WAITING: 当前线程在等待唤醒,导致了阻塞
🍢TIMED_WAITING: 线程代码中是用来sleep或者是join
🍜TERMINATED: 操作系统中线程已经执行完毕,销毁了,但是Thread对象还在
通过代码来演示前两个:
通过 getState 来获取当前线程的一个状态
NEW:
public class Demo1 {
public static void main(String[] args) {
Thread thread = new Thread(() ->{
});
System.out.println(thread.getState());
thread.start();
}
}
RUNNABLE:
public class Demo1 {
public static void main(String[] args) {
Thread thread = new Thread(() ->{
});
thread.start();
System.out.println(thread.getState());
}
}
为什么要这么细分?
这是因为以后我们在日常开发过程中,经常会遇见程序 “卡死” 的情况,这其实代表着一些关键的线程阻塞了,而此时我们就可以通过获取当前线程的一个状态来分析卡死的原因🍭
1.2线程状态转换简图
2️⃣:线程安全
我们都知道,操作系统在调度线程的时候是随机的,这就会带来很大的问题,例如产生bug,如果一个线程因为线程调度产生bug,我们就可以认为此线程是不安全的,如果操作系统调度线程并没有产生bug,则就认为此线程是安全的
典型案例:使用两个线程对同一个变量自增5w次
class A{
int count;
public void Count(){
count++;
}
}
public class Demo2 {
public static A a = new A();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
a.Count();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
a.Count();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(a.count);
}
}
这个代码的运行结果并不是我们最开始希望的10w,总是在5w到10w之间
这不禁令人思索,count++到底干嘛了呢?原因是什么?
我们可以站在CPU的角度来想,count++实际上执行了三个指令:
①将count从内存中加载到CPU的寄存器中(load)
②将count++ (add)
③将寄存器加过的值返回到内存中(save)
两个线程分别都执行了这三个指令,而我们知道,CPU的调度是随机的,也就会导致每个线程的是哪个指令并没有先后顺序之分,可能在线程1中执行了load后就直接执行线程2的load了,充满着随机性,这种结果最终导致本该加2的count变成加1了,也就导致输出的结果并不是我们预期的10w
3️⃣:线程不安全产生的原因
3.1修改共享数据
就如上述例子,多个线程同时修改同一份数据
怎么解决?加锁
3.2没有原子性
这里的原子性和之前介绍事务的原子性是本质上是一样的,举个例子,我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的
怎么解决?加锁
有时候这个现象也叫 互斥 ,表示操作是互相排斥的,一般来说,如果java语句只有一条,那么这个语句肯定是原子性的,但是实事并不止一条java语句,比如上述例子的count++,他是分为三条语句执行的
如果不能保证原子性会给多线程带来什么样的问题呢,一个线程正在好端端的修改着数据,突然另一个线程突然的闯了进来,是的原本的线程被打断,就会导致这个线程修改的数据就是错误的
3.3内存可见性
举一个简答的例子,针对同一个变量,一个线程进行读操作(循环的读),另一个线程进行写操作(在合适的时候进行修改)
thread1这个线程在循环的读这个变量,根据我们所知,读取内存这个操作相比较于读取寄存器,是一个非常低效的操作,因此thread1频繁的读取这里的内存值,就会非常的低效,而且如果thread2线程一直不修改数据的内容,thread1读取的数据始终是一样的
所以此时thread1就有个大胆的想法💣 直接不从内存中读取数据,而是直接从寄存器里读取数据(哇哦w(゚Д゚)w),正当thread1跑到寄存器去读取数据的时候,thread2这个线程突然修改了count这个变量,但是这个修改之后的变量thread1并没有看到,这就是我们所说的内存不可见问题
如何解决?加锁
3.4代码的顺序性/指令重排序
对于程序员来说,我们写的代码谁在前谁在后都无所谓,但是编译器就不这么认为的,在保证逻辑性不变的前提下,编译器会自动的调整顺序,如果是单线程的代码编译器的调整都是无所谓的,会是的代码更优,但是多线程的代码就不一样了,编译器可能会产生误判
如何解决?加锁
4️⃣:synkronized关键字-监视器锁monitor lock
synchronized是Java多线程中元老级的锁,使用了锁就能能够解决线程不安全问题!
4.1synkronized使用方法
1)直接修饰普通的方法
class A{
int count;
//给普通方法加锁
synchronized public void Count(){
count++;
}
}
public class Demo2 {
public static A a = new A();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
a.Count();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
a.Count();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(a.count);
}
}
上述案例,通过synkronized对A中的普通方法加锁,就能够解决多线程修改同一个变量产生的不安全的后果
使用synkronized加锁,本质上是针对某个“对象”进行加锁,而这个对象就是this
一个对象,在Java中,每个类都继承自object,每个new出来的实例,里面乙方面包含了自己安排的一些属性,一方面包含了“对象头”,对象一些元数据,例如:
对象头对于我们来说是用不到的,但是JVM会用到,砸门所说的加锁操作,就是在给这个对象头里进行设置一个标志位
2)修饰一个代码块
synchronized(this) {
//业务代码
}
需要显示制定针对那个对象加锁(Java中的任意对象都可以作为锁对象)
3)修饰一个静态方法
synchronized void staic method() {
//业务代码
}
相当于针对当前类的类对象进行加锁
4.2synkronized的主要作用
1)原子性:所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。被synchronized
修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
2)可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
3)有序性:有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。