通俗易懂带你了解Java多线程处理

何夜息随笔录-多线程的使用

进程和线程

首先需要分清进程和线程,进程是系统分配的单位,就是任务管理器可以看到的进程,这是由系统自动分配和管理的,每个进程都有进程PID

线程就是程序的一块逻辑,这是我们通过代码去创建的,我们可以操作多个进程,程序运行的时候就会吧线程自动放到进程中去执行。

一个进程可以包括多个线程!线程是CPU执行和调度的单位。

线程的创建

首先是使用继承Thread类,然后重写run方法,然后调用start方法执行线程。

然后看源码可以发现,这个被继承的Thread类,其实是实现了Runnable接口,然后接口里有一个run方法,所以第二种我们就可以直接实现

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8fIcw3d6-1631801467817)(https://www.heyexi.com/markdown/clipboard-1619094281173.png)]

public class ThreadTest extends Thread
{
    @Override
    public void run()
    {
        System.out.println("这是线程里的方法");
    }

    public static void main(String[] args)
    {
        System.out.println("这是主线程里的方法");

        ThreadTest threadTest = new ThreadTest();
        threadTest.start();//调用start方法,不是run方法
    }
}


输出内容:
这是主线程里的方法
这是线程里的方法

注意,并不是先执行main方法里面的,再执行新建的线程,只是因为运行过快才会看到先执行了主线程里的输出,如果时间久的话这两个线程是会相互交叉的,因为线程是根据CPU的资源进行自由调度的。

当然,最通用的方法就是实现Runnable接口,重写里面的run方法,

然后呢,我们需要把这个实现了Runnable接口的类,作为Thread对象的构造参数,还是需要用Thread来启动线程!

public class ThreadTest2 implements Runnable
{
    @Override
    public void run()
    {
        while (true)
            System.out.println("这里是线程内容");
    }
    public static void main(String[] args)
    {
        new Thread(new ThreadTest2()).start();
        System.out.println("这是主线程里的方法");
    }
}

img

线程的状态:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lO63U3PE-1631801467823)(https://www.heyexi.com/markdown/clipboard-1619094322984.png)]

线程的停止

线程创建后,我们需要根据具体的条件今天停止线程,官方不建议使用stop等方法来强制停止,因为这个不安全。

常规做法是,自定义个停止线程的方法,用一个全局的布尔值的标识符来停止线程,当为假时就停止线程,这个条件由主方法进行控制。

package com.heyexi;

public class StopThread implements Runnable
{

    private boolean flag = true;

    @Override
    public void run()
    {
        while (flag)
            System.out.println("线程正在运行....");
    }

    //线程停止方法
    public void stop()
    {
        this.flag = false;
        System.out.println("线程已经停止");
    }

    public static void main(String[] args)
    {
        StopThread stopThread = new StopThread();

        new Thread(stopThread).start();

        for (int i = 0; i < 1000; i++)
        {
            System.out.println("i="+i);
            if(i==456)//停止线程条件
                stopThread.stop();
        }
    }
}


输出:
i=452
i=453
i=454
i=455
i=456
线程已经停止
i=457
i=458
线程休眠

有时候为了防止线程资源在段时间内被某个线程抢完,我们需要禁止暂停,包括进行延迟等等,这时候我们需要调用sleep静态方法。

以下实现每秒输出一下当前时间。

package com.heyexi;

import java.text.SimpleDateFormat;
import java.util.Date;

public class SleepThread
{
    public static void main(String[] args) throws InterruptedException
    {
        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

        while (true)
        {
            String time = dateFormat.format(new Date().getTime());
            System.out.println("当前时间:"+time);
            Thread.sleep(1000);
        }
    }
}


当前时间:22:27:38
当前时间:22:27:39
当前时间:22:27:40
当前时间:22:27:41
当前时间:22:27:42
线程礼让

线程礼让(yield)是一个线程把CPU的资源拿出来,给另一个线程使用,但是线程的调度是CPU自由调度的,所以虽然某个线程礼让,但是不一定礼让成功,也就是CPU没有掉其他线程,还是调用了礼让的那个线程。

Thread.currentThread().getName()是获取当前线程的名字
public class test
{

    public static void main(String[] args)
    {
        yeild y = new yeild();

        new Thread(y,"AAA").start();
        new Thread(y,"BBB").start();
    }
}

class yeild implements Runnable{
    @Override
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
            System.out.println(Thread.currentThread().getName()+"开始执行线程!");
        }
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName()+"线程结束!");
    }
}

输出
BBB开始执行线程!
AAA开始执行线程!
AAA开始执行线程!
BBB线程结束!
AAA线程结束!

但是线程一旦启动都是CPU来调度的,这个例子是CPU资源足够,所以礼不礼让都应该B线程能执行,礼让应该是在资源不够使用才需要使用。

强制执行线程join

这是一个比较强制的措施,就是多线程一般都是交叉执行的,那有时候我们需要有一个事务的过程,就是有先后顺序,比如必须是取款机密码输入正确它才能执行取钱的进程。

所以我们可以考试使用join,也就是等输入正确密码这个线程执行完后才能执行取款线程。

请看如下代码:

package com.sfc;

public class test
{

    public static void main(String[] args)
    {
        Thread thread1 = new Thread(new InputPwdThread());
        Thread thread2 = new Thread(new GetMoney());
        thread1.start();
        thread2.start();
    }
}

class  InputPwdThread implements Runnable{
    @Override
    public void run()
    {
        for (int i = 0; i < 5; i++)
        {
            System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");
        }
        System.out.println("密码输入成功!");
    }
}
class GetMoney implements Runnable{
    @Override
    public void run()
    {
        System.out.println(Thread.currentThread().getId()+"取钱成功!");
    }
}

输出
15取钱成功!
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!

可以看到这不是我们想要的结果

所以我们需要先让输入密码的线程执行完

package com.sfc;

public class test
{

    public static void main(String[] args) throws InterruptedException
    {
        Thread thread1 = new Thread(new InputPwdThread());
        Thread thread2 = new Thread(new GetMoney());
        thread1.start();
        thread1.join();//需要写在线程2执行之前
        thread2.start();
    }
}

class  InputPwdThread implements Runnable{
    @Override
    public void run()
    {
        for (int i = 0; i < 5; i++)
        {
            System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");
        }
        System.out.println("密码输入成功!");
    }
}
class GetMoney implements Runnable{

    @Override
    public void run()
    {
        System.out.println(Thread.currentThread().getId()+"取钱成功!");
    }
}
输出
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
15取钱成功!
线程的状态

线程有六种状态,可以通过Thread.getState();获取得到状态

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  • 阻塞(BLOCKED):表示线程阻塞于锁。

  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  • 终止(TERMINATED):表示该线程已经执行完毕。

线程的优先级

线程有优先级,范围是1-10,如果我们不知道优先级,默认就是5,我们如果想让某个线程先执行,可以设置它的优先级,通过setPriority(),方法来实现。

注意:需要先设置优先级,再执行start方法。

public class test
{

    public static void main(String[] args) throws InterruptedException
    {
        Thread thread1 = new Thread(new InputPwdThread());
        Thread thread2 = new Thread(new GetMoney());

        thread2.setPriority(8);
        thread1.setPriority(2);

        thread1.start();
        thread2.start();
    }
}

class  InputPwdThread implements Runnable{
    @Override
    public void run()
    {
        for (int i = 0; i < 5; i++)
        {
            System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");
        }
        System.out.println("密码输入成功!");
    }
}
class GetMoney implements Runnable{

    @Override
    public void run()
    {
        System.out.println(Thread.currentThread().getId()+"取钱成功!");
    }
}

输出
15取钱成功!
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!

但这优先级也不是绝对,CPU也可能先调用优先级低的,只是大多时是先调用优先级高的。

用户线程和守护线程

线程可以分为用户线程和守护线程,这两个有什么区别呢?

JVM需要确保用户线程被执行完,比如我们自定义的线程和主线程,但是守护线程是不用等待执行完的,比如垃圾回收线程,只要所有用户线程执行完了,就算守护线程没有执行完,程序也会结束。

那么默认的线程是用户线程,我们可以通过setDaemon()方法为true,就变成了守护线程。

package com.sfc;

public class test
{

    public static void main(String[] args) throws InterruptedException
    {
        Thread thread1 = new Thread(new InputPwdThread());
        Thread thread2 = new Thread(new GetMoney());

        thread2.setDaemon(true);

        thread1.start();
        thread2.start();
    }
}

class  InputPwdThread implements Runnable{
    @Override
    public void run()
    {
        for (int i = 0; i < 3; i++)
        {
            System.out.println(Thread.currentThread().getId()+"我是用户线程");
        }
    }
}
class GetMoney implements Runnable{

    @Override
    public void run()
    {
        while (true)
        {
            System.out.println("我是守护线程");
        }
    }
}
输出
我是守护线程
我是守护线程
我是守护线程

进程已结束,退出代码 0

可以看到,在用户线程结束后,守护线程还运行了一会,没有立刻停止,不过也停止了。

并发

什么是并呢?很简单,就是用一个对象或者资源,同时被多个线程操作。

比如学校的选修课,一个选修课同时被多个学生去请求选课,就会造成并发,每个学生的手机都创建了一个用户进程,然后都去请求修改同一个选修课的名额,就造成了并发。

那如何解决并发问题呢?

这时候我们需要使用线程同步,也就是队列+锁。

队列就是我们把学生请求的进程作为多个队列,按队列一个一个来操作。

然后就是锁,锁就是当某个线程获得了资源,那么就需要保证该线程独占资源,其他线程都不能访问,只有当这个进程访问完资源后其他线程才能访问该资源。、

我们首先来模拟一下没有处理过的并发:

import lombok.SneakyThrows;
public class test
{
    public static void main(String[] args)
    {
        GetCourse getCourse = new GetCourse();

        new Thread(getCourse, "何夜息").start();//同学1抢票
        new Thread(getCourse, "张三").start();
        new Thread(getCourse, "李四").start();
        new Thread(getCourse, "王五").start();
    }
}

//模拟学生抢票
class GetCourse implements Runnable{

    private int courseNum = 10;//课程名额
    private boolean flag = true;//外部停止方式

    @SneakyThrows
    @Override
    public void run()
    {
        while (flag)
        {
            getCourse();
        }
    }

    //抢课
    private void getCourse() throws InterruptedException
    {
        if(courseNum<=0)//没有名额了
        {
            this.flag = false;
        }
        Thread.sleep(200); //暂停一会,否则被用同一个进程抢完了,模拟延时
        //名额减一
        courseNum--;
        System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
    }
}

输出
李四拿到了票,剩余:9
张三拿到了票,剩余:9
何夜息拿到了票,剩余:9
王五拿到了票,剩余:8
李四拿到了票,剩余:7
何夜息拿到了票,剩余:6
张三拿到了票,剩余:7
王五拿到了票,剩余:5
李四拿到了票,剩余:3
张三拿到了票,剩余:3
何夜息拿到了票,剩余:3
王五拿到了票,剩余:2
张三拿到了票,剩余:-1
李四拿到了票,剩余:-1
何夜息拿到了票,剩余:-1
王五拿到了票,剩余:-2
张三拿到了票,剩余:-3

进程已结束,退出代码 0

可以看到,前三个人都拿到了票,但是票还剩余9张,这就不合理了。还有就是没有票了,但是进程太快,还是拿到了,这也是不合理的,如果是前优惠券或者红包之类的,那这就很危险了。

如何解决呢?我们可以使用同步方法,就是让对象排队去修改,只需要在方法前加上synchronize关键字,就声明该方法为同步方法。

package com.sfc;

import lombok.SneakyThrows;

public class test
{

    public static void main(String[] args)
    {
        GetCourse getCourse = new GetCourse();

        new Thread(getCourse, "何夜息").start();//同学1抢票
        new Thread(getCourse, "张三").start();
        new Thread(getCourse, "李四").start();
        new Thread(getCourse, "王五").start();
    }
}

//模拟学生抢票
class GetCourse implements Runnable{

    private int courseNum = 10;//课程名额
    private boolean flag = true;//外部停止方式

    @SneakyThrows
    @Override
    public void run()
    {
        while (flag)
        {
            getCourse();
        }
    }

    //抢课
    private synchronized void getCourse() throws InterruptedException
    {
        if(courseNum<=0)//没有名额了
        {
            this.flag = false;
            return;
        }
        Thread.sleep(200); //暂停一会,否则被用同一个进程抢完了,模拟延时
        //名额减一
        courseNum--;
        System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
    }
}

输出
何夜息拿到了票,剩余:9
何夜息拿到了票,剩余:8
何夜息拿到了票,剩余:7
何夜息拿到了票,剩余:6
何夜息拿到了票,剩余:5
何夜息拿到了票,剩余:4
何夜息拿到了票,剩余:3
何夜息拿到了票,剩余:2
何夜息拿到了票,剩余:1
何夜息拿到了票,剩余:0
进程已结束,退出代码 0

可以看到,确实是依次拿了,没有在抢票。

这里就有重点了,这个synchronized同步方法,锁的是this它本身的类,也就是这个方法所属的类,这里就是GetCourse这个类,那其他类来执行时就锁不住了。

如果是其他对象来操作这个方法,我们就可以使用synchronized(cls){}代码块。

也就是我们指定这个对象修改完了,其他对象才能访问。

注意,加了同步修饰,效率就会变低,因为我们只能等前面的对象用完了后面的对象才能使用,所以同步代码块我们一般只需要加在对修改的时候使用,对读取肯定不需要加。

package com.sfc;

import lombok.SneakyThrows;

public class test
{

    public static void main(String[] args)
    {
        GetCourse getCourse = new GetCourse();

        new Thread(getCourse, "何夜息").start();//同学1抢票
        new Thread(getCourse, "张三").start();
        new Thread(getCourse, "李四").start();
        new Thread(getCourse, "王五").start();
    }
}

//模拟学生抢票
class GetCourse implements Runnable{

    private int courseNum = 10;//课程名额
    private boolean flag = true;//外部停止方式

    @SneakyThrows
    @Override
    public  void run()
    {
        while (flag)
        {
            getCourse();
        }
    }

    //抢课
    private void getCourse() throws InterruptedException
    {
        synchronized (this)
        {
            if(courseNum<=0)//没有名额了
            {
                this.flag = false;
                return;
            }
            Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时
            //名额减一
            courseNum--;
            System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
        }

    }
}
输出
李四拿到了票,剩余:9
李四拿到了票,剩余:8
李四拿到了票,剩余:7
李四拿到了票,剩余:6
李四拿到了票,剩余:5
李四拿到了票,剩余:4
李四拿到了票,剩余:3
李四拿到了票,剩余:2
李四拿到了票,剩余:1
李四拿到了票,剩余:0

进程已结束,退出代码 0

这里我们指定对象为this,因为这个方法是在该对象的run方法中调用的,所以同步代码块和同步方法都解决了并发的问题。

死锁

什么死锁?

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

死锁产生的4个必要条件?

产生死锁的必要条件:

  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
Reentrant lock(可重入锁)

这个也是同步解决并发的方法,而且我感觉比synchronized更简单好用一些,synchronized 你需要考虑对那个对象加锁,这个只需要把不安全的修改代码加个锁,等使用完了,再把资源解锁了就行。

package com.sfc;

import lombok.SneakyThrows;

import java.util.concurrent.locks.ReentrantLock;

public class test
{

    public static void main(String[] args)
    {
        GetCourse getCourse = new GetCourse();

        new Thread(getCourse, "何夜息").start();//同学1抢票
        new Thread(getCourse, "张三").start();
        new Thread(getCourse, "李四").start();
        new Thread(getCourse, "王五").start();
    }
}

//模拟学生抢票
class GetCourse implements Runnable{

    private int courseNum = 10;//课程名额
    private boolean flag = true;//外部停止方式
    private final ReentrantLock lock = new ReentrantLock();//声明一个锁

    @SneakyThrows
    @Override
    public  void run()
    {
        while (flag)
        {
            getCourse();
        }
    }

    //抢课
    private void getCourse() throws InterruptedException
    {
        //把不安全的代码进行加锁
        try
        {
            lock.lock();//开始加锁不安全的代码
            if(courseNum<=0)//没有名额了
            {
                this.flag = false;
                return;
            }
            Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时
            //名额减一
            courseNum--;
            System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
        } finally
        {
            lock.unlock();//这里对资源进行解锁
        }
    }
}
线程池

为什么要使用线程池,当我们需要使用多线程时,如果都是自己管理去管理,可能会造成性能降低,抢占资源等各种现象,所以我们可以一个线程池容器,来为我们管理这些线程。

我通过ExecutorService来创建线程池,启动线程,关闭线程池。

package com.sfc;

import lombok.SneakyThrows;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

public class test
{

    public static void main(String[] args)
    {
        GetCourse getCourse = new GetCourse();
        //创建线程池服务
        ExecutorService service = Executors.newFixedThreadPool(10);//参数为线程池大小

        //执行线程,不用自己掉start()了
        service.execute(new Thread(getCourse, "何夜息"));
        service.execute(new Thread(getCourse, "张三"));
        service.execute(new Thread(getCourse, "李四"));
        service.execute(new Thread(getCourse, "王五"));

        //关闭线程池
        service.shutdown();
    }
}

//模拟学生抢票
class GetCourse implements Runnable{

    private int courseNum = 10;//课程名额
    private boolean flag = true;//外部停止方式
    private final ReentrantLock lock = new ReentrantLock();//声明一个锁

    @SneakyThrows
    @Override
    public  void run()
    {
        while (flag)
        {
            getCourse();
        }
    }

    //抢课
    private void getCourse() throws InterruptedException
    {
        //把不安全的代码进行加锁
        try
        {
            lock.lock();
            if(courseNum<=0)//没有名额了
            {
                this.flag = false;
                return;
            }
            Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时
            //名额减一
            courseNum--;
            System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);
        } finally
        {
            lock.unlock();
        }


    }
}
输出
pool-1-thread-1拿到了票,剩余:9
pool-1-thread-3拿到了票,剩余:8
pool-1-thread-3拿到了票,剩余:7
pool-1-thread-3拿到了票,剩余:6
pool-1-thread-3拿到了票,剩余:5
pool-1-thread-3拿到了票,剩余:4
pool-1-thread-3拿到了票,剩余:3
pool-1-thread-3拿到了票,剩余:2
pool-1-thread-3拿到了票,剩余:1
pool-1-thread-3拿到了票,剩余:0

进程已结束,退出代码 0

然后发现给线程取名字好像不行哦,都是人家帮你弄好了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值