Java多线程学习指南

本文详细探讨了Java中的多线程概念,包括程序、进程和线程的区别,线程的创建方式(如继承Thread类和实现Runnable接口),线程的生命周期、同步和通信。深入分析了线程同步的多种方法,如Synchronized和Lock机制,以及解决线程安全问题的策略。此外,还介绍了线程死锁和生产者消费者问题的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

chap8多线程

8-1程序、进程、线程

线程:每个进程(即运行起来的程序)还可以支持并行的指向多个线程。线程拥有独立的运行栈和程序计数器PC。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KkRa5yAF-1644342913716)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206115728339.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Chxwwjei-1644342913717)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206115843369.png)]

1.内存结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lR8XwvkY-1644342913717)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206120039704.png)]

每个线程都有自己独立的栈和程序计数器

2.并行与并发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tc1UwLO-1644342913718)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206120413732.png)]

3.优点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KIpR7JAg-1644342913718)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206120959847.png)]

8-2 创建线程的方式

1.对Thread类的理解

java.lang.Thread 类

Thread类的对象,人如其名,可以抽象的把它看作一个线程。它要执行的内容,封装在Thread对象的run()方法中。

但直接启动这个线程的办法,是start()方法。

Thread类结构:

构造器:

 Thread():创建新的Thread对象 Thread(String threadname):创建线程并指定线程实例名    	
 Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法 
 Thread(Runnable target, String name):创建新的Thread对象

2.方式一:Thread子类

方式一:继承Thread类

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
/**

线程的创建:方式1 Thread子类

 */
public class ThreadTest  {
    public static void main(String[] args) {
        Mythread mythread=new Mythread();
        mythread.start();
    }
}

class Mythread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            if(i%2==0)
                System.out.println(i);
        }
    }
}

注意:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-osiya2XU-1644342913719)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122124519.png)]

从整个时间的角度来看,时间是这样子的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ofug9Vye-1644342913720)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220118195011841.png)]

3.方式二:实现Runnable接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0goh2DDz-1644342913720)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206123645682.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8zGOIkXt-1644342913721)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206123659182.png)]

public class ThreadTest1 {
    
    Thread thread=new Thread(new MThread());
}
class MThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {

        }
    }
}

4.线程的有关方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-heCdXYwg-1644342913722)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122650024.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1Wm1YZ6-1644342913722)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122702917.png)]

5.线程的调度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdyiCYhU-1644342913723)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122742525.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVLKVkO6-1644342913723)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122754291.png)]

6.线程的分类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1aJ3RSe-1644342913724)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122822346.png)]

8-3线程的生命周期

1.线程的几种状态

在JDK中用Thread.State类定义了线程的五种状态:

  1. 新建:一个Thread类的对象被创建后,start前
  2. 就绪:线程被start后,但没有分配到CPU资源
  3. 运行:当就绪的线程被调度并且获得CPU资源时
  4. 阻塞:在运行的过程中被临时挂起
  5. 死亡:被强制性的终止或者异常导致结束

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwtQTvS9-1644342913724)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206134729487.png)]

8-4线程的同步

同步这一范畴的提出是基于这样的问题:

多个线程在操作共享的数据时,可能会同时进行。比如同时在银行取2000块钱,那么有可能出现只更新一次的风险。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfQ5h091-1644342913725)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220118221912515.png)]

下面来看一个问题来加深这个现象的理解:

模拟火车站售票程序,开启三个窗口售票。

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 t = new Window1();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }

}

class Window1 implements Runnable{
    private int  ticket=100;

    @Override
    public void run() {
        while (true){
            if(ticket>0){

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName()+ ":卖票");
                ticket--;
            }else
                break;
        }
    }
}

如果是这样的程序,会输出票数为-1或0的情况,来分析一下为什么会出现这个问题。

在极端状态下,三个线程同时能因为是1而进入。但是都被阻塞,这样再输出时就会出现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7IgrpiKU-1644342913725)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220120220536191.png)]

1.多线程产生的问题

问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。

解决办法:

共享数据执行线程时,让一个线程先执行,其他线程不允许执行,

下面是Java当中的解决办法

2.同步的方式一:Synchronized同步代码块方式

同步机制Java中有两种写作方式:

1. 同步代码块:
synchronized (同步监视器){
// 需要被同步的代码;
}
2. synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (String name){ 
….
}

1.同步的原理?其实就是多个线程,在某一过程面前停下来,只让一个先从先走,走完了其他再上。

这里,觉得的单元是逻辑代码而不是数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXMEYpTY-1644342913725)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220120223337636.png)]

2.同步监视器是什么?

俗称 锁

谁(某个线程)能拿到锁,就能操作后面的代码。

任何类的对象都能充当锁。

但是:多个线程,必须公用同一个锁

3.如果用同步方法来实现同步,则同步方法的锁:静态方法(类名.class)、非静态方法(this)

3.方式二:同步方法

代码示例:

/**
 * 同步方法来解决线程同步问题
 *
 */
public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t = new Window2();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("t1窗口");
        t2.setName("t2窗口");
        t3.setName("t3窗口");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window2 implements Runnable{

    private int  ticket=100;
    Object obj=new Object();

    @Override
    public void run() {
       while (true){
           show();
       }
    }

    private synchronized void show(){
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":卖票");
            ticket--;
        }
    }
}

1.同步方法好像没有指出同步监视器,它真的没有锁吗?

答案是有,就是this,即本对象。

4.对同步问题的讨论

1、如何找问题,即代码是否存在线程安全?(非常重要)

(1)明确哪些代码是多线程运行的代码

(2)明确多个线程是否有共享数据

(3)明确多线程运行代码中是否有多条语句操作共享数据

2、如何解决呢?

(非常重要) 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的这些语句都要放在同步范围中

3、切记:

范围太小:没锁住所有有安全问题的代码

范围太大:没发挥多线程的功能。

5.释放锁

在遇到下面的情况时,线程会把锁释放掉,以给下一个线程占据

 当前线程的同步方法、同步代码块执行结束。

 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导 致异常结束。
 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。

不会释放锁的操作:

注意:线程执行同步代码块或同步方法时,程序调用Thread.sleep()、 Thread.yield()方法暂停当前线程的执行 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程 挂起,该线程不会释放锁(同步监视器)。 应尽量避免使用suspend()和resume()来控制线程

6.单例设计模式之懒汉式(线程安全)

所谓的懒汉式,就是这样设计的类:

提供一个公共静态方法来获取类的对象

public class Bank {

    private Bank(){}

    private static Bank instance=null;
    
    public static Bank getInstance(){
        if (instance==null){
            instance=new Bank();
        }
        return instance;
    }
}
  • 因为有判定语句,所以在多线程运行时,可能会创建两个对象,线程不安全。因此要对这个设计模式进行线程安全改造
public class Bank {

    private Bank(){}

    private static Bank instance=null;

    public synchronized static Bank getInstance(){
        if (instance==null){
            instance=new Bank();
        }
        return instance;
    }
}

7.死锁问题

定义:

不同的线程,分别占有对方线程所需要的资源不放弃,这样,双方都无法获得资源而结束自己的线程。

演示死锁问题:

package com.atguigu.java1;
//死锁的演示
class A {
   public synchronized void foo(B b) { //同步监视器:A类的对象:a
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 进入了A实例的foo方法"); // ①
//    try {
//       Thread.sleep(200);
//    } catch (InterruptedException ex) {
//       ex.printStackTrace();
//    }
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 企图调用B实例的last方法"); // ③
      b.last();
   }

   public synchronized void last() {//同步监视器:A类的对象:a
      System.out.println("进入了A类的last方法内部");
   }
}

class B {
   public synchronized void bar(A a) {//同步监视器:b
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 进入了B实例的bar方法"); // ②
//    try {
//       Thread.sleep(200);
//    } catch (InterruptedException ex) {
//       ex.printStackTrace();
//    }
      System.out.println("当前线程名: " + Thread.currentThread().getName()
            + " 企图调用A实例的last方法"); // ④
      a.last();
   }

   public synchronized void last() {//同步监视器:b
      System.out.println("进入了B类的last方法内部");
   }
}

public class DeadLock implements Runnable {
   A a = new A();
   B b = new B();

   public void init() {
      Thread.currentThread().setName("主线程");
      // 调用a对象的foo方法
      a.foo(b);
      System.out.println("进入了主线程之后");
   }

   public void run() {
      Thread.currentThread().setName("副线程");
      // 调用b对象的bar方法
      b.bar(a);
      System.out.println("进入了副线程之后");
   }

   public static void main(String[] args) {
      DeadLock dl = new DeadLock();
      new Thread(dl).start();


      dl.init();
   }
}

8.方式三:Lock锁机制来实现同步

lock机制是java5的新特性,通过显式定义同步锁对象来实现同步。

不过Lock只是个接口,ReentrantLock才是实现类。用它来实现代码的同步:

class Window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法lock()
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }
}
/**
 * 解决线程安全问题的方式三:Lock锁  --- JDK5.0新增
 *
 * 1. 面试题:synchronized 与 Lock的异同?
 *   相同:二者都可以解决线程安全问题
 *   不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
 *        Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
 *
 * 2.优先使用顺序:
 * Lock  同步代码块(已经进入了方法体,分配了相应资源)  同步方法(在方法体之外)
 *
 *
 *  面试题:如何解决线程安全问题?有几种方式
 * @author shkstart
 * @create 2019-02-15 下午 3:38
 */

8-5线程的通信

解释:线程的通信就是两个线程之间,建立一种关系。

例如,下面这个例题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TL7nbDTh-1644342913726)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220208225749574.png)]

package com.atguigu.java2;

/**
 * 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
 *
 *
 * 涉及到的三个方法:
 * wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
 * notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
 * notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
 *
 * 说明:
 * 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
 * 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
 *    否则,会出现IllegalMonitorStateException异常
 * 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
 *
 * 面试题:sleep() 和 wait()的异同?
 * 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
 * 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
 *          2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
 *          3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
 *
 * @author shkstart
 * @create 2019-02-15 下午 4:21
 */
class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while(true){

            synchronized (obj) {

                obj.notify();

                if(number <= 100){

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }
    }
}


public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}
  • 面试题:sleep() 和 wait()的异同?
  • 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
  • 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
  • 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
  • 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

例题1 生产者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如 果店中有产品了再通知消费者来取走产品。 这里可能出现两个问题: 生产者比消费者快时,消费者会漏掉一些数据没有取到。 消费者比生产者快时,消费者会取相同的数据。

解答:关键在于,把Clerk作为中间类,消费者和生产资作为进程类。

每当产品大于20个时,终止当前线程(这时,线程肯定是生产者线程)

小于0个时,终止当前线程。

class Clerk{

    private int productCount = 0;
    //生产产品
    public synchronized void produceProduct() {

        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");

            notify();

        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
    //消费产品
    public synchronized void consumeProduct() {
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;

            notify();
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

class Producer extends Thread{//生产者

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品.....");

        while(true){

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.produceProduct();
        }

    }
}

class Consumer extends Thread{//消费者
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品.....");

        while(true){

            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consumeProduct();
        }
    }
}

public class ProductTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消费者2");

        p1.start();
        c1.start();
        c2.start();

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值