Java线程

概念

  • 什么是进程?什么是线程?
    • 进程是一个应用程序。
    • 线程是一个进程中的执行场景/执行单元。
    • 一个进程可以启动多个线程。
    • image.png
  • 注意:进程之间内存独立资源不共享。
    • 线程之间(在Java语言中)
      • 堆内存和方法区内存共享。
      • 但是栈内存独立,一个线程一个栈,每个栈之间互不干扰,各自执行各自的,这就是多线程并发。
      • Java中之所以有多线程机制,目的就是为了提高程序的处理效率。
  • 思考:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
    • 不能,对于单核的CPU来说,在某一个时间节点上实际只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人类的感觉“多个线程同时在工作”。

线程的实现

第一种方式:

  • 编写一个类,直接继承java.lang.Thread,重写run方法
    • 怎么创建线程对象 ==> new
    • 怎么启动线程 ==> 调用start()方法
// 创建一个可运行类
public class MyRunnable extends Thread {

    @Override
    public void run() {

    }
}
// 创建线程对象
Thread t = new MyRunnable();
// 启动线程
t.start();
  • start()的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了
  • 启动成功的线程会自动调用run()方法,并且run()方法位于栈底部(压栈)。
  • run方法在分支栈的底部,main方法在主栈的底部,run和main是平级的。
  • run方法与start方法区别:run方法并不会开辟分支栈
    • image.png
    • image.png

第二种方式:

  • 编写一个类实现java.lang.Runnable接口。
// 创建一个可运行类
public class MyRunnable implements Runnable {

    @Override
    public void run() {

    }
}
// 创建线程对象
Thread t = new Thread(new myRunnable());
// 启动线程
t.start();

建议使用:实现接口方式,面向接口编程;继承有局限性,只能继承一个类。

线程的生命周期

新建状态、就绪状态、运行状态、阻塞状态、死亡状态
image.png

实现线程的第三种方式

实现Callable 接口,JDK8新特性,这种方式实现的线程可以获取线程的返回值,之前讲解的两种线程无法获取返回值的,以为run方法返回viod ,此方式效率低(获取结果可能线程会阻塞)。

实现方式:

  1. 创建一个“未来对象类”对象,参数为 Callable 接口的实现类
  2. 重写call方法 相当于 run方法

注意:

  1. 获取线程结果 未来任务类对象.get()
  2. get方法的执行可能会造成“当前线程阻塞”,因为其拿另一个线程的结果,可能需要等待很久。

代码实现

public static void main(String[] args) throws ExecutionException, InterruptedException {


    // 创建“未来任务类”对象
    FutureTask task = new FutureTask(new Callable() {
        @Override
        public Object call() throws Exception {
            // 模拟执行线程任务
            System.out.println("call method begin");
            Thread.sleep(1000 * 5);
            System.out.println("call method over");


            int a = 100;
            int b = 200;
            return a + b;
        }
    });

    // 创建线程对象
    Thread thread = new Thread(task);

    thread.start();

    // 获取线程执行结果
    Object o = task.get();

    System.out.println(o);
}

线程常用方法

  • 获取当前线程对象
    • static Thread.currentThread()
  • 获取/创建线程名称
    • 线程对象.get/setName()
  • 让当前线程休眠,进入“阻塞状态”,放弃占有时间片,让给其他线程使用
    • static void sleep(long millis)
    • 使用场景:间隔特定的时间,去执行一段特定的代码。每隔多久就执行一次。
  • 终止线程睡眠
    • 线程对象.interrupt()
  • 终止线程
    • 线程对象.stop() 此方法已过时,建议使用 boolean 标记进行终止。

线程调度

  • 抢占式调度模型(Java采用的模式):哪个线程的优先级高,抢到的CPU时间片的概率高一些/多一些。
  • 均分式调度模型:平均分配CPU时间片,每个线程占有的时间片时间长度相等。一切平等。
  • 实例方法(设置/获取线程优先级)【级别1-10】
    • void setPriority(int newPriority)
    • int getPriority()
    • 默认级别为5
  • 静态方法(让位方法,让给其他线程使用)
    • static void yield()
    • 使得该线程从运行状态转换到就绪状态
  • 实例方法(合并线程)
    • void join()
    • 合并到当前线程中,当前线程会阻塞

线程安全

注意:在实际开发中,我们的项目运行在服务器中,而服务器已经将线程的定义、创建、启动都已经实现完了,这些代码不需要我们编写。我们要关注的是这些数据在多线程并发的环境下是否安全。
什么时候会发生安全问题:

  1. 多线程并发
  2. 有共享数据
  3. 共享数据有修改行为

image.png

同步和异步

异步编程模型:

  • 线程t1和线程t2,各自执行各自的,互不相干,谁也不需要等谁,其实就是多线程并发(效率较高)。

同步编程模型

  • 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行完。两个线程之间发生等待关系,其实就是线程排队执行(效率较低)。

Java中哪些变量会产生线程安全问题

只有实例变量、静态变量会产生,而局部变量不会。

  • 实例变量:存储在堆内存(数据共享)。
  • 静态变量:存储在方法区(数据共享)。
  • 局部变量:存储在栈内存(数据不共享)。

只有共享数据才可能出现线程安全问题。

如何解决线程安全问题

  • 线程排队执行(不能并发),这种机制被称为“线程同步机制”。
  • 线程同步就是线程排队,线程排队就会牺牲一些效率,安全第一位,只有数据安全的情况下,才可以谈效率。

详解如下

public class Account {

    private String accountName;
    private double balance;

    public Account(String accountName, double balance) {
        this.accountName = accountName;
        this.balance = balance;
    }

    public Account() {

    }

    public String getAccountName() {
        return accountName;
    }

    public void setAccountName(String accountName) {
        this.accountName = accountName;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }


    public  void withdraw(double money) {

        synchronized (this) {
            // 获取当前余额
            double balance = this.getBalance();
            // 剩余余额
            balance -= money;
            // 模拟网络延迟,出现线程安全问题
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 更新余额
            this.setBalance(balance);
        }
    }
}


public class AccountThread extends Thread{


    // 两个线程共享一个账户
    private Account account;

    // 通过构造方法传递过来构造对象
    public AccountThread(Account account) {
        this.account = account;
    }


    @Override
    public void run() {
        // 执行取款操作
        account.withdraw(5000);

        System.out.println(Thread.currentThread().getName()+"对"+account.getAccountName()+"取款成功,余额为"+account.getBalance());
    }
}

public class AccountTest {

    public static void main(String[] args) {

        // 创建账户
        Account account = new Account("act-001", 10000);
        // 创建两个线程
        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);

        // 设置name
        t1.setName("t1");
        t2.setName("t2");

        //启动线程取款
        t1.start();
        t2.start();
    }
}

保证线程同步方式
使用 synchronized (){} 同步代码块,同步代码块的范围越小效率越高。
注意1:小括号() 中传入的数据是相当关键的,这个数据必须是多线程共享数据,才能达到线程排队。
注意2:在实例方法上使用synchronized,此时的锁一定是this,不能是其他对象,这种方式具有局限性,导致整个方法体都需要同步,程序的执行效率降低。
执行原理:
在Java语言中,任何一个对象都有“一把锁”,其实这个锁就是标记(只是它叫做锁)(100个对象100把锁)。

  1. 假设处t1t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
  2. 假设t1先执行了,遇到了synchronized,这个时候自动找后面“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放
  3. 假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被1占有,t2只能在同步代码块外面等待1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
  4. 这样就达到了线程排队执行。这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。|

image.png

死锁

图解
image.png
线程t1与线程t2都在等待锁的释放,造成死锁现象

public class Test {

    public static void main(String[] args) {


        Object o1 = new Object();
        Object o2 = new Object();

        Object1 t1 = new Object1(o1, o2);
        Object2 t2 = new Object2(o1, o2);

        t1.start();
        t2.start();


    }
}


class Object1 extends Thread {
    Object o1;
    Object o2;

    public Object1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o1) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2) {

                System.out.println("666");
            }
        }
    }
}

class Object2 extends Thread {
    Object o1;
    Object o2;

    public Object2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o2) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1) {

                System.out.println("999");
            }
        }
    }

}

注意:实际开发中,应避免synchronized(){} 同步代码块的嵌套,

死锁的必要条件:

1).互斥使用
线程1拿到了锁,线程2就只能阻塞等待,无法使用锁.
2).不可抢占
线程1拿到锁后,线程2不能强夺锁,只能阻塞等待,等待线程1释放锁
3).请求和保持
线程1拿到锁A后,再尝试获取锁B,此时锁A还是保持的(并不会因为要获取锁B就释放锁A)
4).循环等待
线程1尝试获取锁A和锁B 线程2尝试获取锁B和锁A
线程1在获取锁B的时候等待线程2释放锁B,线程2在获取锁A的时候等待线程1释放锁A

如何解除死锁

在死锁的四个必要条件中,前三个都是锁的基本特性,我们无法更改,所以说破除死锁的突破口就是:循环等待
如何破除循环等待呢?
最简单的办法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除.

实际开发解决安全问题

开发第一步,就是考虑使用synchronized同步代码块吗?
答案:不是,使用同步代码块,会使程序执行效率降低,用户体验不好。系统的吞吐量降低。在不得已的情况下才会使用。
解决方案一:
尽量使用局部变量代替“实例变量”和“静态变量”。
解决方案二:
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

守护线程

Java语言中线程分为两大类

一类是:用户线程
一类是:守护线程(后台线程),最有代表性之一就是垃圾回收线程。

守护线程的特点

一般守护线程是一个死循环,所有的用户线程只要结束,守护线程就会自动结束。

注意: 主线程main方法是一个用户线程。
守护线程创建方法:
线程对象.setDaemon(true);

定时器

作用:间隔特定的时间,执行特定的程序。

实现方式

  1. 可以使用sleep方法,这种是最原始的方法,很少用。
  2. 在Java类库中已有定时器,java.util.Timer ,可以直接使用,不过在在实际开发中很少用,因为有很多高级框架都是支持定时任务的。
  3. 在实际开发中,目前使用较多的是spring框架所提供的SpringTask框架,只需要进行简单的配置,就可以完成定时器任务。
public class Test {

    public static void main(String[] args) throws ParseException {
        // 创建定时器对象
        Timer timer = new Timer();
        // 指定定时任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2023-3-20 10:20:30");

        timer.schedule(new timeTask(), firstTime, 1000 * 2);

    }
}


class timeTask extends TimerTask {

    @Override
    public void run() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":完成了一次数据备份");
    }
}

关于object类中的wait和notify方法。(生产者和消费者模式!)

wait 和 notify 方法不是线程对象的方法,是Object类中自带的,是Java中任何一个对象都有的方法。

Object o = new Object; o.wait(); o.notify();
wait()方法作用:

  • o.wait() 让正在o对象上活动的线程t进入等待状态,并释放掉t线程之前占有的o对象的锁。
  • o.notify() 让正在o对象上等待的线程唤醒,只是通知,不会释放锁。

生产者和消费者模式:

image.png
模拟一个需求:

  1. 仓库我们采用List集合
  2. List集合中假设最多只能存储一个元素
  3. 保持 List仓库中元素个数在 0-1 保持动态平衡

代码实现:

/**
 * 消费线程
 */
class Consume implements Runnable {

    /**
     * 共享仓库 List
     */
    private List list;

    public Consume(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直消费
        while (true) {
            synchronized (list) {
                if (list.size() == 0) {
                    // 此时仓库为空,不可消费
                    try {
                        // 暂停消费线程,释放锁,一直等待
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 执行到此处,可消费
                Object o = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + o);
                // 唤醒生产线程
                list.notifyAll();
            }
        }
    }
}


class Product implements Runnable {

    /**
     * 共享仓库 List
     */
    private List list;

    public Product(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直生产
        while (true) {
            synchronized (list) {
                if (list.size() > 0) {
                    // 此时仓库为满,不可生产
                    try {
                        // 暂停生产线程,并释放锁,一直等待,
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 执行到此处,可生产
                Object o = new Object();
                list.add(o);
                System.out.println(Thread.currentThread().getName() + "--->" + o);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}

image.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值