Java基础之多线程

在这里插入图片描述


前言

随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。


一、线程简介

多任务:边吃饭边玩手机,同时做多件事
多线程:多车道,多条线路同时执行任务

二、线程实现

1.继承Thread类

不建议使用:为了避免OOP单继承局限性

Thread类本身实现了Runnable接口

实现步骤:

① 自定义线程类继承Thread类

② 重写run方法,编写程序执行体

③ 创建线程对象,调用start()方法启动线程

代码如下(示例):

/**
 * 描述:继承Thread类创建线程
 */
public class Thread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("子线程: " + i);
        }
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        // thread1.run();//先执行run方法,再执行接下来的代码(没起到多线程的作用)
        thread1.start();//主线程和子线程交替执行,且不可控,每次执行结果都不一样
        for (int i = 0; i < 20; i++) {
            System.out.println("主线程: " + i);
        }
    }
}

总结: 线程开启不一定立即执行,由CPU调度执行,直接调用run方法相当于调用普通方法,不会创建新的线程。


2.实现Runnable接口

Java 是单继承,推荐使用Runnable接口,方便同一个独享被多个线程使用

避免了单继承的局限性:即在Java中一个类只能使用extends继承一个父类.,如果继承多个父类,而父类有同名方法时就不知道调用哪一个方法了,另外会是两个类的耦合性增加,如果父类有改动时会直接影响子类。

实现步骤:

① 定义MyRunnable类实现Runnabke接口

② 实现Run()方法,编写程序执行体

③ 创建线程对象,调用start()方法启动线程

代码如下(示例):

/**
 * 描述:实现Runnable接口
 */
public class Thread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("子线程: " + i);
        }
    }

    public static void main(String[] args) {
        //创建Runnable接口实现类的对象
        Thread2 thread2 = new Thread2();
        //创建线程类对象,通过线程对象开启我们的线程(代理)
        new Thread(thread2).start();
        for (int i = 0; i < 20; i++) {
            System.out.println("主线程: " + i);
        }
    }
}

3.实现Callable接口

了解即可

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(t1);
  6. 获取结果: boolean r1 = result1.get()
  7. 关闭服务:ser.shutdownNow();

三、线程状态

1.五种状态

创建、就绪、运行、阻塞、死亡
在这里插入图片描述


2.线程停止(标志位)

不推荐使用jdk提供的stop(),destory()方法,建议使用一个标志位进行终止变量,当 flag = false,则线程终止运行。

死亡之后的线程不能再次启动

代码如下(示例):

/**
 * 描述:停止线程
 */
public class ThreadStop implements Runnable {
    //1.线程中定义线程体使用的标识
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        //2.线程体使用该标识
        while (flag){
            System.out.println("run.....Thread"+(i++));
        }
    }

    //3.对外提供方法改变标识
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        ThreadStop threadStop = new ThreadStop();
        new Thread(threadStop).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程: "+i);
            if ( i == 900){ // 900时就停止子线程
                threadStop.stop();
                System.out.println("run线程停止了!");
            }
        }
    }
}

3.线程休眠(sleep)

  • sleep(时间) 指定当前线程阻塞的毫秒数
  • sleep时间达到后线程进入就绪状态
  • 每一个对象都有一个锁,sleep不会释放锁
  • sleep可以模拟网络延时(放大问题的发生性,比如多线程卖票,一票多卖),倒计时等

代码如下(示例):

/**
 * 描述:线程休眠
 */
public class ThreadSleep implements Runnable {
    @Override
    public void run() {

    }

    //模拟打印当前时间
    public static void main(String[] args) throws InterruptedException {
		testDown();
        //当前系统时间
        Date date = new Date(System.currentTimeMillis());
        while (true){
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)); // 格式化输出
            date = new Date(System.currentTimeMillis()); // 更新当前系统时间
            Thread.sleep(1000);
        }
    }

    //模拟倒计时
    public static void testDown() throws InterruptedException {
        int num = 10;
        while (true){
            Thread.sleep(1000); // 1秒,毫秒单位
            System.out.println(num--);
            if (num <= 0){
                break;
            }
        }
    }
}

4.线程礼让(yield)

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,礼让不一定成功

代码如下(示例):

/**
 * 描述:线程礼让
 */
public class ThreadYield implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程结束执行");
    }

    public static void main(String[] args) {
        ThreadYield threadYield = new ThreadYield();
        new Thread(threadYield,"a").start(); // 线程a
        new Thread(threadYield,"b").start(); // 线程b
    }
}

总结: 即使不进行礼让,也会出现礼让成功,因为是多线程,cpu调度来决定。


5.线程强制执行(join)

join合并线程,待此线程执行完毕之后,在执行其他线程,其他线程阻塞,可以想象成插队。

注意:join是通过 new Thread 对象来调用的,而sleep和yield是通过 Thread 直接调用。

代码如下(示例):

/**
 * 描述:线程强制执行
 */
public class ThreadJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("join线程:" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadJoin threadJoin = new ThreadJoin();
        Thread thread=  new Thread( threadJoin);
        thread.start();

        for (int i = 0; i < 50; i++) {
            if( i == 25 ) {
                thread.join(); // 插队,让join线程先执行完后到主线程
            }
            System.out.println("主线程"+i);
        }
    }
}

四、线程优先级

线程优先级高不一定优先执行,大多数时候,线程优先级高的线程会优先执行,先设置优先级,再start线程!

java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

线程的优先级用数字表示,范围从1~10。

  • Thread.MIN_PRIORITY = 1;
  • Thread.MAX_PRIORITY = 10;
  • Thread.NORM_PRIORITY = 5;

代码如下(示例):

/**
 * 描述:线程优先级
 */
public class ThreadPriority {
    public static void main(String[] args) {
        //主线程设置默认优先级
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority,"t1");
        Thread t2 = new Thread(myPriority,"t2");
        Thread t3 = new Thread(myPriority,"t3");
        Thread t4 = new Thread(myPriority,"t4");
        Thread t5 = new Thread(myPriority,"t5");

        //先设置线程优先级,再启动
        t1.setPriority(1);
        t1.start();
        t2.setPriority(3);
        t2.start();
        t3.setPriority(6);
        t3.start();
        t4.setPriority(Thread.MAX_PRIORITY);    // 优先级=10
        t4.start();
        t5.setPriority(Thread.MIN_PRIORITY);    // 优先级=1
        t5.start();
    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---线程被执行了!---" + Thread.currentThread().getPriority());
    }
}

五、守护线程(daemon)

线程分为用户线程和守护线程

守护线程在用户线程执行完毕之后也会执行完毕,不过JVM需要一点时间

应用场景:后台记录操作日志,监控内存,垃圾回收等

首先:守护线程先执行,然后用户线程执行与守护线程一起执行,用户线程执行完毕后,守护线程执行完毕。

代码如下(示例):

/**
 * 描述:守护线程
 */
public class ThreadDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread thread = new Thread(god);
        thread.setDaemon(true); // 默认为false用户线程,true守护线程
        thread.start();
        new Thread(you).start();
    }
}

class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("上帝守护你");
        }
    }
}

class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("开心着活着每一天------");
        }
        System.out.println("----goodbye!Beautiful World!!!------");
    }
}

六、线程同步

解决多个线程操作同一个对象的安全问题。

线程同步其实就是一种等待机制。多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。

线程同步形成条件:队列+锁

锁的对象就是多个线程共享的对象,锁谁取决于共同操作的对象是谁(且这个对象是要改变的)

只读代码是不需要加锁的,只有需要进行修改的代码才需要加锁(所以产生了同步方法和同步代码块)

  • 同步方法: public synchronized void method(int args)f
  • 同步块: synchronized(Obj) {} 注意:监视的对象是需要增删改查的对象

线程同步案例

1.线程安全买票

如果没有同步,剩一张票的时候,3个人都可以买到,会出现负数,线程不安全。

代码如下(示例):

/**
 * 描述:线程安全买票
 */
public class BuyTicket implements Runnable {

    // 标志位
    private boolean flag = true;

    // 票数
    private int ticketNums = 10;

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

    // 买票
    // synchronized同步方法,锁的是this
    private synchronized void buy(){
        if (ticketNums <= 0){
            flag = false;
            return;
        }
        // 模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 买票
        System.out.println(Thread.currentThread().getName() + "拿到"+ ticketNums--);
    }

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

}

2.银行取钱

因为synchronized默认所得独享是this,那么这里就是锁的银行,但是我们操作是对account进行操作的,银行是没有变的,所以我们需要synchronized同步块,锁account。

代码如下(示例):

/**
 * 描述:银行取钱
 */
public class BankDraw {
    public static void main(String[] args) {
        //取钱首先得有账户
        Account account = new Account(100,"结婚基金");
        DrawMoney you = new DrawMoney(account,50,"张三");
        DrawMoney girl = new DrawMoney(account,100,"李四");
        you.start();
        girl.start();
    }
}

class Account{
    int money;   // 账户余额
    String name; // 卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

class DrawMoney extends Thread{
    Account account;    // 账户
    int drawingMoney;   // 取多少钱
    int nowMoney;       // 现在有多少钱

    public DrawMoney(Account account, int drawingMoney, String name) {
        super(name);    // 调用父类的方法
        this.account = account;
        this.drawingMoney = drawingMoney;
//        this.nowMoney = nowMoney;
    }

    @Override
    public void run() {
        synchronized (account){ // 锁账户
            // 判断账户有没有钱
            if (account.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
                return;
            }
            // 模拟延时
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 卡内余额
            account.money = account.money - drawingMoney;
            // 现在手里的钱
            nowMoney = nowMoney + drawingMoney;
            System.out.println(account.name+"余额为:"+account.money);
            // this就是调用当前方法的对象,Drawing继承了Thread,所以this也是一个线程对象,可以调用Thread的getName方法来获取线程的名字
            // Thread.currentThread().getName()=this.getName()
            System.out.println(this.getName()+"手里的钱"+nowMoney);
        }
    }
}

3.线程安全集合

如果没有锁住集合两个线程同一瞬间操作了同一个位置,导致覆盖,元素变少。

代码如下(示例):

/**
 * 描述:线程集合
 */
public class Gather {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() ->{ // lamda 表达式
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        // 让主线程休眠一会再打印,电脑运行速度太快了。线程里add还没执行完就执行主线程的打印语句。会导致打印结果偏小。
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

4.JUCC集合

JUCC是JAVA提供的线程安全集合。

代码如下(示例):

/**
 * 描述:线程安全的集合
 */
public class SafeGather {
    public static void main(String[] args) {
        CopyOnWriteArrayList list = new CopyOnWriteArrayList();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

七、死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

应用场景: 相当于两台并行前进的车,左边的车想变道右边,右边的车想变道左边,谁也不让谁。

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

代码如下(示例):

/**
 * 描述:死锁
 */
public class DeadLock {
    public static String obj1 = "A对象";
    public static String obj2 = "B对象";

    public static void main(String[] args) {
        Thread a = new Thread(new Thread1());
        Thread b = new Thread(new Thread2());
        a.start();
        b.start();
    }
}

class Thread1 implements Runnable{
    @Override
    public void run() {
        try {
            System.out.println("线程1 Running");
            while (true){
                synchronized (DeadLock.obj1){
                    System.out.println("线程1 锁定 A对象");
                    Thread.sleep(3000); // 获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
                    synchronized (DeadLock.obj2){
                        System.out.println("线程1 锁定 B对象");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Thread2 implements Runnable{
    @Override
    public void run() {
        try {
            System.out.println("线程2 Running");
            while (true){
                synchronized (DeadLock.obj2){
                    System.out.println("线程2 锁定 B对象");
                    Thread.sleep(3000); // 获取obj2后先等一会儿,让Lock1有足够的时间锁住obj1
                    synchronized (DeadLock.obj1){
                        System.out.println("线程2 锁定 A对象");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

简单来说:

  1. 线程1和线程2的执行逻辑中都需要锁定对象A和对象B。
  2. 线程1在执行中先锁定了A对象。
  3. 线程2在执行中锁定了B对象。
  4. 线程1需要锁定B对象时发现B对象已经被其他线程锁住,所以线程1需要等待B对象锁释放后继续执行。
  5. 线程2需要锁定A对象时发现A对象已经被其他线程锁住,所以线程2需要等待A对象锁释放后继续执行。

总结: 线程1和线程2都在相互等待,它们都将无法推进下去。


八、Lock锁

ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 与 Synchronized 对比:

  • Lock是显式锁(手动开启和关闭,别忘记关闭锁)Synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,Synchronized有代码块锁和方法锁

代码如下(示例):

/**
 * 描述:ReentrantLock可重入锁
 */
public class TestLock implements Runnable {

    int ticketNums = 10;

    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {
                // 上锁
                lock.lock();
                if (ticketNums > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"获得了第"+(ticketNums--)+"票");
                }else {
                    break;
                }
            } finally {
                // 释放解锁
                lock.unlock();
            }
        }
    }

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

九、线程通信

方法:管程法信号灯法


十、线程池

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

思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处: 提高响应速度(减少了创建新线程的时间)、降低资源消耗、便于线程管理

  • ExecutorService:真正的线程池接口
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

代码如下(示例):

/**
 * 描述:线程池
 */
public class ThreadPool {
    public static void main(String[] args) {
        //1、创建服务,创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10); //newFixedThreadPool 参数为:线程池大小
        //2、执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //3、关闭连接
        service.shutdown();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"天天向上");
    }
}

总结

好了Java多线程基础先到这里了,记得点个赞哦!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值