Java:多线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rn0gU5Z5-1595728834282)(F:\JianShu_material\Java\多线程\000.png)]

1. 创建多线程的方式一:继承Thread类

1.1 Thread创建多线程

Thread创建多线程步骤

  1. 创建一个继承Thread类的子类
  2. 重写Thread类的run( )方法 ------>将此线程执行的操作声明在run( )方法中
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start( )方法

举例:说明创建多线程过程

题目:在线程1内输出0-100间的奇数,在线程2内输出0-100间的偶数。


// 1.创建一个继承Thread类的子类
class MyThread extends Thread {

//2.重写Thread类的run( )方法  ------>将此线程执行的操作声明在run( )方法中
@Override
public void run() {
   for(int i = 0; i <= 100; i++){
       if(i % 2 ==0){
           System.out.println("线程2:" + i);
       }
   }
}
}

public class ThreadTest {
public static void main(String[] args) {
   //3.创建Thread类的子类的对象
   MyThread t1 = new MyThread();
   
   //4.通过此对象调用start( )方法
   //start()有两个功能:①启动当前线程 ②调用当前线程的run()
   t1.start();
   //注意:不能直接通过调用run()方法启动线程
   //t1.run()
   
   //注意:一个对象只能调用一次start(),若想重新创建一个线程需重新创建一个对象

   //在主线程中输出0-100的奇数
   for(int i = 0; i <= 100; i++){
       if(i % 2 !=0){
           System.out.println("线程1:" + i);
       }
   }
}
}

//在上述过程中,先进入main()函数,创建t1,调用start()方法
//在此之后线程1、2交互运行,分为两条线

练习:用匿名对象创建多线程

题目:在两个分线程中用匿名对象的方法实现在线程1内输出0-100间的奇数,在线程2内输出0-100间的偶数。

public class ThreadExer {
    public static void main(String[] args) {

        //通过匿名对象重写run(),调用start()
        new Thread(){
            public void run(){
                for(int i = 0; i <= 100; i++){
                    if(i % 2 ==0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();

        //通过匿名对象重写run(),调用start()
        new Thread(){
            public void run(){
                for(int i = 0; i <= 100; i++){
                    if(i % 2 !=0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
    }
}

1.2 线程中的常用方法

线程中有哪些常用方法

  1. start():启动当前线程;调用当前线程的run()
  2. run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  3. currentThread():静态方法,返回执行当前代码的线程
  4. getName():获取当前线程的名字5.setName():设置当前线程的名字
  5. yield():释放当前CPU的执行权
  6. join():在线程a中调用线程b的join(),此时线a就进入阻塞状态,直到线b完全执行完以后,线程a才结束阻塞状态。
  7. sleep(Long millitime):当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前我程是阻塞状态。
  8. isAlive():判断当前线程是否存活
  9. stop():当执行此方法时,强制结束当前线程。已过时
package code1;

class HolleThread extends Thread {

    @Override
    public void run() {
        for(int i = 0; i <= 100; i++){
            if(i % 2 ==0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            if(i == 30){
                // 因为重写的run()没有抛出异常,所以只能使用try-catch
                try {
                    // 当i=30时,该线程阻塞1秒
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if(i % 20 ==0){
                // 完整写法应该为:this.yield(),
                // this代表当前对象,与Thread.currentThread()意义相同
                yield();
            }
        }
    }

    // 设置构造器来命名,因为父类Thread中有方法用来命名
    public HolleThread(String name){
        super(name);
    }
    public HolleThread(){

    }
}

public class ThreadMethodTest {
    public static void main(String[] args) {
        HolleThread h1 = new HolleThread("线程一");
        // 使用setName()命名
        h1.setName("线程一");
        h1.start();

        Thread.currentThread().setName("主线程");
        for(int i = 0; i <= 100; i++){
            if(i % 2 !=0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            if(i == 20){
                try {
                    // 调用h1的join(),使主线程发生阻塞,开始执行h1的线程
                    // 直到h1的线程执行完后,才开始执行主线程
                    h1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        // 判断h1线程是否存活
        System.out.println(h1.isAlive());
    }
}

1.3 线程的优先级

线程优先级的介绍

线程的优先级:

  1. MAX_PRIORITY:10
  2. MIN_PRIORITY:1
  3. NORM_PRIORITY:5 ----->默认优先级

如何获取和设置当前线程的优先级:

  1. getPriority():获取线程的优先级
  2. setPriority(int p):设置线程的优先刻

说明: 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概事上讲,高优先级的线程高概事的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

package code1;

class MyThread extends Thread {

    @Override
    public void run() {
        for(int i = 0; i <= 100; i++){
            if(i % 2 ==0){
                // getPriority()设置线程的优先级
                System.out.println(getName() + getPriority() + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        // setPriority()设置线程的优先级
        t1.currentThread().setPriority(Thread.MAX_PRIORITY);
        t1.start();
        
        for(int i = 0; i <= 100; i++){
            if(i % 2 !=0){
                System.out.println("线程1:" + i);
            }
        }
    }
}

2. 创建多线程的方式二:实现Runnable接口

2.1 Runnable接口创建多线程

Runnable创建多线程步骤

  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法:run( )
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start( )
package code1;

// 1.创建一个实现了Runnable接口的类
class MThread implements Runnable{

    // 2.实现类去实现Runnable中的抽象方法:run( )
    @Override
    public void run() {
        for(int i =0; i < 100; i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        // 3.创建实现类的对象
        MThread mThread = new MThread();
        // 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t = new Thread(mThread);
        // 5.通过Thread类的对象调用start()
        t.start();

        // 若再想启动一个线程,只用创建一个Thread对象,不用创建实现类的对象
        Thread t_1 = new Thread(mThread);
        t_1.start();

    }
}

2.2 Thread和Runnable的比较

比较创建线程的两种方式

开发中,优先选择实现Runnable接口的方式创建多线程

原因:

  • 实现的方式没有类的单继承的局限性,因为Java只支持单继承,继承了Thread类就不能继承其他类
  • 实现的方式更适合来处理多个线程有共享数据的情况

联系: 在API中Thread类也实现了Runnable接口

相同点: 两种方式都需要重写run( ),将要执行的逻辑声明在run( )中

3. 线程的生命周期

理解线程的生命周期

线程的几种状态:

  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  • 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  • 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

>[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLtFpVw4-1595728834287)(F:\JianShu_material\Java\多线程\001.png)]

4. 线程的安全问题

4.1 线程安全问题的概念

什么是线程的安全问题

概念: 线程安全问题的出现是由于存在共享数据,比如窗口卖票的票数,银行卡的钱数。

表现: 出现重票、错票就是指出现了线程的安全问题。在极端情况下,窗口一进入线程后发生阻塞,此时窗口二也进入了线程,也发生了阻塞,那么它们对于共享数据票数的操作将会异常。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6E6wi76E-1595728834293)(F:\JianShu_material\Java\多线程\002.png)]

原因: 当某个线程操作共享数据的过程中,尚未操作完成时,其他线程参与进来,也操作共享数据。

解决方案: 当一个线程a在操作共享数据的时候,其他线程不能参与进来。直到线程a操作完共享数据时,其他线程才可以操作共享数据。这种情况即使线程a出现了阻塞,也不能被改变。

4.2 解决线程安全问题的方法一:同步代码块

解决方法:同步代码块

synchronized(同步监视器){
//需要被同步的代码
}

说明:

  1. 需要被同步的代码,就是要操作的共享数据
  2. 共享数据:多个线程共同操作的变量,比如票数
  3. 同步监视器:俗成锁,任何一个类的变量,都可以充当锁
  4. 要求:多个线程必须共用一把锁

补充:

  1. 在实现Runnable接口创建多线程的方式中,我们可以考感使用this充当同步监视器。
  2. 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考感使用当前类充当同步监视器。类名.class

举例:用同步代码块解决实现Runnable接口的线程安全问题

题目: 3个窗口卖共100张票


class Window1 implements Runnable{
    private int ticket = 100;
    // 设置同步监视器,即锁
    Object obj = new Object();

    @Override
    public void run() {
        while (true){
            // 将操作共享数据的代码用synchroized()包住
            // 可以考虑使用synchronized(this)充当同步监视器
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.start();
        t2.start();
        t3.start();
    }
}

举例:用同步代码块解决继承Tread类的线程安全问题

题目: 3个窗口卖共100张票


class Window extends Thread {

    private static int ticket = 100;

    //使用static关键字保证其同步监视器唯一
    private static Object obj = new Object();


    @Override
    public void run() {
        while(true){
            // 也可以用synchronized(Whindow.class)充当同步监视器
            synchronized (obj){
                if(ticket > 0){
                    System.out.println(getName() + ":" + ticket);
                    ticket--;
                }else
                    break;
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {

        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.start();
        w2.start();
        w3.start();

    }
}

4.3 解决线程安全问题的方法二:同步方法

解决方法:同步方法

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

将此方法用synchronized修饰,将该方法放入run()函数中。

注意:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们最式的声明。
  2. 非静态的同步方法,同步监视器是:this。
  3. 静态的同步方法,同步监视器是:当前类本身。

举例:用同步方法解决实现Runnable接口的线程安全问题

题目: 3个窗口卖共100张票

package first.knowlodge.code;

class Window2 implements Runnable{
    private int ticket = 100;
    // 设置同步监视器,即锁
    Object obj = new Object();

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

            show();
        }
    }

    // 此时默认的同步监视器为this
    private synchronized void show(){

            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + ":" + ticket);
                ticket--;
            }
    }
}
public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.start();
        t2.start();
        t3.start();
    }
}

举例:用同步方法解决继承Tread类的线程安全问题

题目: 3个窗口卖共100张票

package first.knowlodge.code;

class Window3 extends Thread {

    private static int ticket = 100;


    @Override
    public void run() {
        while(true){
            show();
        }
    }
    
    // 此时默认的同步监视器是Window3.class
    // static不能缺少,其保证了锁是同一把锁
    private static synchronized void show(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName() + ":" + ticket);
            ticket--;
        }
    }
}

public class WindowTest3 {
    public static void main(String[] args) {

        Window3 w1 = new Window3();
        Window3 w2 = new Window3();
        Window3 w3 = new Window3();

        w1.start();
        w2.start();
        w3.start();

    }
}

4.4 解决线程安全问题的方法三:lock锁

解决方法:lock锁

步骤:

  1. 创建ReentranLock类的对象
  2. 将操作共享数据的代码用try包含,在第一行使用ReentranLock类的lock()方法,实现加锁
  3. 使用finally与try配合,在finally第一行使用ReentranLock类的unlock()方法,实现解锁

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{
    private int ticket = 100;
    // 实例化ReentrantLock
    ReentrantLock lock = new ReentrantLock();

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

                // 创建同步监视器
                lock.lock();

                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally {
                // 解除同步监视器
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.start();
        t2.start();
        t3.start();
    }
}

4.5 synchronized 与lock的异同

两种解决线程安全问题的对比

相同:

  1. 两者都可以解决线程安全问题

不同:

  1. lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
  2. lock只有代码块锁,synchronized有代码块锁和方法锁。
  3. 使用lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。

优先使用顺序:

lock锁--------> 同步代码块(已经进入了方法体,分配了相应资源)--------> 同步方法(在方法体之外)

4.6 死锁问题

死锁问题

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

说明:

  1. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
  2. 我们使用同步时,要避免出现死锁。

解决方法:

  1. 使用专门的算法、原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步

5. 线程的通信

5.1 线程通信的说明

线程的通信

概念: 是指在多线程使用过程中使用三个方法,达到控制线程先后顺序的目的

三个方法:

  1. wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  2. notify(): 一旦执行此方法,就会唤醒截wait的一个线程。如果有多个线程被wait,就唤醒优先级高的线程。
  3. notifyAll(): 一旦执行此方法,就会唤醒所有被wait的线程。

注意:

  1. wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
  2. wait(),notify(),notifyALL()三个方法的调用者必须是同步代码染或同步方法中的同步监视器,否则,会出现ILLegalMonitorStateException异常。
  3. wait(),notify(),notifyAlL()三个方法是定义在java.Lang.Object类中声明的。

举例:使用线程的通信解决问题

题目: 线程1和线程2交替打印1-100

过程:

  1. 假设线程1先操作共享数据,输出1后,触发wait(),被阻塞,同时释放同步监视器,允许线程2进入。
  2. 线程2得到线程1释放的同步监视器进入代码中,触发notify(),线程1被唤醒,离开代码。
  3. 线程2执行步骤1.的步骤,然后就实现了线程1和线程2交替打印1-100.

class Number implements Runnable{
    private int number = 1;

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

            // 唤醒造成阻塞的线程
            notify();
            synchronized (this) {
                if (number <= 100) {
                    System.out.println(Thread.currentThread().getName() + number);
                } else {
                    break;
                }

                // 使用wait()造成当前线程阻塞,并解锁同步监视器
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}
public class CommunicateTest {
    public static void main(String[] args) {
        Number n = new Number();
        Thread t1 = new Thread(n);
        Thread t2 = new Thread(n);
        Thread t3 = new Thread(n);

        t1.start();
        t1.start();
        t1.start();
    }
}

5.2 sleep()和wait()的对比

两者间的对比

相同点: 一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点:

  1. 两个方法声明的位置不同:Thread类中声明sLeep(),Object类中声vait()。
  2. 调用的要求不同:sLeep()可以在在何需要的场景下调用。wait()必须使用在同步代码块或同步方法。
  3. 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不释放同步监视器,wait()则释放同步监视器。

6. 创建多线程的方式三:实现Callable

6.1 Callable接口创建多线程

Callable接口创建多线程步骤

  1. 创建一个实现Callable接口的实现类
  2. 实现Call方法,将此线程需要声明的方法写入Call()中
  3. 创建Callable接口实现类的对象
  4. 将Callable接口实现类的对象作为参数传递到FutureTask类的构造器中,创建FutureTask对象
  5. 将FutureTask的对象作为参数,传递到Thread类中,创建thread对象调用start方法
  6. 获取Callable中call()返回值

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1.创建一个实现Callable接口的实现类
class NumberSum implements Callable{
    // 2.实现Call方法,将此线程需要声明的方法写入Call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 1; i <= 100; i++){
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class NewThread {
    public static void main(String[] args) {
        // 3.创建Callable接口实现类的对象
        NumberSum numberSum = new NumberSum();

        // 4.将Callable接口实现类的对象作为参数传递到FutureTask类的构造器中,创建FutureTask对象
        FutureTask futureTask = new FutureTask(numberSum);

        // 5.将FutureTask的对象作为参数,传递到Thread类中,创建thread对象调用start方法
        new Thread(futureTask).start();

        try {
            // get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            // 6.获取Callable中call()返回值
            Object sum = futureTask.get();
            System.out.println("总数和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

6.2 Callable和Runnable的比较

两者间的比较

  1. Callable接口比Runnable接口的功能强大
  2. call()有返回值
  3. call()可以抛出异常,被外面的操作捕获,获取异常的信息
  4. Callable支持泛型

7. 创建多线程的方式四:使用线程池

7.1 线程池创建多线程

为什么要用线程池创建多线程

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。

可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

线程池创建多线程步骤

  1. 提供指定数量的线程池
  2. 执行指定的线程的操作,需要提供实现Runnable或Callable接口实现类的对象

import java.lang.reflect.Executable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

class NumberThread2 implements Runnable{
    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

public class ThreadPool {
    public static void main(String[] args) {
        // 1.提供指定数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        // 2.执行指定的线程的操作,需要提供实现Runnable或Callable接口实现类的对象
        service.execute(new NumberThread1()); // 适合Runnable接口
        service.execute(new NumberThread1()); // 适合Runnable接口
        // 适合Callable接口
        service.submit()  
    }
}

7.2 线程池的好处

使用线程池的好处有哪些

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值