企业解决方案公司有哪些,东莞企业网站seo,请人做网站合同,利尔化学股票最新消息#x1f451;专栏内容#xff1a;Java⛪个人主页#xff1a;子夜的星的主页#x1f495;座右铭#xff1a;前路未远#xff0c;步履不停 目录 一、线程状态1、New#xff08;初始状态#xff09;2、Terminated#xff08;终止状态#xff09;3、Runnable#xff08;… 专栏内容Java⛪个人主页子夜的星的主页座右铭前路未远步履不停 目录 一、线程状态1、New初始状态2、Terminated终止状态3、Runnable可运行状态4、休眠状态 二、线程安全1、线程不安全的原因2、Demo问题解决3、synchronized关键字3.1 互斥特性3.2 刷新内存3.3 可重入3.4 使用示例 4、volatile关键字4.1 内存可见性问题4.2 volatile用法 三、wait 和 notify1、wait方法2、notify方法3、 wait 和 sleep 的对比 一、线程状态
在Java中线程的状态可以分为以下几种主要状态New新建状态、Runnable可运行状态、Blocked阻塞状态、Waiting等待状态、Timed Waiting定时等待状态、Terminated终止状态。
1、New初始状态
New新建状态线程处于新建状态已经创建了线程对象但尚未调用其start()方法。在这个状态下线程对象已经被创建但尚未分配系统资源。 public static void main(String[] args) {Thread t new Thread(()-{while (true){}});System.out.println(t.getState());t.start();}2、Terminated终止状态
Terminated终止状态线程进入终止状态表示它已经执行完成或因异常而终止。一旦线程的run()方法完成它就会进入终止状态。一旦线程终止它将不再处于任何其他状态。 public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.getState());t.start();t.join();System.out.println(t.getState());}3、Runnable可运行状态
Runnable就绪状态线程在这个状态下已经被启动可以运行。它可能正在执行也可能处于等待CPU资源的状态或者是在等待某个特定的条件如等待I/O操作完成。就绪状态可以理解为两种情况1.线程正在 CPU 上运行。 2. 线程在这里排队随时都能去 cpu 上执行。 public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while(true){}});System.out.println(t.getState());t.start();Thread.sleep(1000);System.out.println(t.getState());}4、休眠状态
Timed_Waiting定时等待状态线程进入定时等待状态是因为调用了具有超时参数的等待方法如sleep()或join()。它会在指定的时间间隔内等待或者直到被唤醒或中断。
Blocked阻塞状态线程进入阻塞状态通常是因为它在等待某个条件满足而无法继续执行例如等待某个锁。一旦条件满足线程将进入Runnable状态。
Waiting等待状态线程进入等待状态是因为调用了wait()方法或者类似的等待方法它会一直等待直到被其他线程唤醒或中断。
总结Blocked是因为锁产生了阻塞Waiting是因为wait()方法产生的阻塞Timed_Waiting是因为sleep()或join()产生的阻塞。 public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.getState());t.start();Thread.sleep(1000);System.out.println(t.getState());}二、线程安全
在开始线程安全之前先通过一个小Demo来感受一下线程安全。
class Counter{public int count 0;public void increase(){count;}
}
//线程安全演示
public class Demo {public static void main(String[] args) throws InterruptedException {Counter counter new Counter();Thread t1 new Thread(()-{for(int i 0;i5000;i){counter.increase();}});Thread t2 new Thread(()-{for(int i 0;i5000;i){counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}
这个例子中两个线程针对同一个变量进行循环自增各自自增5000次。按照正常情况预计结果应该是10000。 但是实际结果却是7794并且同样的代码每次运行结果居然还不一样 正常来看我们的代码肯定没有问题但是还是出现了这个bug这个 bug 其实就是线程安全问题。
1、线程不安全的原因
线程不安全的根本原因是多个线程之间的调度是“随机的”操作系统使用的是“抢占式”执行的策略来调度线程 。
根据这个根本原因可以衍生出一些其他原因
多个线程同时修改同一变量。 需要注意的是一个线程修改一个变量、多个线程读取一个变量或多个线程修改多个不相关的变量通常是安全的。进行的修改不是原子的。“原子” 通常指的是一个操作是不可分割的、不可中断的单位。内存可见性引起的线程安全问题。指令的重排序引起的线程安全问题。
2、Demo问题解决
知道了线程不安全的原因后再看一下上面的Demo分析为什么会出现线程不安全的问题。 上面的线程不安全问题的bug主要的原因就是count 这个代码出现了问题。我们拆解一下count 这个操作。 这个count 操作其实本质上是三个步骤
把内存中的数据加载到CPU的寄存器中load把寄存器中的数据进行1add把寄存器中的数据写回到内存中save
如果上述的操作出现在单线程中其实是不会出现任何问题的但是出现在多线程中就会出现问题。因为是两个线程并发执行线程的调度是随机的抢占式的执行。
这个Demo中除了根本原因外还出现了上面问题中的两个问题“多个线程同时修改同一变量” 、“进行的修改不是原子的。” 多个线程修改同一个变量这个是我们的需求所以我们没办法去改变他所以我们只能去解决“进行的修改不是原子的”这个问题。
如何修改为原子操作加锁所谓加锁就是把把一组操作打包成为一个原子的操作。Java 中引入了一个synchronized关键字进行加锁。这个关键字在后面详细解释先使用这个给count操作进行加锁。
class Counter{public int count 0;synchronized public void increase(){count;}
}使用synchronized 给方法加锁进入方法就自动加锁lock出了方法就自动解锁unlock。 当这个这个方法加锁后这个方法就变成如下的样子 当 t1 加锁后t2 也尝试加锁t2 就会阻塞等待。等待到 t1 释放锁后才能加锁成功。直到 t1 完成了 countt2 才能真正进行 count。把穿插执行变成了串行执行。
3、synchronized关键字
3.1 互斥特性
synchronized 会起到互斥效果某个线程执行到某个对象的 synchronized 中时其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
进入synchronized 修饰的代码块相当于加锁退出synchronized 修饰的代码块相当于解锁
可以粗略理解为每个对象在内存中存储的时候都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”)。
如果当前是 “无人” 状态那么就可以使用使用时需要设为 “有人” 状态。如果当前是 “有人” 状态那么其他人无法使用只能排队。
3.2 刷新内存
synchronized 的工作过程
获得互斥锁从主内存拷贝变量的最新副本到工作的内存执行代码将更改后的共享变量的值刷新到主内存释放互斥锁
所以 synchronized 也能保证内存可见性。
3.3 可重入
synchronized 同步块对同一条线程来说是可重入的不会出现自己把自己锁死的问题 什么是 “把自己锁死” 一个线程没有释放锁, 然后又尝试再次加锁。
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();按照之前对于锁的设定第二次加锁的时候就会阻塞等待。直到第一次的锁被释放才能获取到第二个锁。但是释放第一个锁也是由该线程来完成结果这个线程已经躺平了啥都不想干了也就无法进行解锁操作。这时候就会出现死锁这种锁也叫不可重入锁。Java 中的 synchronized 是 可重入锁因此没有上面的问题。
3.4 使用示例
下面有一些使用示例但是大家只需要牢记一条规则当两个线程针对同一个对象进行加锁的时候就会出现竞争/锁冲突。一个线程先拿到锁另外一个线程就会进行阻塞等待知道第一个线程释放了锁之后第二个线程才能获取到锁继续往下执行。针对那个对象加锁的并不重要重要的是两个线程是不是针对同一个对象进行加锁。
修饰方法 这种方式是修饰整个方法即使方法中没有同步代码块也会锁定这个方法这种方式适用于整个方法需要同步的情况。
public synchronized void method() {// 同步代码块
}修饰代码块 这种方式是将同步代码块包在synchronized括号内只有在执行到synchronized代码块时才会锁定这种方式适用于只需要同步执行部分代码的情况。
public void method() {synchronized (this) {// 同步代码块}
}修饰静态方法 和修饰方法类似这种方式是锁定整个静态方法适用于整个静态方法需要同步的情况。
public synchronized static void method() {// 同步代码块
}
修饰类 这种方式是锁定整个类即使不同实例中的线程也会被锁定适用于整个类需要同步的情况。
public void method() {synchronized (ClassName.class) {// 同步代码块}
}4、volatile关键字
4.1 内存可见性问题
import java.util.Scanner;//内存可见性
public class Demo {private static int isQuit 0;public static void main(String[] args) {Thread t1 new Thread(()-{while (isQuit 0){;}System.out.println(t1 执行结束);});Thread t2 new Thread(()-{Scanner scanner new Scanner(System.in);System.out.print(请输入isQuit的值);isQuit scanner.nextInt();});t1.start();t2.start();}
}这段代码的理想状态 t1始终在进行while循环t2则是要让用户通过控制台输入一个整数作为isQuit的值。当用户输入的仍然是0的时候t1线程继续执行。如果用户输入的非0t1线程就应该循环结束。
但是实际上是当输入非 0 值的时候已经输入 isQuit 的值的时候t1 线程还在继续执行不符合实际的预期。 导致这个的原因是因为程序在编译运行的时候Java 编译器和 jvm 可能会对代码进行一些优化。当你的代码实际执行的时候编译器 jvm 就可能把你的代码给改了在保持原有逻辑不变的情况下提高代码的效率。
编译器优化本质上是靠代码智能的对你的代码进行分析判断这个过程中大部分是 ok 的能保证代码逻辑不变但是如果遇见多线程了此时优化就有可能出现差错。使程序原有的逻辑发生改变。 编译器/jvm 发现在这个逻辑中代码要反复快速的读取同一个内存的值并且这个值每次读取的还是一样。此时编译器做出了一个大胆的决定直接把 load 操作给优化掉了只是第一次执行 load 后续不再执行 load 操作直接拿寄存器中的数据进行比较了。但是编译器没有想到程序员在另外一个线程中修改了 isQuit 的值因此就出现了误判。
4.2 volatile用法
volatile 本质上是保证变量的内存可见性禁止该变量的读操作被优化到读寄存器中不是原子性。
代码在写入 volatile 修饰的变量的时候
改变线程工作内存中volatile变量副本的值将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候
从主内存中读取volatile变量的最新值到线程的工作内存中从工作内存中读取volatile变量的副本
为了解决上面的问题我们只需要用volatile 来修饰这个变量后编译器就会明白这个变量是易变的编译器会禁止上述优化。
public class Demo {private volatile static int isQuit 0;public static void main(String[] args) {Thread t1 new Thread(()-{while (isQuit 0){;}System.out.println(t1 执行结束);});Thread t2 new Thread(()-{Scanner scanner new Scanner(System.in);System.out.print(请输入isQuit的值);isQuit scanner.nextInt();});t1.start();t2.start();}
}
三、wait 和 notify
由于线程之间是抢占式执行的因此线程之间执行的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序wait 和 notify就是解决这个问题的。
1、wait方法
wait 需要做的事情
使当前执行代码的线程进行等待. (把线程放到等待队列中)释放当前的锁满足一定条件时被唤醒, 重新尝试获取这个锁
wait 要释放当前的锁那前提就是他必须要上锁。所以wait 要搭配 synchronized 来使用脱离 synchronized 使用 wait 会直接抛出异常。
public class Demo4 {public static void main(String[] args) throws InterruptedException {Object object new Object();synchronized (object){object.wait();}System.out.println(wait结束);}
}这里的 wait 会阻塞到其他线程 notify 为止。其中最典型的一个场景就是能够有效的避免线程饿死。
2、notify方法
notify 方法是唤醒等待的线程。
方法notify()也要在同步方法或同步块中调用该方法是用来通知那些可能等待该对象的对象锁的其它线程对其发出通知notify并使它们重新获取该对象的对象锁。如果有多个线程等待则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)在notify()方法后当前线程不会马上释放该对象锁要等到执行notify()方法的线程将程序执行完也就是退出同步代码块之后才会释放对象锁。
package thread;public class Demo {private static Object locker new Object();public static void main(String[] args) {Thread t1 new Thread(()-{while (true){synchronized (locker){System.out.println(t1 wait开始);try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(t1 wait结束);}}});t1.start();Thread t2 new Thread(()-{while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker){System.out.println(t2 notify 开始);locker.notify();System.out.println(t2 notify 结束);}}});t2.start();}
}
注意事项
要想让notify能够顺利唤醒wait就需要确保wait和notify都是使用同一个对象调用的。wait和notify都需要放到synchronized之内虽然notify不涉及解锁操作但是Java也强制要求notify 放到synchronized中如果notify的时候另外一个线程没有处于wait状态此时的notify相当于空打一炮没有任何副作用。
3、 wait 和 sleep 的对比
其实理论上 wait 和 sleep 完全是没有可比性的因为一个是用于线程之间的通信的一个是让线程阻塞一段时间唯一的相同点就是都可以让线程放弃执行一段时间。
硬说区别的话就是如下
wait 需要搭配 synchronized 使用.sleep不需要wait 是 Object 的方法sleep 是 Thread 的静态方法