掘金 后端 ( ) • 2024-06-23 09:47

一、线程的概念

1、进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础,是一个程序的一次执行过程;一个进程包括由操作系统分配的内存空间,包含了一个或多个线程。

2、线程

线程(Thread)是操作系统能够进行运算调度的最小单位,又称为是轻量级的进程。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程。一个线程不能独立的存在,它是进程的一部分,必须依赖进程而存在。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

3、一个线程的生命周期

线程的生命周期是一个动态执行的一个过程,他也有一个从产生到结束的过程,这个过程就是线程的生命周期,我们把线程的生命周期描述为五种状态:创建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。下面是一个简单的线程生命周期介绍。

1719043875593.png

(1)创建

当创建Thread类的一个线程对象后,该线程对象就处于新建状态,它将保持这个状态直到程序 start() 这个线程。

(2)就绪

当线程Thread对象调用了start()方法之后,该线程就进入就绪状态,此时该线程等待获取CPU的资源,抢到CPU资源之后就到了运行状态。

(3)运行

当就绪的线程获取到CPU资源之后就进入了运行状态,这时会执行run()方法里面的内容。

(4)阻塞

如果一个线程因为某些异常原因执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态,可以重新进入到就绪状态。

(5)死亡

一个运行状态的线程完成任务或者其他终止条件发生时,那么线程就要被销毁,释放资源,意味着一个线程的终结。

二、线程的创建方式

Java中线程的基本创建方式有三种:

1、通过实现 Runnable 接口 2、通过继承 Thread 类 3、通过 Callable 和 Future 创建有返回值的线程。

三、线程控制的基本方法

线程的启动

线程的启动需要调用Thread的start方法,不能直接调用run方法,如果直接调用run方法相当于方法调用。

线程的终止

当run方法返回,线程终止,一旦线程终止后不能再次启动;线程的终止可以调用线程的interrupt方法。

Thread相关方法

public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

public void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

public final void setName(String name)
修改线程名字。

public final void setPriority(int priority)
更改线程的优先级。

public final void setDaemon(boolean on)
修改线程为守护线程或用户线程。

public final void join()
插入线程/插队线程

public void interrupt()
中断线程

public final boolean isAlive()
检查线程是否还在活动

上述方法是被 Thread 对象调用的,下面介绍Thread的静态方法

public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。

public static void sleep(long millisec)
让当前线程休眠指定时间,单位毫秒

public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。

public static Thread currentThread()
返回对当前正在执行的线程对象的引用。

public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

四、线程创建详细描述

线程的创建方式共有三种,其中常用的Runnable 接口和Thread 类是没有返回值的,而通过 Callable 和 Future 创建线程的方式适用于需要有返回值的线程;下面将做详细说明。

1、通过实现 Runnable 接口来创建线程

下面是一个简单的通过实现 Runnable 接口来创建线程的示例:

/**
 * 实现Runnable接口并重写run()方法
 */
public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //获取当前线程对象
            Thread t = Thread.currentThread();
            System.out.println("我是线程"+t.getName());
        }

    }
    public static void main(String[] args) {
        //创建demo对象
        RunnableDemo demo1 = new RunnableDemo();
        RunnableDemo demo2 = new RunnableDemo();
        Thread thread1= new Thread(demo1,"1");
        Thread thread2 = new Thread(demo2);
        //设置线程名称
        thread2.setName("2");
        //启动线程
        thread1.start();
        thread2.start();
    }
}

运行以上代码之后得到下面的结果,可以看到两个线程是交替执行:

我是线程1
我是线程2
我是线程2
我是线程2
我是线程1
我是线程2
我是线程2
我是线程1
我是线程1
我是线程2
我是线程2
我是线程2
我是线程2
我是线程2
我是线程1
我是线程1
我是线程1
我是线程1
我是线程1
我是线程1

Process finished with exit code 0

2、通过继承Thread来创建线程

下面是一个简单的通过继承Thread来创建线程的示例:


public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是线程"+getName());
        }
    }

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        myThread1.setName("1");
        myThread2.setName("2");
        myThread1.start();
        myThread2.start();
    }
}

运行之后得到如下结果:

我是线程2
我是线程1
我是线程1
我是线程1
我是线程1
我是线程1
我是线程1
我是线程2
我是线程1
我是线程2
我是线程2
我是线程2
我是线程2
我是线程2
我是线程1
我是线程2
我是线程1
我是线程2
我是线程2
我是线程1

Process finished with exit code 0

3、通过 Callable 和 Future 创建线程

  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。


public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 0; i <= 100; i++) {
            sum=sum+i;
        }
        return sum;
    }

    public static void main(String[] args) throws Exception {
        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable callable = new MyCallable();
        //创建FutureTask的对象(作用管理多线程运行的结果)
        FutureTask futureTask = new FutureTask<>(callable);
        //创建Thread类的对象,并启动(表示线程)
        Thread t1 = new Thread(futureTask);
        //启动线程
        t1.start();
        //获取多线程运行的结果
        Object result = futureTask.get();
        System.out.println(result);
    }

}

运行结果如下:

5050

Process finished with exit code 0

4、三种方式的比较

  1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类,更加的灵活,但是不能直接访问Thread中的方法,编程相对复杂。

  2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程,缺点是扩展性较差,不能继承其他类。

  3. Runnable 和 Thread 是没有返回值的,而 Callable 是有返回值的。

5、使用Lambda表达式创建线程


public class RunnableThreadTest {
    // 目的是为了代码的重用【静态方法】
    public static void threadRunCode_Static() {
            System.out.println(Thread.currentThread().getName() + " " + 
    }
 
    // 目的是为了代码的重用【非静态方法】
    public void threadRunCode() {
            System.out.println(Thread.currentThread().getName() + " " + i);
    }
 
    @Test
    public void testNoStatic() {
        // 重用非静态方法中的代码【使用方法引用】
        RunnableThreadTest temp = new RunnableThreadTest();
        new Thread(temp::threadRunCode, "线程1").start();
    }
 
 
    @Test
    public void testStatic() {
        // 重用静态方法中的代码【使用方法引用】
    new Thread(RunnableThreadTest::threadRunCode_Static, "线程1").start();
             
    }
    @Test
    public void testLambda() {
        // 重用静态方法中的代码【使用方法引用】
        new Thread(() -> {                       System.out.println(Thread.currentThread().getName() + " " + b);
                },"线程1").start();
}
 

6、使用匿名内部类实现线程


  new Thread() {                                 
          public void run(){                      //2.重写run方法
              for(int i = 0;i < 1000;i++) {
                  System.out.println(i);
              }
          }
      }.start();                                  




  new Thread(new Runnable() {                 
      public void run(){                      //2.重写run方法
          for(int i = 0;i < 1000;i++) {               
              System.out.println(i);
          }
      }           
  }).start();                                

五、线程同步

如果有一个影院在卖100张票,它有多个窗口,如果多个窗口同时操作一张票,那么就会导致混乱了,这将导致线程安全的问题。

如下面的代码所示:


public class MyThread1 implements Runnable{
    static int  ticket = 0;
    @Override
    public void run() {
        while (true){
            //判断共享数据是否到了末尾,如果到了末尾就结束线程
            if (ticket==100){
                break;
            }else {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
            }
        }
    }

    public static void main(String[] args) {
        MyThread1 thread1 = new MyThread1();
        MyThread1 thread2 = new MyThread1();
        Thread a = new Thread(thread1,"a");
        Thread b = new Thread(thread2,"b");
        a.start();
        b.start();
    }

}


运行结果如下:


......
b正在售卖第95张票
a正在售卖第95张票
a正在售卖第96张票
b正在售卖第97张票
b正在售卖第99张票
a正在售卖第99张票
a正在售卖第100张票
b正在售卖第101张票

可以看到存在两个线程在抢同一个资源,这样显然是有问题的

那么如何去解决这种问题呢?线程同步就是为了解决这一问题的。

ynchronized关键字

• synchronized 是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

1、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

2、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

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

下面展示加锁的代码采用了同步方法:


public class MyThread1 implements Runnable{
    static int  ticket = 0;
    @Override
    public void run() {
        while (true){
            //同步代码块
            if(method()){
                break;
            }

        }
    }
    public synchronized  boolean method(){
        if (ticket>=100){
            return true;
        }else {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ticket++;
            System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
        }
        return false;
    }

    public static void main(String[] args) {
        MyThread1 thread1 = new MyThread1();
        Thread a = new Thread(thread1,"a");
        Thread b = new Thread(thread1,"b");
        a.start();
        b.start();
    }

}


输出结果如下,可以看到解决了线程安全问题。


b正在售卖第90张票
a正在售卖第91张票
b正在售卖第92张票
b正在售卖第93张票
a正在售卖第94张票
a正在售卖第95张票
b正在售卖第96张票
b正在售卖第97张票
a正在售卖第98张票
b正在售卖第99张票
a正在售卖第100张票

Process finished with exit code 0


六、死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种僵局,无一个线程能够继续执行下去。

为了避免死锁,可以采取以下措施:

避免在一个线程内部做长时间的操作,特别是与其他资源有依赖关系的操作。

设计时尽量保证资源的有序获取,避免循环等待条件。

使用定时锁而非一直等待,给锁设置超时时间。

下面是一个简单的造成死锁的代码:


public class MyLock {
        private static Object lockA = new Object();
        private static Object lockB = new Object();

        public static void main(String[] args) {
           new Thread(new Runnable() {
                public void run() {
                    synchronized (lockA) {
                        System.out.println("Thread 1: Holding lock A");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (lockB) {
                            System.out.println("Thread 1: Holding lock B");
                        }
                    }
                }
            }).start();

           new Thread(new Runnable() {
                public void run() {
                    synchronized (lockB) {
                        System.out.println("Thread 2: Holding lock B");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (lockA) {
                            System.out.println("Thread 2: Holding lock A");
                        }
                    }
                }
            }).start();

        }

}

可以看到两个线程在都在等待对方释放,导致了死锁,程序将无法向下面执行。

七、线程通信

在Java中,线程间的通信通常是通过共享对象的wait()和notify()方法来实现的。这些方法是Object类的成员方法,用于控制线程的执行。

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

以下是一个简单的例子,展示了如何使用wait()和notify()方法来实现线程间的通信:


public class CommDemo {
    private boolean ready = false;

    public synchronized void print(String msg) {
        while (!ready) {
            try {
                System.out.println("等待其他线程释放中");
                wait(); // 线程等待
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
        System.out.println(msg);
        ready = false; // 重置标志
        notifyAll(); // 唤醒其他等待的线程
    }

    public synchronized void setReady() {
        ready = true;
        notifyAll(); // 通知等待的线程
    }

    public static void main(String[] args) {
        CommDemo demo = new CommDemo();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.print("Thread A finished execution.");
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                Thread.sleep(5000);
                demo.setReady();
            }
        });

        threadA.start();
        threadB.start();
    }
}


上面的例子中,有一个ready标志来控制是否能打印,线程A进入打印时发现在使用中,那么线程A阻塞,5秒钟后,线程B修改ready标志并通知线程A,线程A使用打印方法。