目录
setPriority()和getPrioritry()方法
多线程,字面意思就是多个线程(多个线程执行程序),我们在之前学习的都是单线程,多线程我们在以后的开发中会经常使用到,举个例子,就比如说一个音乐播放器,我们在搜索的时候,同时还可以播放歌曲,这就是多线程.
并发和并行
并发:多个指令在在单个cpu执行
并发:多个指令在多个cpu上执行
那就离谱了,我的电脑明明就一个cpu呀,怎么说是多个cpu呢.其实我们的电脑有八核十六线程,十六核三十二线程,三十二核六十四线程.拿八核十六线程来说,八个核在十六个线程之间来回执行,我们注意,核在找线程时,时随机的,概论问题.
多线程的实现方式
多线程的实现方式有三种方式分别是:
1.继承Thread类的方式实现
过程如下:
1.定义一个类继承Thread类
2.重写Thread类中的run方法
2.在测试类中创建自己实现类的对象,并启动线程.
参考下面代码
public class Test {
public static void main(String[] args) {
MyThread myThread=new MyThread();
MyThread myThread2=new MyThread();
myThread.setName("线程1 ");
myThread2.setName("线程2 ");
myThread.start();
myThread2.start();
}
}
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ "hello word"+" "+i);
}
}
}
把上面代码复制到开发环境,会发现会交替执行输出hello word.
2.实现Runnable接口的方式实现
过程如下
1.自己创建一个类实现Runnable接口
2.重写run()方法
3.创建测试类,实例化自己实现的类
4.创建Thread对象,并将自己实例化的对象传入
5.启动线程
代码实现如下
public class Test {
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
Thread thread=new Thread(myRunnable);
Thread thread2=new Thread(myRunnable);
thread.setName("线程1");
thread2.setName("线程2");
thread.start();
thread2.start();
}
}
public class MyRunnable implements Runnable{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ "hello word"+" "+i);
}
}
}
3.利用Callable接口和Ftuture接口实现
过程如下
1.创建一个MyCallable类,实现Callable接口
2.重写call()方法(是有返回值的,表示多线程的运行结果
3.创建MyCallable对象(表示多线程要执行的任务)
4.船舰Future对象,Future是一个接口,其实是实现FutureTask类(作用:管理多线程的运行结果)
5.创建Thread类的对象,并启动
代码如下
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable =new MyCallable();
//这里注意如果用Future作为类型要强转,这是因为Thread类实现了Runnable接
//Future<Integer> future=new FutureTask<>(myCallable);
//Thread thread=new Thread((Runnable) future);
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread=new Thread(futureTask);
thread.start();
int get=futureTask.get();
System.out.println(get);
}
}
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
int sum=0;
@Override
public Integer call() throws Exception {
for (int i = 0; i < 100; i++) {
sum+=i;
}
return sum;
}
}
上面就是实现多线程的三种方式
多线程三种实现方式的比较
优点 | 缺点 | |
继承Thread类 | 代码简单,可以直接使用 Thread类中的方法 | 可扩展性差, 不能再继承其他的类 |
实现Runnable接口 | 扩展性强,实现接口的同时还可以继承类 | 代码比较复杂,不能直接使用Thread中的类 |
实现Callable接口 | 扩展性强,实现接口的同时还可以继承类 | 代码比较复杂,不能直接使用Thread中的类 |
Thread中常见的成员方法
方法名称 | 说明 |
String getName() | 返回此线程的名称 |
void setName() | 设置线程名称(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield | 出让线程/礼让线程 |
public static void join | 插入线程/插队线程 |
下面对每个方法进行介绍,第一个和第二个方法,在前面代码中已经用过了,比较简单就不介绍了,补充一下,当我们不给线程设置名字的时候,它有一个默认的名字,格式是 : Thread-x(x是序号 从0开始),利用构造方法设置线程名字,要用到super关键字,因为构造方法是不能继承的,要是有父类的构造就要再写一个构造方法,并用super关键字
currentThread() 方法
该方法是获得当前线程的对象,这个方法我们上面也用到了,就是获得线程名字的时候用的,它获得了,线程的对象就可以调用该对象的方法.如果们在main方法中使用,结果就是打印main,当Java虚拟机启动之后,会自动的启动多条线程,其中一条就叫做main线程,它的作用就是调用main方法.
sleep()方法
哪条线程执行到这个方法,那么哪条线程就会在这个地方停留.停留时间自己设置,单位是毫秒,当时间到了之后,线程会自动醒来,自动执行下面的代码.
setPriority()和getPrioritry()方法
这个方法就是设置线程的优先级,在学习之前我们先学习一下线程的调度
抢占式调度(随机):就是多个线程在抢夺cup的执行权,cpu在选择执行哪条线程是不确定的,执行多长时间也是不确定的.
非抢占式调度:就是你一次我一次这样的调度
在Java中采用的式抢占式调度
我们利用setPriority()方法设置优先级,优先级越大,被执行的概率就越大,默认是5,最大是10,当我们把优先级设置为10的时候,并不意味着100%执行,只是执行的概率变大了.main方法默认也是5.
使用方式也很简单就是用创建好的线程对象去调用这个两个方法,就不演示了,非常简单.
setDaemon()方法
守护线程,表面意思就是让一个线程去守护某一个线程,当一个线程执行完之后,守护线程也会陆续执行完毕,注意这里不是立马就停止,而是陆续停止.还是创建两个线程对象,让其中一个线程调用这个方法,设置为守护线程,另一个不设置,我们可以看到当那个不是守护线程的线程执行完之后,守护线程也会陆续停止,即使守护线程的逻辑还没执行完也会陆续停止.这就是守护线程,也就是当非守护线程执行完了,守护线程也就没有存在的必要了.
守护线程的应用场景就好比我们用聊天软件聊天,你向一个人发送文件,在发送的途中你关闭了聊天,发送就会停止.
yield()方法
让出先线程,意思就是把线程让出去,如果要执行下面的代码,则需要重新抢夺cpu.
join()方法
就是强线程的执行权.
线程的生命周期
看下面这个图
线程的安全问题
一般线程安全
以买票问题为例子,有100张票,用三个窗口进行出售,我们就可以把三个窗口看成三个线程.看下面这段代码,我们以第二种方式来创建多线程.来看下面这段代码
public class Test {
public static void main(String[] args) {
Lottery lottery=new Lottery();
Thread thread1=new Thread(lottery);
Thread thread2=new Thread(lottery);
Thread thread3=new Thread(lottery);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
public class Lottery implements Runnable{
int ticket=0;
@Override
public void run() {
while(true){
if(ticket<100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
}else{
break;
}
}
}
}
执行结果如下
我们就发现不对劲,三个窗口同时在卖1张票,这不是我想要的结果.怎么解决这个问题呢,下面就来介绍一下
解决线程安全问题在Java中就是用锁,在Java中,实现锁这个功能,要用到synchronized 这个关键字,上面这个例子有点麻烦,我们举一个简单的例子,创建两个线程,然后写两个循环,创建一个变量,两个变量在这两个循环中都自增加加.代码如下
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
thread1.start();
thread.start();
thread.join();
thread1.join();
System.out.println(count);
}
我们知道,这段代码按我们没接触多线程之前,觉得count的结果为10000,但是当我们运行程序会发现并不是我们想的那样.那这事为什那么呢,我们知道当一个变量要被修改时,首先要从内存中读取到寄存器,把这个过程我简称为(load),之后进行累加(add),最后一个步骤写入到内存中(save).
那么就能意识到问题了,当一个线程刚加了一次,并且写到了内存里面,但是另一个线程是之前就拿到了,也加加了,但是这个线程没有另一个线程执行的次数多,就导致,执行快的线程,刚累加完,慢线程,把一个小的值又给覆盖了.这就导致了程序出现了bug.那么解决方案就是加锁.代码如下
private static int count=0;
private static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized(object){
count++;
}
}
});
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized(object){
count++;
}
}
});
thread1.start();
thread.start();
thread.join();
thread1.join();
System.out.println(count);
}
其实上面就改了一点点代码执行结果就正确了,由此可见,在多线程中加锁的重要性.那么,分析一下,程序为什么执行正确,看上面代码,我们给count++加了一层锁,两个线程,当有一个线程执行到count++时,另一个线程就不会执行这个操作,当这个操作执行完之后,锁打开了,那么另一个线程才会去执行,也就是当一个线程执行完一套完整的操作后,另外一个线程才会去执行一整套操作.
还有就是要注意,加的锁对象可以是任意的对象,但是必须是同一个对象,如果对象不相同,那么这个锁就相当于没加.还有就是可以是类对象,就是我们每写一个Java文件就是一个类对象,JVM,会将Java文件编译成以 .class为后缀的文件,就直接在这个Java文件加上 .class就可以,它就是一个类对象.
当一个类去点class时,这其实也是Java的一种反射操作,通过反射操作,我们可以拿到这类的所有东西,包括被private修饰的变量或者方法.
此处的synchronized是JVM提供的功能,而JVM是C++实现的.进一步也是通过操作系统api来实现的加锁功能,而系统api又是cup特殊的指令来实现的.
还有就是synchronized能是自己能加锁开锁的,像其他语言也有这种功能.
synchronized修饰方法
看下面的代码
class Func{
public int count;
synchronized public void add(){
count++;
}
}
public class demon1 {
public static void main(String[] args) throws InterruptedException {
Func func=new Func();
Thread thread=new Thread(()->{
for (int i = 0; i < 5000; i++) {
func.add();
}
});
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
func.add();
}
});
thread.start();
thread1.start();
thread1.join();
thread.join();
System.out.println(func.count);
}
}
上面在方法前面加synchronized,和之前代码执行结果相同,哪个对象调用这个add方法就要将这个过程执行完,开锁后其他对象才能调用.当然还有另外一种简写的方式代码如下:
class Func1{
public int count;
public void add(){
synchronized(this){
count++;
}
count++;
}
}
public class demon2 {
public static void main(String[] args) throws InterruptedException {
Func func=new Func();
Thread thread=new Thread(()->{
for (int i = 0; i < 5000; i++) {
func.add();
}
});
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
func.add();
}
});
thread.start();
thread1.start();
thread1.join();
thread.join();
System.out.println(func.count);
}
}
这种方法就是在synchronized后面加this关键字,我们知道this关键字表示当前对象的引用.哪个对象调用这个方法都会对该方法上锁,其他对象调用不了.当然这种方法也有不是所有的场景都适用,看下面的代码
class Func3{
public int count;
public static void add(){
synchronized(Func3.class){
}
}
}
public class demon3 {
public static void main(String[] args) throws InterruptedException {
Func func=new Func();
Thread thread=new Thread(()->{
for (int i = 0; i < 5000; i++) {
func.add();
}
});
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
func.add();
}
});
thread.start();
thread1.start();
thread1.join();
thread.join();
System.out.println(func.count);
}
}
如果一个方法是一个静态方法,那么它没有实例,也就用不了this这个关键字.当然如果我们实在想用一个对象,那么可以直接随便new一个对象,但是要加static.
上面是面对线程安全问题的基础解决方案,但是我们在平时写代码的时候可能,会出现一种情况----------死锁.下面我们来看死锁问题.
死锁
所谓的死锁就是针对一把锁连续加锁两次.当你在外层加一层锁之后,在里面又加了一个锁,按道理来说,程序会陷入阻塞.看下面代码
class Func1{
public int count;
public void add(){
synchronized(this){
count++;
}
count++;
}
}
public class demon2 {
public static void main(String[] args) throws InterruptedException {
Func func=new Func();
Thread thread=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (func){
func.add();
}
}
});
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (func){
func.add();
}
}
});
thread.start();
thread1.start();
thread1.join();
thread.join();
System.out.println(func.count);
}
}
上面代码加了两层锁,在我们分析看来,一定会陷入阻塞,但是实际上执行结果任然是10000,程序并没有出现bug,原因就是JVM对synchronized关键字做了优化,让其不会出现死锁现象,如果是C++或者Python就会出现死锁,Java为了减少程序员写出死锁的概率,引入了特殊机制,解决上述死锁问题,这种机制又叫可重入锁.
死锁是一个大的话题设计的东西非常多下面我会一一介绍,包括怎么处理死锁现象.
那么再说一下为什么会产生线程安全问题
1.操作系统对线程的调度是随机的(抢占式执行)
2.多个线程对同时对一个变量进行修改
3.修改操作不是原子的(所谓的原子性:是指一个操作或者一组操作要么完全执行,要么不执行,不被其他线程中断(原子操作不会看到执行的中间状态).
4.内存可见性
5.指令重排序
第四点和第五点后面会说
好那么下面来说可重入锁,可重入锁会判断当前锁所加的所是不是当前线程,如果是就不会进行任何加锁,也不会有任何的阻塞,而是直接放行.
好,那么当面对锁的多重嵌套,第一层锁是加锁,那么在什么时候释放锁呢,当然是最后一层,如果中间有其他代码,那就又有线程安全问题了,那么JVM是怎么来判断在最后一个大括号呢,其实是有一个程序计数器,比方说count,初始时让count等于0,然后遇见右括号,就加 1 ,遇见左括号就减 1,当count=0时,就可以判断出这是最后一个括号了,这时候,就要释放锁了.
那么有没有synchronized无法结局的死锁呢,当然有,就比如:
线程1现针对A加锁,线程2针对B加锁,线程1在不释放锁A的情况下,再针对B加锁.线程2在不释放B的情况下,再针对A加锁.看下面代码
public class demon4 {
private static Object locker1=new Object();
private static Object locker2=new Object();
public static void main(String[] args) {
Thread thread1=new Thread(()->{
synchronized(locker1){
System.out.println("t1加锁,locker1完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(locker2){
System.out.println("t1加锁 locker2完成");
}
}
});
Thread thread=new Thread(()->{
synchronized (locker2){
System.out.println("t2 加锁locker2完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t1加锁 locker1完成");
}
}
});
thread.start();
thread1.start();
}
}
运行结果:
我们发现线程停在这里不动了,就形成死锁.
解决方案也很简单,就是在给A加锁加锁之后释放锁,然后再给B加锁.也就是下面这段代码
package thread_learn2;
public class demon5 {
private static Object locker1=new Object();
private static Object locker2=new Object();
public static void main(String[] args) {
Thread thread1=new Thread(()->{
synchronized(locker1){
System.out.println("t1加锁,locker1完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized(locker2){
System.out.println("t1加锁 locker2完成");
}
});
Thread thread=new Thread(()->{
synchronized (locker2){
System.out.println("t2 加锁locker2完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker1){
System.out.println("t1加锁 locker1完成");
}
});
thread.start();
thread1.start();
}
}
这时一种解决死锁的一种方法.
还有一种方法,就是破除循环等待,啥意思呢,就比如一个程序员要修关于进出公司的bug,然后保安不让他进,必须要出示码,但是程序员不去修bug怎么出示码呢,把这种情况出现到代码上,也就构成了死锁.
那么解决上面问题也很简单,就是给每一个锁编号,然后约定每个线程都按一定顺序来进行加锁,看下面代码
package thread_learn2;
public class demon6 {
private static Object locker1=new Object();
private static Object locker2=new Object();
public static void main(String[] args) {
Thread thread1=new Thread(()->{
synchronized(locker1){
System.out.println("t1加锁,locker1完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(locker2){
System.out.println("t1加锁 locker2完成");
}
}
});
Thread thread=new Thread(()->{
synchronized (locker1){
System.out.println("t2 加锁locker1完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t1加锁 locker2完成");
}
}
});
thread.start();
thread1.start();
}
}
我们会发现,除了我们不可逆转的情况出现的死锁,其他就是代码结构了,代码结构涉及的问题就是我上述说的两个问题,
1.请求和保持(这个就是线程1在不释放锁A的情况下去拿锁B,线程2不能把锁B强过来(解决方法就是等锁A释放后再拿锁B).
2.循环等待(就是那个程序员进公司的问题)解决方案就是把锁编号,约定顺序加锁.其实这个解决方案就是看哪个线程先start,先start的这个线程会优先被执行完.但是注意上述代码都是建立在sleep的基础上.如果没有sleep代码可能会死锁,也可能不死锁,因为线程是抢占式执行,谁先抢到谁先执行,结果是未知的.
还有就是死锁的两个基本特征,一个是互斥性和不可抢占.
- 互斥确保了同一时刻只有一个线程可以访问共享资源,从而避免数据竞争。
- 不可抢占确保了持有锁的线程可以安全地完成其操作,而不会被其他线程中断或抢占。
volatile 关键字
volatile关键字是用来解决内存可见性的问题,我们在多线程中除了死锁问题,还有就是内存可见性的问题,这不是程序员逻辑问题导致出错的,而是JVM的问题,所以Java提供volatile关键字来解决问题,先看有问题的代码
package thread_learn2;
import java.util.Scanner;
public class demon7 {
private static int n=0;
public static void main(String[] args) {
Thread thread=new Thread(()->{
while(n==0){
}
});
Thread thread1=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数:");
n=scanner.nextInt();
});
thread.start();
thread1.start();
}
}
执行结果如下:
发现程序并没有终止,这就是内存可见性的问题.那么为什么会有这种问题呢.原因如下:
线程thread一直在循环并没有打印,,首先要进行条件判断,就是n是否等于0.要进行判断,就要先把数据从内存读到寄存器中,然后在寄存器中比较.然而这两个过程速度差距非常大,相差几个数量级,也就是说在线程thread1中对n的更改,要经过这两个步骤,但是在JVM看来,几万次比较后的结果都是0,并且过程一相对于过程二开销比较大,所以JVM直接将过程一直接优化掉了,也就导致,就算我们去更改n的值程序也不能停下来.
关于JVM优化这件事,其实就是提高代码的执行效率,因为每个程序员写的代码不一样,有的运行速度慢,但是当JVM优化后,运行速度就差不多了.
还有就是JVM优化在单线程中是非常准确的不会出现内存可见性问题.但是多线程会出现.
还有就是JVM这里的优化和之前C语言Debug版本的Release版本不同,Dubge版本编译的时候,将中间的符号表也编译到exe文件中了,Release版本没有.而C语言的优化是要靠指令来完成,
-O0是不优化
-O3是优化最高级.
然后就是解决上面的问题,我们可以直接sleep,让线程thread休眠一下,这时候n就能改变值,但是这个方法很不好,程序运行速率会下降很多,所以就用上面说的关键字volatile关键字,.看下面代码
package thread_learn2;
import java.util.Scanner;
public class demon7 {
private volatile static int n=0;
public static void main(String[] args) {
Thread thread=new Thread(()->{
while(n==0){
}
});
Thread thread1=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数:");
n=scanner.nextInt();
});
thread.start();
thread1.start();
}
}
看运行结果,程序是没有错的.这也是volatile关键字的用法.
还有就是什么时候要加volatile这个关键字们就是在一个变量被一个线程读,一个线程写中情况.
下面我们还来谈内存可见性,按网上资料的说法,引入了两个概念
(1)工作内存
(2)主内存
整个Java程序都持有主内存,每个Java线程都会有一份工作内存.
就拿上面的代码来举例,变量n就在主内存中,当两个线程执行的时候,会将变量n加载到工作内存中,线程thread1修改了n,就会将n再写道主内存中.线程thread会将n从主内存读取到工作内存中,然后依照工作内存中n的值来进行判定的,而此时线程thread1修改了主内存n的值,但是,还是依据线程thread中的工作内存来进行判定的.也就出现了内存可见性问题.
我们此时类比一下,这里的主内存不就是内存吗,工作内存不就是cpu寄存器和cache吗,这其实就是换了个说法.站的角度不同.
好了关于内存可见性问题就说到这里,最后一点,volatile不能解决原子性问题,只能解决内存可见性问题.
wait和notify
上面其实已经提到了wait和notify这个方法,这里我们仔细来讨论一下.
wait是等待,notify是通知,上面我们说是唤醒,还是通知比较合适.
我们拿一个例子,来说一下这两个方法怎么用.
去ATM取钱,一群人去ATM取钱,第一个人进去之后,发现ATM没钱了,他前脚刚出去,门还没关上,要不我再去看看吧,重复这个过程.其他的人也进不去,这种现象叫做 "线程饿死".解决方案也很简单,就是让这个人去等待,等通知有钱了,再去通知他,让他去取钱,其他人也可以进ATM机,这就解决了.
所谓线程饿死这种现象,是概率问题,和调度器的策略相关.就好比上面的例子,程序不会一直重复这个过程,但是重复个几百次还是有的.
值得注意的是wait和notify是Object提供的方法,任意的object对象都可以用来wait,notify
好那么我们先简单使用一下wait这个方法看下面代码
package thread_learn2;
public class demon8 {
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
}
运行结果如下
报错了,这是为什么呢
IllegalMonitorStateException Illegal是非法的意思,Monitor是监视器的意思,但是这里的意思是锁的意思,是synchronized这个锁.State是状态的意思,Exception是异常的意思.连起来就是非法的锁状态.
这里object没有进行加锁.总的来说,wait会进行一个操作,就是进行解锁.所以使用wait要放在synchronized代码块里面.所以要先加锁,才能用wait这个方法.
只要改一下代码就行了,看下面代码
package thread_learn2;
public class demon8 {
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
System.out.println("wait 之前");
synchronized (object){
object.wait();
}
System.out.println("wait 之后");
}
}
上述代码就是正确的代码.
还有wait这个方法,就是解锁和等待是同时的(打包成原子的).为什么要是打包成原子的呢,如果不是同时的可能会发生线程切换.这就可能导致线程不能及时被唤醒.
这里总结一下wait主要做的三件事:
(1)释放锁
(2)进入阻塞状态,准备接受通知
(3)接受到通知后,唤醒,并尝试获取锁.
还有就是,必须是同一个锁对象,才能被唤醒
notify也是这样,必须是同一个锁对象才能进行通知,同时notify也要确保先加锁,才能执行.
wait默认是死等,也是有参数的,这就和sleep就很相似.当wait等待时间到了,就不再进行等待,会尝试去获取锁,获取锁之后会执行下面的代码,出了synchronized代码块之后释放锁.
我们做一个练习,利用多线程来顺序打印ABC.代码如下:
package thread_learn2;
public class demon9 {
static Object object1=new Object();
static Object object2=new Object();
public static void main(String[] args) {
Thread thread=new Thread(()->{
System.out.println("A");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object1){
object1.notify();
}
});
Thread thread1=new Thread(()->{
synchronized (object1){
try {
object1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("B");
synchronized (object2){
object2.notify();
}
});
Thread thread2 =new Thread(()->{
synchronized (object2){
try {
object2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("C");
});
thread.start();
thread1.start();
thread2.start();
}
}