java基础之多线程解析

下面我们通过一张图来了解一下多线程我们需要学习什么内容

在这里插入图片描述

程序、进程、线程

程序

  • 是为完成特定任务,用某种语言编写的一段代码的集合,即一段静态的代码

进程

  • 程序的一次执行过程或是正在运行的程序

  • 进程作为资源分配的基本单位,系统在运行时会为每个进程分配一定的内存区域

线程

  • 进程可进一步细化为线程,是程序运行过程中的一条执行路径

  • 线程作为调度和运行的最小基本单位,每个线程拥有自己独立的pc(程序计数器)和运行栈,线程切换的开销小,

    可以理解为独立运行的子任务

多线程优点:充分利用cpu的资源


进程可以进一步细化为线程

每个线程,都拥有独立的:栈,程序计数器

多个线程共享一个进程里的:堆、方法区


一个java.exe至少应该包括三个线程,一个是主线程,一个是垃圾回收线程,一个是异常处理线程,如果发生异常,会影响主线程的运行
并行与并发

并行:多个cpu执行多个任务。比如:多个人同时做不同的事

并发:一个cpu执行多个任务(本质是单线程)


一个线程的生命周期

在这里插入图片描述

  • 新建状态

使用new关键字和Thread类或其子类创建一线程个对象后,该线程对象就处于新建状态,它保持这个状态直到程序**start()**这个线程

  • 就绪状态

调用start()方法之后线程就进入就绪状态,就绪状态的线程处于就绪队列中,等待JVM线程调度器的调度

  • 运行阶段

当就绪状态的线程获取到了cpu的资源后,线程就会运行run()方法进入运行状态,这个时候的线程最为复杂,因为它下一个阶段可能变成阻塞状态、死亡状态、就绪状态

  • 阻塞状态

当一个信号线程执行了sleep(睡眠)或suspend(挂起)等方法,失去所占资源之后,该线程就进入了阻塞状态,直到睡眠时间结束或获得设备资源后就可以重新进入就绪状态,阻塞状态可以分为三种

  1. 等待阻塞:运行状态中的线程执行wait()方法,使线程就进入到了等待阻塞状态,
  2. 同步阻塞:线程在获取synchronized同步锁失败(因为同步锁被其他线程占用)
  3. 通过调用线程的sleep方法或join发出I/O请求时,线程就会进入到阻塞状态,等到sleep状态超时,join等待线程终止或超时,或者I/O处理完毕,线程重新转入就绪状态
  • 死亡状态

一个运行状态的线程完成任务或者其他终止条件发生时,线程就进入了终止状态

线程的优先级

每一个java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

java线程的优先级是一个整数,取值范围是1(Thread MIN-PRI0RITY)— 10(Thread MAX-PRI0RITY)


创建线程的方式

下面是我们要用到的线程创建的类

public class MyThread1 implements Runnable{			//这种线程类是通过实现Runnable接口的线程类

    @Override
    public void run(){
        System.out.println("我是通过实现Runnable接口创建的线程");
    }
}


public class MyThread extends Thread{				//这种线程类是通过继承Thread的线程类
    public MyThread(){

    }

    public MyThread(String name){
        super(name);
    }
    @Override
    public void run(){
        System.out.println("我是通过继承Thread创建的线程");
    }
}

方式一:实现Runnable接口的方式
  1. 创建实现了Runnable接口的类
  2. 重写run()方法
  3. 创建实现类对象
  4. 将该实现类作为参数或该实现类和一个String变量作为参数(当我们想要给该线程命名时)传入Thread的构造器中创建一个Thread对象
  5. 调用Thread对象的start()方法,来达到开启线程的目的

start()方法作用

  1. 开启线程
  2. 调用run()方法

下面是摘自菜鸟教程的一段话,我想可以更加深入我们对多线程类的理解

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。
为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:
public void run()
你可以重写该方法,重要的是理解的 run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。

Thread 定义了几个构造方法,下面的这个是我们经常使用的:
Thread(Runnable threadOb,String threadName);
这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。
新线程创建之后,你调用它的 start() 方法它才会运行。

比较实现Runnable接口和继承Thread两种方式的优良性

对于实现Runnable接口和继承Thread来创建多线程的两种方式来说,建议使用实现Runnable接口的方式来创建多线程,可以解决继承Thread创建多线程的局限性,因为继承是单继承的,所以(1)实现Runnable接口还可以继承其他类,(2)实现Runnable接口有利于我们在进行多线程任务时,如果想要数据共享,更好实现;

共同点:都需要重写run()方法,将线程要执行的逻辑声明在run()方法之内


下面是摘自jdk源码的一部分,我想我们可以更深入的了解通过实现Runnable接口较之继承Thread创建多线程的优势

Runnable 接口应该由其实例旨在由线程执行的任何类实现。该类必须定义一个名为 run 的无参数方法。此接口旨在为希望
在活动时执行代码的对象提供通用协议。例如,Runnable 是由类 Thread 实现的。活跃仅仅意味着一个线程已经启动并且还没有停止。此外,Runnable 提供了一个使类处于活跃状态而不是子类化 Thread 的方法。通过实例化 Thread 实例并将自身作为目标传入,实现 Runnable 的类可以在不继承 Thread 的情况下运行。在大多数情况下,如果您只打算覆盖 run() 方法而不打算覆盖其他 Thread 方法,则应该使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则类不应被子类化。

下面是一部分代码:

public static void main(String[] args) {
        Object object = new Object();
        MyThread1 my1 = new MyThread1();

        //将实现了Runnable接口的实现类作为参数传递给Thread的构造方法,通过调用Thread对象的start方法,来达到开启线程的目的。
        //start方法的作用:(1)首先开启线程,(2)之后调用run方法

        Thread thread1 = new Thread(my1);       //系统默认命名
        System.out.println(thread1.getName());
        thread1.start();         //开启线程

        Thread thread2 = new Thread(my1, "自主命名");
        System.out.println(thread2.getName());
        MyThread1 my2 = new MyThread1();	
        /*
        my2.start();
        Error  我们通过查看Runnable接口的源码可以发现,Runnable接口中没有start方法,不能达到开启线程的目的
        但是我们又不能直接调用run方法,直接调用run方法是方法的调用,而不是线程的开启,因此:
        对于实现Runnable接口的实现类,要想开启线程,必须将其作为Thread类的构造方法的参数传递过去
        并调用Thread对象的start方法来开启线程
         */
        /*
        需要注意的是,对于实现了Runnable接口的实现类来说,当我们想要给这样的线程命名时,
        我们不能另外定义一个实现类的构造参数,在实现类构造时传入一个String类型的参数,来达到命名的目的,这样是不对的
        我们知道这个实现类它并没有继承,只是实现了一个接口,因此它的父类是Object,在String类中并没有对应的构造方法
        在创建这种线程时,如果我们想要命名,我们应该将该实现类对象和一个String类型的变量作为参数传递给
        Thread的构造方法来达到命名的目的,当然我们也可以不传入String变量类型,只传入实现类的对象作为参数,这样我们
        线程的名字就是依照系统的命名规则来命名了
         */
    }

方式二:继承Thread的方式
  1. 创建一个继承了Thread的子类
  2. 重写run()方法——>将要执行的逻辑写在这之中
  3. 创建一个子类对象
  4. 将该子类对象传递给一个Thread对象或该对象和一个String变量(如果想要给该线程命名的话)作为其构造方法的参数(其实也可以不穿,直接调用子类的start方法)
  5. 调用Thread对象的start()方法

这种方式有两种命名方法

  • 一种是将线程的名字传给子类(子类需要有一个传入String变量的构造器,并且在子类构造器内调用父类的构造方法命名)
  • 一种是将线程名字传给Thread构造器

代码如下

public static void main(String[] args) {
        MyThread mythread = new MyThread("MyThread命名");			//子类命名线程的名字
        mythread.start();										 //子类开启线程
        System.out.println(mythread.getName());
        Thread thread = new Thread(mythread, "Thread命名");		//Thread命名线程的名字
        System.out.println(thread.getName());
    }

注意:尽管该种方式被认为是一种多线程实现方式,但是其本质也是实现了一个Runnable接口的实例


方式三:使用Callable和FutureTask
  1. 首先创建一个Callable的实现类,并实现call方法(将要执行的逻辑写在其中)
  2. 创建一个FutureTask对象并将Callable的实现类作为参数传递给它的构造函数,
  3. 创建一个Thread对象并将上面的FutureTask对象作为参数传递给它的构造函数
  4. Thread执行start方法
  5. 异常处理机制

下面是代码的实现

第一步:创建一个Callable的实现类

public class MyThread03 implements Callable<Integer> {

    @Override
    public Integer call(){
        return 5;
    }
}
	

第二步:创建一个FutureTask对象

//需要将Callable的实现类对象作为参数传递给FutureTask的构造方法
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread03());

第三步:创建一个Thread对象

//需要将FutureTabsk对象作为参数传递给Thread的构造方法
Thread thread = new Thread(futureTask);

第四步:开启线程

thread.start();             //这里也可以用匿名类

注意:第三步和第四步可以使用匿名类合为一步

第五步:异常处理

try{
            Integer i = futureTask.get();
            System.out.println(i);
        }catch (Exception e){
            e.printStackTrace();
        };

下面是主方法里的代码:

package 练习.多线程练习;

import java.util.concurrent.FutureTask;

public class Main2 {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread03());
        Thread thread = new Thread(futureTask);
        thread.start();             //这里也可以用匿名类
        try{
            Integer i = futureTask.get();
            System.out.println(i);
        }catch (Exception e){
            e.printStackTrace();
        };
    }
}

方式四:线程池

总结:

java创建多线程的方式有几种,四种

Thread方法
被对象调用的方法
1. public void start()
启动线程,java虚拟机调用该线程run()方法
2. public void run()
通常需要重写此方法,将创建的线程要执行的操作声明在此方法中
3. public final void setName(String name)
设置线程对象的名字
4. public final void getName()
获取线程对象的名字
5. public final void setPriority(int priority)
设置线程的优先级
6. public final void getPriority()
获取当前线程对象的优先级
7. public final void setDaemon(Boolean on)
将该线程标记为守护线程还是用户线程
on为true则标记为守护线程,on为false则标记为用户线程
8. join()方法
在线程a中调用线程b的join(),此时线程a就进入到了阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
9. publci void interrupt()
中断线程
10. public final Boolean isAlive()
测试线程是否处于活动状态
Thread类中的静态方法,

类的静态方法可以被Thread类调用

1. public static void yield()
暂停当前正在执行的线程对象,并执行其他线程
2. public static void sleep(long millisec)
让当前线程在指定毫秒数休眠
3. public static Boolean holdsLock(Object object)
当且仅当当前线程在指定的对象保持监视器锁时,才返回true
4. public static Thread currentThread()
返回当前正在执行的线程对象的引用
5. public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流


线程的优先级

设置线程的优先级

public final void setPriority(int priority)

获得线程的优先级

public final void getPriority()

注意:线程的优先级只是说线程执行的概率大小,并不是说一定要高优先级执行完低优先级才执行

这是继承了Thread的类

package 练习.多线程练习;

public class MyThread extends Thread{
    public MyThread(){

    }

    public MyThread(String name){
        super(name);
    }
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName());
    }
}
这是我们在主方法运行的程序
public class Main2 {

    public static void main(String[] args) throws InterruptedException {
        MyThread[] myThreads = new MyThread[10];
        for(int i = 0; i < 10; i++){
            myThreads[i] =  new MyThread();
            myThreads[i].setPriority(i+1);	//从小到大设置优先级
        }
        for(int i = 0; i < 10; i++){
            myThreads[i].start();
        }
    }
}

下面是执行结果

在这里插入图片描述

线程分类

一种是守护线程,一种是用户线程

区别是判断JVM何时离开

  • 守护线程是用来服务用户线程的,在start()方法之前调用thread对象名字.setDoean(true)可以把一个用户线程变成一个守护线程
  • Java垃圾回收就是一个典型的守护线程
  • 若JVM中都是守护线程,当前JVM将会退出

守护线程的意思是一个线程守护另一个线程【此线程非守护线程】,守护的线程称为守护线程,被守护的线程被称为非守护线程。作用是为其他线程运行提供便利的服务。

下面是一个守护线程与非守护线程的例子

//一个继承了Thread的线程类
public class MyThread4 extends Thread{

    public MyThread4(){

    }

    public MyThread4(String name){
        super(name);
    }
    @Override
    public void run(){
        while (true) {
            System.out.println(Thread.currentThread().getName() + "正在运行");
        }
    }
}

//主方法

public class Main2 {

    public static void main(String[] args) throws InterruptedException {
        MyThread4 thread = new MyThread4("守护线程");
        thread.setDaemon(true);
        thread.start();
        System.out.println(Thread.currentThread().getName() + "正在运行!");
    }
}

运行结果:

在这里插入图片描述

从以上结果我们可以看出,主线程停止之后守护线程也停止了,但并不是立即停止


同步与异步的区别

同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去

异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程可以请求得到,A线程无需等待

显然,同步是最安全、最保险的;异步不安全,容易导致死锁,整个进程会崩溃,但是没有同步机制的存在,性能会有所提升


线程的同步

下面是问题的引入:三个窗口卖10张票

我们使用实现Runnable接口的方式创建多线程,下面是实现类

public class MyThread implements Runnable{
    private int count = 10;
    @Override
    public void run(){
        while(count > 0){
            System.out.println("当前线程" + Thread.currentThread().getName() + "卖出票号:" + count);
            count--;
        }
    }
}

下面是主类

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread[] threads = new Thread[3];       //定义Thread数组
        for(int i = 0; i < 3 ; i++){
            threads[i] = new Thread(myThread, "线程" + (i + 1));  //new Thread 并将实现类作为参数传进去
        }
        for(int i = 0; i < 3 ; i++){
            threads[i].start();                 //开启线程
        }
    }
}

我们按照生活中的正常情况的话,应该是三个窗口总共卖出10张票,且每一张票都只卖出一次,不重复卖出

下面是运行结果

在这里插入图片描述

我们发现卖出了重票的问题(显然我们的程序是不合理的,我们要完善我们的程序,使之符合现实的实际情况,),这就涉及到了线程安全问题


解决机制

同步代码块
  • 方式一:同步代码块
synchronized(对象){
	//需要被同步的代码
}

说明:

  1. 操作共享数据的代码,即为需要被同步的代码(代码块不能包含过长或过短)

  2. 共享数据:多个线程共同操作的变量(上面的count就是共享数据)

  3. 同步监视器,俗称锁,任何对象都可以作为锁。要求:多个线程(这几个线程操作了共享数据)必须使用同一把锁。

补充:

  1. 在通过实现Runnable接口的方式创建线程时,考虑使用this充当锁
  2. 在通过继承Thread的方式创建线程时,考虑使用当前对象(当前类名.class)作为锁
  • 对于继承Runnable创建多线程来说

我们使用synchronized(this)方式来同步我们操作共享数据的代码

public class MyThread implements Runnable{
    private int count = 10;
    @Override
    public void run(){
        while (count > 0)
            synchronized(this) {
            {
                System.out.println("当前线程" + Thread.currentThread().getName() + "卖出票号:" + count);
                count--;
            }
        }
    }
}

下面的运行结果证明这种方式的正确性

在这里插入图片描述

  • 对于继承Thread子类创建多线程的方式来说

我们采用synchronized(MyThread.class的方式

为什么?我们来看下面synchronized(this)这个例子

这是子类的代码

package 练习.多线程练习2;

public class MyThread extends Thread{
    private int count = 10;
    @Override
    public void run(){
     while (count > 0)
     {
        synchronized (this) {
            {
                System.out.println("当前线程" + Thread.currentThread().getName() + "卖出票号:" + count);
                count--;
            }
        }
       }
    }
}

下面是主方法

public class Main {
    public static void main(String[] args) {
        MyThread[] threads = new MyThread[3];       //定义Thread数组
        for(int i = 0; i < 3 ; i++){
            threads[i] = new MyThread();  //new Thread 并将实现类作为参数传进去
        }
        for(int i = 0; i < 3 ; i++){
            threads[i].start();                 //开启线程
        }
    }
}

下面是运行结果

在这里插入图片描述

我们依据上面的运行结果发现仍然没有解决问题,为什么呢?每个类要进入同步代码块都必须获得一把锁,里面的对象是锁(synchronized(this)指明每个对象要访问同步时代码块时锁都是自己),我们采用这种方式,就有三把锁了,线程1在运行时获取到线程1锁,线程2在运行时获取到线程2锁,线程3在运行时获取到线程4锁,之间互不影响,并没有起到同步的作用


我们再看看synchronized(Mythread.class)方式的运行结果(类名.class返回一个class类型的对象)

在这里插入图片描述


//这是子类的方法
package 练习.多线程练习2;

public class MyThread extends Thread{
    private  static  int count = 10;
    @Override
    public void run(){
        while (count > 0)
        synchronized (MyThread.class) {
            if (count > 0) {					//在这里加了一个判断,避免一些细节问题的产生
                System.out.println("当前线程" + Thread.currentThread().getName() + "卖出票号:" + count);
                count--;
            }
        }
    }
}

同步方法
  • 方式二:同步方法

如果操作共享数据的代码完整的生命在一个方法中我们不妨将这个方法声明为同步的

@Override
public synchronized void run(){

注意:用继承Thread类的话,用Thread的子类创建多个线程,synchronized注释run()方法不能用(修饰的是非静态方法的话,多把锁,之间独立,并没有起到同步的作用)

因此这种同步机制如果想要修饰run()方法的话,只能修饰实现Runnable接口实现类的run()方法,不能修饰继承Thread类的run()方法

下面是测试代码

package 练习.多线程练习3;

public class MyThread1 implements Runnable{
    private int count = 10;
    @Override
    public void run(){
        System.out.println("当前线程" + Thread.currentThread().getName() + "卖出票号:" + count);
        count--;

    }
}


package 练习.多线程练习3;

import 练习.多线程练习2.MyThread;

public class Main {

    public static void main(String[] args) {
        MyThread1 thread = new MyThread1();
        Thread[] threads = new Thread[3];
        for(int i = 0; i < 3 ; i++){
            threads[i] = new Thread(thread);
        }
        threads[0].start();
        threads[1].start();
        threads[2].start();

    }
}

运行结果如下

在这里插入图片描述

而我们在使用synchronized修饰实现Runnable接口实现类的run()后结果如下

package 练习.多线程练习3;

public class MyThread1 implements Runnable{
    private int count = 10;
    @Override
    public synchronized void run(){
        System.out.println("当前线程" + Thread.currentThread().getName() + "卖出票号:" + count);
        count--;

    }
}

在这里插入图片描述

我们发现完美地解决了问题

关于同步方法的总结

  1. 同步方法仍然涉及到同步监视器
  2. 非静态的同步方法,实际上是对调用该方法的对象加锁
  3. 静态的同步方法,实际上是对调用该方法的类加锁
1. synchronized修饰非静态方法,实际上是对方法的对象加锁,俗称“对象锁”(这也就是对于继承Thread创建的子类的run()方法不能用synchronized修饰的原因)
	* 情况1:
		多线程访问同一个类对象的不同同步方法
		结果:会发生互斥
		解释:因为锁的是对象,当想要调用同步方法时,因为只有一把锁(一个对象),必须等到把锁归还。
	* 情况2:
		多线程访问不同类对象的相同同步方法
		结果:不会发生互斥
		解释:有多个对象,多把锁,之间互不干扰
2. synchronized修饰静态方法,实际上是对该方法的类加锁,俗称“类锁”
	* 情况1:
		访问同一个类的不同同步方法
		结果:会发生互斥
		解释:因为锁的是类,访问时必须等待将类锁归还
	* 情况2:
		多线程访问不同类对象的相同同步方法
		结果:会发生互斥
		解释:因为锁的是类,访问时必须等待将类锁归还
Lock锁(接口)
  • 方式三:Lock锁(JDK 5.0 新增)

从JDK 5.0 开始,java可以通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当

synchronized和Lock的异同

Lock是个接口,锁实现了对共享资源的独占访问,每次只能有一个线程对,线程访问之前应先获得Lock锁对象

ReentrantLock(可重入锁)是一个实现了Lock接口的对象,它拥有与synchronized相同的并发行和内存语义,在实现线程安全的开发中,比较常用的是ReentrantLock锁,可以显式加锁,释放锁

下面是代码

package 练习.多线程练习4;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class  MyThread2 implements Runnable{
    private int count = 10;
    private Lock lock = new ReentrantLock(true);
    @Override
    public void run(){
        lock.lock();
        {
            if (count > 0) {
                try {
                    System.out.println("窗口:" + Thread.currentThread().getName() + "卖出了" + count + "票");
                    count--;
                } catch (IllegalThreadStateException i) {
                    i.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}


package 练习.多线程练习4;

import 练习.多线程练习2.MyThread;

public class Main {

    public static void main(String[] args) {

        MyThread2 myThread2 = new MyThread2();
        Thread[] threads = new Thread[3];

        threads[0] = new Thread(myThread2);
        threads[1] = new Thread(myThread2);
        threads[2] = new Thread(myThread2);
        threads[0].start();
        threads[1].start();
        threads[2].start();
    }
}

运行结果如下

在这里插入图片描述

我们发现并没有重票问题,问题也被解决了

  • 说明

    ReentrantLock(bool fair)含参构造器:
    	fair为true,锁为公平锁,先进先出
    	fair为false,锁为非公平锁,多个线程抢占式
    ReentrantLock()无参构造器:默认为false,非公平锁,抢占式
    
    这里的try其实并非是为了捕获异常,只是为了最后释放锁
    
    
    
  • 相同:二者都可以解决线程安全问题

  • 不同点

    • synchronized在执行完线程后,自动释放同步监视器
    • Lock需要手动的启动同步lock(),同时结束同步unLock()也需要手动的实现
    • Lock是一个接口,而synchronized是一个关键字
    • Lock只有代码块锁,而synchronized有方法锁和代码快锁

Lock: -->同步代码块(已经进入了方法体,分配了相应资源)

		  -->同步方法(在方法体之外)
同步的利弊
  • 好处:同步的方式,解决了线程的安全问题
  • 坏处:操作同步代码块时,只能有一个线程参与,其他线程等待,相当于是一个单线程过程,效率低
什么是锁,什么是死锁

:可以理解为普通意义上的锁,不过锁是用来锁资源的,比如给一段资源(代码块或一个方法)加上一把锁,则这段资源在同一时间只能有一个线程对它访问,只有当这个线程访问完了,其他线程才能访问。这不就是同步机制吗?没错,锁存在的意义,就是让它产生同步机制的。

死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都会处于阻塞状态,无法继续。

下面是死锁现象的举例

//第一个线程等待拿s2,第二个线程等待拿s1
public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run(){
                synchronized (s1){
                    s1.append('a');
                    s2.append('b');
                    try{
                        Thread.sleep(1000);

                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }

                    synchronized (s2){
                        s1.append('a');
                        s2.append('b');
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append('a');
                    s2.append('b');
                    synchronized (s1){
                        s1.append('a');
                        s2.append('b');
                    }
                }
            }
        }).start();
    }
线程通信
  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
  • notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,则唤醒优先级最高的那个
  • notifyAll():唤醒所有被wait的线程

说明

1. wait(), notify(), notifyAll()三个方法必须使用在同步方法或同步代码块中。
2. wait(), notify(), notifyAll()三个方法的调用者必须是同步监视器。否则,会出现IllegalMonitorStateException异常
3. wait(), notify(), notifyAll()三个方法是定义在java.long.Object中的
面试题

wait()和sleep的区别

  • 相同点

    一旦调用,都会进入阻塞状态

  • 不同点

    • 两个方法的声明位置不同:Thread中声明sleep(),java.lang.Object中声明wait
    • 调用的要求不同:sleep()在任何场景中都可以使用,wait()必须使用在同步方法或同步代码块中
    • 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中去,sleep()不会释放,wait()会释放

总结

		释放锁的操作
1. 当前线程的同步方法,同步代码块结束
2. 当前线程在同步方法,同步代码块中遇到了break(),return终止了同步方法,同步代码块的执行
3. 当前线程在同步方法,同步代码块中遇到了Error,未处理的Exception终止了同步方法,同步代码块的执行
4. 当前线程在同步方法,同步代码块中执行了wait(),当前线程暂停,并释放锁
		不会释放锁的操作
1. 线程在同步方法或同步代码块中执行Thread.sleep(),Thread.yield()方法暂停当前线程的执行
2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将线程挂起,该线程不会释放锁(同步监视器)
   应尽量避免使用suspend()和resume()来控制线程

学习于:
https://www.cnblogs.com/vccyb/p/13912112.html

https://www.runoob.com/java/java-multithreading.html

https://www.jianshu.com/p/d901b25e0d4a

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值