Java多线程(线程创建、安全问题、生命周期)


线程是程序内部的一条执行路径,是调度和执行的单位,每个线程拥有独立的运行栈和程序计数器。一个进程中的多个线程共享相同的内存单元/地址空间,可以访问相同的变量和对象,这种资源共享使得线程间的通信更加高效,但同时会带来安全隐患。

1.线程的生命周期

线程的生命周期包含五个状态,分别为新建、就绪、运行、阻塞和死亡,线程状态的切换如下图所示:

新建: 当一个Thread类或其子类的对象被声明并创建时,线程处于新建状态;
就绪: 新建的线程调用start()后,便具备了运行的条件,只是没分配到CPU资源;
运行: 就绪的线程被调度并获得CPU资源时,便进入运行状态。
阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态 。
死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束;
新创建的线程最终都会走向死亡的状态。相关方法在本文最后会介绍到。

2.线程的创建

方式一:继承Thread类

  1. 创建一个继承于Thread类的子类;
  2. 重写run()方法,将线程执行的操作写在run()中;
  3. 创建该子类的对象,调用start()方法。
package multithreadingCSDN;

/**
 * @author dbx
 * @create 2020-08-16 10:06
 */
public class ThreadTest {
    public static void main(String[] args) {
        MultiThreading1 mt1 = new MultiThreading1();
        MultiThreading2 mt2 = new MultiThreading2();
        mt1.start();
        mt2.start();
    }
}
class MultiThreading1 extends Thread{
    @Override
    public void run() {
        for (int i = 0;i < 100;i++){
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class MultiThreading2 extends Thread{
    @Override
    public void run() {
        for (int i = 0;i < 100;i++){
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

方式二:实现Runnable接口

  1. 创建Runnable接口的实现类;
  2. 实现Runnable中的抽象方法run(),将线程执行的操作写在run()中;
  3. 创建实现类的对象,并作为参数调用Thread构造器;
  4. 调用start()方法。

相比于继承Thread类的方式,实现Runnable接口的方式具有如下优点:

  1. 没有单继承的局限性;
  2. 更适合处理多个线程共享数据的情况。
package multithreadingCSDN;

/**
 * @author dbx
 * @create 2020-08-17 11:33
 */
public class ThreadTest2 {
    public static void main(String[] args) {
        Thread mt3 = new Thread(new MultiThreading3());
        Thread mt4 = new Thread(new MultiThreading4());
        mt3.start();
        mt4.start();
    }
}
class MultiThreading3 implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i < 100;i++){
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class MultiThreading4 implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i < 100;i++){
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

方式三:实现Callable接口

  1. 创建Callable接口的实现类;
  2. 实现call()方法,将线程执行的操作写在call()中;
  3. 创建实现类的对象,并作为参数调用FutureTask构造器;
  4. 将FutureTask的对象作为参数调用Thread构造器,调用start()方法;
  5. 可查看返回值:futuretask对象.get()。

实现Callable接口的方法是JDK5.0新增的创建多线程方式,相比前两种方式具有如下优点:

  1. 可以有返回值;
  2. 可以抛异常,并被外面的操作捕获;
  3. 支持泛型。
package multithreadingCSDN;

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

/**
 * @author dbx
 * @create 2020-08-17 11:41
 */
public class ThreadTest3 {
    public static void main(String[] args) {
        FutureTask ft5 = new FutureTask(new MultiThreading5());
        FutureTask ft6 = new FutureTask(new MultiThreading6());
        Thread mt5 = new Thread(ft5);
        Thread mt6 = new Thread(ft6);
        mt5.start();
        mt6.start();
        try {
            System.out.println("返回值为:" + ft5.get());//查看返回值
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class MultiThreading5 implements Callable{
    @Override
    public Object call() throws Exception {
        for (int i = 0;i < 100;i++){
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
        return 1;
    }
}
class MultiThreading6 implements Callable{
    @Override
    public Object call() throws Exception {
        for (int i = 0;i < 100;i++){
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
        return null;
    }
}

方式四:使用线程池

  1. 提供指定线程数量的线程池;
  2. 执行指定的线程操作,需提供Runnable、Callable实现类的对象;
  3. 关闭线程池。
package multithreadingCSDN;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 1. @author dbx
 2. @create 2020-08-17 17:23
 */
public class ThreadTest4 {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new MultiThreading7());
        service.submit(new MultiThreading8());
        service.shutdown();
    }
}
class MultiThreading7 implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i < 100;i++){
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class MultiThreading8 implements Callable {
    @Override
    public Object call() throws Exception {
        for (int i = 0;i < 100;i++){
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
        return null;
    }
}

使用线程池的好处有如下三点:

  1. 提高响应速度;(减少了创建新线程的时间)
  2. 降低资源消耗;(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理 。

线程池相关API:
🔺ExecutorService: 线程池接口;
    ●void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable;
    ●< T>Future< T> submit(Callable< T> task):执行任务,有返回值,一般用来执行 Callable;
    ●void shutdown() :关闭连接池 。

🔺Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池;
    ●Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池;
    ●Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池 ;
    ●Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池 ;
    ●Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

3.线程的安全问题

3.1线程安全问题

线程的安全问题出现在多个线程操作共享的数据时,当一个线程还没有执行完对共享数据的操作时,其他线程也开始操作共享数据,就会出现线程安全问题。我们可以看下边的例子。

package exer1;

/**
 * 银行有一个账户。 有两个储户(甲、乙)分别向同一个账户存3000元,
 * 每次存1000,存3次。每次存完打印账户余额。
 * @author dbx
 * @create 2020-07-23 17:24
 */
class Account {
    private int balance = 0;
    public void deposit(int num){
        balance += num;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance);
    }
}

class Custom implements Runnable{
    private Account acct;
    public Custom(Account acct){
        this.acct = acct;
    }
    @Override
    public void run() {
        for (int i = 0;i < 3;i++){
            acct.deposit(1000);
        }

    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account();
        Custom cust = new Custom(acct);
        Thread c1 = new Thread(cust);
        Thread c2 = new Thread(cust);
        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();
    }
}

/*执行结果如下所示:
甲存钱成功,余额为:2000
乙存钱成功,余额为:3000
甲存钱成功,余额为:4000
乙存钱成功,余额为:4000
乙存钱成功,余额为:6000
甲存钱成功,余额为:6000
 */

结果显然是不对的,问题就出在了多线程上,当一个线程执行进行run()方法中,sleep()使该线程被阻塞,此时其他线程进入,并操作共享的数据(账户余额),使得对余额的操作发生了错乱。

3.2解决线程安全问题

解决线程安全问题的思路很简单,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。JAVA中的同步机制解决了线程安全问题。

方式一:同步代码块

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

其中需要被同步的代码即为共享数据的代码。同步监视器俗称为锁,任何一个类都可以充当锁,但要求多个线程共用同一把锁。
在实现Runnable方式中,可以考虑使用this作为锁,在继承Thread类方式中,需要慎用this作锁,可以使用当前类(对象.class)作为锁。具体在使用时,需要特别注意多个线程是否使用同一把锁。

方式二:同步方法

synchronized还可以放在方法声明中,表示整个方法为同步方法。例如将上一个账户存钱例子中的方法声明为同步的:

package exer1;

/**
 * 银行有一个账户。 有两个储户分别向同一个账户存3000元,
 * 每次存1000,存3次。每次存完打印账户余额。
 * @author dbx
 * @create 2020-07-23 17:24
 */
class Account {
    private int balance = 0;
    public synchronized void deposit(int num){
        balance += num;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance);
    }
}

class Custom implements Runnable{
    private Account acct;
    public Custom(Account acct){
        this.acct = acct;
    }
    @Override
    public void run() {
        for (int i = 0;i < 3;i++){
            acct.deposit(1000);
        }

    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account();
        Custom cust = new Custom(acct);
        Thread c1 = new Thread(cust);
        Thread c2 = new Thread(cust);
        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();
    }
}

/*执行结果如下所示:
甲存钱成功,余额为:1000
乙存钱成功,余额为:2000
甲存钱成功,余额为:3000
乙存钱成功,余额为:4000
乙存钱成功,余额为:5000
甲存钱成功,余额为:6000
 */

从执行结果能够看出,使用同步监视器之后,多个线程对共享数据的操作不会出现安全问题了。

方式三:Lock锁

  1. 实例化ReentrantLock;
private final ReentrantLock lock = new ReenTrantLock(); 
  1. 开启Lock锁
lock.lock(); 
  1. 解锁
lock.unlock();  

Lock锁的方式是JDK5.0新增的方式,相比于前两种synchronized方式,Lock方式是显示锁,需要手动开启和关闭;使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,因此开发中优先使用Lock。

4.线程相关的方法

4.1Thread类中相关方法

🔺void start(): 启动线程,并执行对象的run()方法 。
🔺String getName(): 返回线程的名称 。
🔺void setName(String name): 设置该线程名称 。
🔺static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。
🔺static void yield(): 线程让步 。
    ●暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 ;
    ●若队列中没有同优先级的线程,忽略此方法 。
🔺join() : 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止 。
    ●低优先级的线程也可以获得执行 。
🔺static void sleep(long millis): (指定时间:毫秒)
    ●令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。;
    ●抛出InterruptedException异常 。
🔺getPriority() : 返回线程优先值 。
🔺setPriority(int newPriority) : 改变线程的优先级 。

4.2线程通信中相关方法

🔺wait(): 令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续从断点处执行。
🔺notify(): 唤醒正在排队等待同步资源的线程中优先级最高者结束等待 。
🔺notifyAll (): 唤醒正在排队等待资源的所有线程结束等待。
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException异常。调用上述三种方法的必要条件:当前线程必须具有对该对象的监控权(加锁) 。

以上就是关系JAVA多线程相关内容,如果有不正确的地方欢迎大家指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值