java学习----多线程

本文详细介绍了Java中的多线程概念,包括进程与线程的区别、线程的创建(通过继承Thread类和实现Runnable接口)、线程状态、常用线程方法、线程安全问题及解决方案,以及死锁的概念。通过实例展示了线程同步的实现,如同步代码块和同步方法,以及如何避免死锁。最后总结了多线程编程的关键点。
摘要由CSDN通过智能技术生成

Java学习----多线程

如果说,java的集合是与计算机的内存有关。
则多线程是和计算机的CPU有关系。
而IO则是与计算机的硬盘有关系。
网络编程又是与网卡有关。
以上四部分也是JavaSE中最核心的内容。

1.什么是线程和进程

进程:进程是CPU资源分配的基本单位,程序是静止的,运行的程序我们叫作进程。
线程:又称为轻量级进程,是CPU的基本调度单位,一个进程一般由多个线程构成,这些线程彼此完成不同的工作,交替执行,这就是多线程。每个进程启动后,都会有一个主线程。
如:java的虚拟机就是一个进程,当我们启动一个java程序的时候,实际上是开了一个线程,其中java程序的的Main执行之后又是主线程。

进程之间不能共享数据段地址,但是同进程之间的线程是可以共享的。

人是一个进程,但是你可以一边上课一边打游戏,上课和打游戏是两个线程。
打开电脑的资源管理器,可以看到诸多正在执行的进程与线程数以及进程的PID等信息。
在这里插入图片描述CPU通过进程的PID来管理线程。

2.线程的组成

线程最基本的组成部分

1.CPU时间片:OS会为每个线程分配执行时间。
2. 运行数据:堆空间:存储线程需要使用的对象,多个线程可以共享堆中的数据。
栈空间:存储线程使用的局部变量,每一个线程都拥有独立的栈。
3. 线程的逻辑代码。

线程的执行特点:
抢占式执行:效率高,可以防止一个线程长时间独占CPU。
在单核CPU上,宏观上同时执行,微观上顺序执行。
多核CPU是可以真正的实现多个线程并发执行。

3.java实现多线程

3.1线程的创建(继承Thread)

ok说了这么多,先看看,java里面一个线程的创建和运行。
JVM运行程序的时候会自动创建一个主线程(main)来执行main方法

public class MyThread extends Thread{

    @Override
    public void  run(){
        for(int i = 0;i < 5 ;i++){
            System.out.println("这是子线程" + i);
        }
    }
}

主函数调用

  public static void main(String[] args) {
        MyThread myThread = new MyThread();

        //启动子线程
        myThread.start();
        //此处不能直接myThread.run(),这样写话实际上还是主线程在执行

        for (int i = 0;i < 5;i++){
            System.out.println("这是主线程" + i);
        }
    }

执行之后的结果如下:
在这里插入图片描述
这里注意一点,由于现成的执行是抢占式执行,所以每次运行的结果都是不一样的
如此处第二次运行的结果与第一次运行的结果是不一样的。
在这里插入图片描述
这种情况下如果我们再添加一个线程myThread1的话,执行之后,子线程的输出会混在一起,无法区别是哪一个子线程的输出,这个时候我们可以通过调用,线程的getID()和getName()方法,进行不同线程的输出区分。

public class MyThread extends Thread{

    public MyThread(String threadName){
        this.setName(threadName);
        //super(threadName); 这样写 也是可以的
    }

    @Override
    public void  run(){
        for(int i = 0;i < 5 ;i++){
            //System.out.println("这是子线程" + i);
            //此处这两个方法,是父类的方法
            System.out.println(this.getId() + "******" + this.getName() + "子线程" + i);

            //但是如果不继承Thread的话就不能使用上述方法
            //通常会使用以下通用方法
            System.out.println(Thread.currentThread().getId() + "---" + Thread.currentThread().getName());

        }
    }
}

主函数:

 public static void main(String[] args) {

        //创建现成的时候就设置现成的名字
        MyThread myThread = new MyThread("thread1");
        MyThread myThread1 = new MyThread("thread2");

        //启动子线程
        myThread.start();
        myThread1.start();
        //此处不能直接myThread.run(),这样写话实际上还是主线程在执行

        for (int i = 0;i < 5;i++){
            System.out.println("这是主线程" + i);
        }
    }

执行结果如下:
在这里插入图片描述

3.1.1 案列,实现窗口各卖100张票

代码很简单

public static void main(String[] args) {

        TicketWin ticketWin1 = new TicketWin();
        TicketWin ticketWin2 = new TicketWin();
        TicketWin ticketWin3 = new TicketWin();
        TicketWin ticketWin4 = new TicketWin();

        ticketWin1.start();
        ticketWin2.start();
        ticketWin3.start();
        ticketWin4.start();
    }

TicketWin

public class TicketWin extends Thread{
    //票
    private int ticket = 100;

    @Override
    public void run(){

        while(true){
            if (ticket <= 0){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - ticket) + "张票");
            ticket --;
        }
    }

}

此示例内存占用示意图如下:
在这里插入图片描述
ticketWin1线程执行的时候。改变的tiket就是属于ticketWin1自己的ticket
在这里插入图片描述

3.2 线程的创建(实现Runnable接口)

Runnable,含有run()方法.
代码如下:

public class MyRunnable implements Runnable {

    /**
     * run()表示的是线程执行的功能
     */
    @Override
    public void run() {

        for(int i = 0; i < 50; i ++ ){
            System.out.println(Thread.currentThread().getName() + "---" + "this is a thread" + i);
        }

    }
}

主函数代码:

public static void main(String[] args) {

        //创建一个可运行对象(封装了线程执行的功能),但是执行的时候需要,交给线程来执行
        MyRunnable myRunnable = new MyRunnable();

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

        //启动线程
        thread.start();
    }

执行结果:
在这里插入图片描述

3.2.1 案例,四个窗口,共卖100张票

100张票涉及到资源共享问题。涉及到线程安全问题。
不考虑线程安全问题的时候,代码如下:
Ticket

public class Ticket implements Runnable{
    //100张票
    private int num = 100;

    //卖票,用run方法实现卖票功能
    @Override
    public void run(){

        while (true){
            if(num <= 0){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "卖了第" + (100 - num) + "张票");
            num --;
        }

    }
}

主函数

 public static void main(String[] args) {
        //1.创建票(也是可执行对象)
        Ticket ticket = new Ticket();

        //2.创建卖票的窗口
        Thread win1 = new Thread(ticket);
        Thread win2 = new Thread(ticket);
        Thread win3 = new Thread(ticket);
        Thread win4 = new Thread(ticket);

        //3.四个窗口用的是同一个票的资源
        win1.start();
        win2.start();
        win3.start();
        win4.start();
    }

由于没有考虑线程的安全性,所以会出现,多个窗口同时卖一张票的情况,执行结果:
在这里插入图片描述
很明显,上述线程0和线程3同时卖了第14张票。考虑线程安全的情况之后会给出。
先看一下,此时的内存示意图。
在这里插入图片描述
这时候,如果是我们启动了win1线程,则,有如下示意图。
在这里插入图片描述
同理,无论是win2win3win4最后变得ticketnum都是来自于同一个ticket

3.2.2 案例,你和你gf共用一张银行卡,你往卡里存钱,你女朋友花钱。

上述,案例中,run方法是可以放在ticket里面,因为上述的案例中,只有一种卖票操作,此案例中有,取钱和存钱两种不同的操作,所以就设置两种行为来分别表征。
代码如下:BankCard

public class BankCard {

    private double money = 0;

    public BankCard(){

    }
    public BankCard(double money) {
        this.money = money;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }
}

存钱类AddMoney

public class AddMoney implements Runnable {

    private BankCard card;

    public AddMoney(BankCard bankCard){
        this.card = bankCard;
    }
    //执行存钱功能。
    @Override
    public void run() {

        for (int i = 0;i < 10; i++){
            card.setMoney(card.getMoney() + 1000);
            System.out.println(Thread.currentThread().getName() + "存了1000元,余额是" + card.getMoney());
        }


    }

取钱类:SubMoney

public class SubMoney implements Runnable{

    private BankCard card;

    public SubMoney() {
    }

    public SubMoney(BankCard bankCard) {
        this.card = bankCard;
    }

    @Override
    public void run() {

        for (int i = 0; i < 10; i++){
            if(this.card.getMoney() >= 1000){
                card.setMoney(card.getMoney() - 1000);
                System.out.println(Thread.currentThread().getName() + "取钱,取了一千,余额是" + card.getMoney());

            }
            else {
                System.out.println("赶紧存钱");
                i--;  //没取到钱的话不算,保证钱一定能够花完
            }
        }
    }

主函数:main()

public static void main(String[] args) {

        //银行卡
        BankCard bankCard = new BankCard();
        //存钱功能
        AddMoney addMoney = new AddMoney(bankCard);
        //取钱功能
        SubMoney subMoney = new SubMoney(bankCard);

        //线程来执行功能
        //你自己要存钱
        Thread yourself = new Thread(addMoney, "你自己");
        //女朋友取钱
        Thread yourgirl = new Thread(subMoney, "女朋友");

        //启动线程
        yourself.start();
        yourgirl.start();
    }

由于目前没有考虑线程安全的问题,所以会出现很多不合理的情况。运行部分结果如下所示。
在这里插入图片描述
上述代码的写法都比较基础,其实可以使用匿名内部类的写法。代码如下:

   public static void main(String[] args) {
        //创建银行卡
        BankCard bankCard = new BankCard();
        //存钱
        Runnable addMoney = new Runnable() {
            private BankCard card = bankCard;
            @Override
            public void run() {
                for(int i = 0; i < 10; i++ ){
                    this.card.setMoney(this.card.getMoney() + 1000);
                    System.out.println(Thread.currentThread().getName() + "存钱成功,余额是" + this.card.getMoney());
                }
            }
        };
        //取钱
        Runnable subMoney = new Runnable() {
            private BankCard card = bankCard;
            @Override
            public void run() {
                for (int i = 0; i < 10; i++){
                    if(this.card.getMoney() >= 1000){
                        this.card.setMoney(this.card.getMoney() - 1000 );
                        System.out.println(Thread.currentThread().getName() + "取钱成功,余额是" + this.card.getMoney());
                    }
                    else {
                        System.out.println("余额不足,赶紧打钱");
                        i--;
                    }
                }
            }
        };
        //执行线程
        new Thread(addMoney, "yourself").start();
        new Thread(subMoney, "yourgirl").start();
    }

执行结果同样是乱七八糟的。
在这里插入图片描述
由于线程是抢占式执行,在写程序的时候,可以多运行几次试试,体验一把每次都是新结果的新奇。

4.线程的状态

现在,结合上述内容说一下线程的诸多状态以及状态之间的转换。

在这里插入图片描述
调用Thread的一个叫getState()的方法:
在这里插入图片描述
去查看State这是一个枚举类型:内含有所有状态。并含有所有状态的描述,有兴趣的可以去看。
在这里插入图片描述

 1. NEW
 2. RUNNABLE
 3. BLOCKED
 4. WAITING
 5. TIMED_WAITING
 6. TERMINATED

有了线程基本状态变化的基础之后,接下来看一下java里面,提供的给线程执行的一些常见方法。

4.1 常见的线程的方法
4.1.1 休眠sleep(long millis)

当前线程主动休眠millis秒,此期间将CPU释放,并不再争抢。

public class MyThread extends Thread{

    @Override
    public void run(){
        for (int i = 0;i < 50;i++){
            System.out.println(Thread.currentThread().getName() + "--" + i);
            try {
                Thread.sleep(1000);   //休眠一会
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行的时候是一个一个慢慢的出现。

4.1.2yield()主动释放CPU

但是释放了CPU之后,还会参与争抢,它只会把CPU让给优先级大于等于它的线程。

public class YieldThread extends Thread{

    @Override
    public void run(){
        for (int i = 0;i < 50;i++){
             System.out.println(Thread.currentThread().getName() + "--" + i);
             Thread.yield();
        }
    }
}

这个具体执行也看不出效果。

4.1.3 join()

允许其他线程加入到当前线程的执行,当前的线程会阻塞,直到加入的线程执行结束。
代码:

public class JoinThread extends Thread {

    @Override
    public void run(){
        for(int i = 0;i < 50;i++){
            System.out.println(Thread.currentThread().getName() + "--" + i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

主函数:

 public static void main(String[] args) {

        JoinThread joinThread = new JoinThread();
        joinThread.start();

        try {
            //加入线程,把joinThread加入到当前线程,
            // 等待joinThread执行完成之后,当前进程再次进行执行。
            joinThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for(int i = 0;i < 50; i++ ){
            System.out.println("这是主线程----*" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

执行结果是:
在这里插入图片描述
可见,子线程全都执行完之后才会执行主线程中的内容。

4.1.4 setPriority设置线程优先级

优先级为1-10,创建线程不设置的话,默认优先级是5。
代码:

public class PriorityThread extends Thread {

    public PriorityThread(String name) {
        super(name);
    }

    @Override
    public void run(){
        for(int i = 0; i < 50; i++ ){
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

主函数:

 public static void main(String[] args) {

        PriorityThread p1 = new PriorityThread("thread1");
        PriorityThread p2 = new PriorityThread("thread2");
        PriorityThread p3 = new PriorityThread("thread3");

        p1.setPriority(1);
        p3.setPriority(10);

        p1.start();
        p2.start();
        p3.start();
    }

设置了优先级之后,p1抢到CPU的几率是最小的,所以p1最晚执行完的几率会大,但是也不是绝对的。

4.1.5 interrupt()打断线程

会打断线程的执行,下面是例子
InterruptThread

public class InterruptThread extends Thread{

    @Override
    public void run(){
        System.out.println("子线程开始执行了");
        try {
            System.out.println("开始休眠");
            Thread.sleep(2000);
            System.out.println("休眠结束");
        }
        catch (InterruptedException e) {
            System.out.println("休眠的两秒内被打断了");
        }
        System.out.println("子线程执行结束了");
    }
}

主函数:

    public static void main(String[] args) throws IOException {
    
        InterruptThread interruptThread = new InterruptThread();
        interruptThread.start();

        System.out.println("20秒之内,输入任意字符,打断子线程");
        Scanner input = new Scanner(System.in);
        System.in.read();//程序执行到这里之后,如果不输入字符就会停止继续往下执行
        interruptThread.interrupt();

    }

如果不输入任何字符的话,子线程会顺利的休眠20S然后执行结束(主线程不会)
在这里插入图片描述
如果此时输入一个任意字符,则子线程还没有休眠完成就会被打断。
在这里插入图片描述

4.1.6守护线程

线程有两类,分别是:用户线程(前台线程)和守护线程(后台线程)
如果程序中所有的前台线程都执行完了,后台线程就会自动结束。
Java中垃圾回收器线程属于守护线程。
java中可以通过setDaemon(true)设置为守护线程。

示例代码:

public class DeamonThread extends Thread{

    @Override
    public void run(){
        for(int i = 0;i < 50;i++){
            System.out.println(Thread.currentThread().getName() + "---" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

主函数代码:

public static void main(String[] args) {

        DeamonThread deamonThread = new DeamonThread();

        //设置为守护线程
        deamonThread.setDaemon(true);

        deamonThread.start();

        for(int i = 0;i < 50; i++ ){
            System.out.println("这是主线程----*" + i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

执行结果:
在这里插入图片描述
我们可以看到,子线程虽然也是循环50次,但是它却在执行完20次的时候就停止了,主要是因为:当前的子线程为后台线程,而后台线程存在的意义是为用户线程(前台线程)服务。所以当主线程执行结束之后,子线程也结束了。

5.线程同步

上述事例中,当多个线程同时执行的时候,由于线程的执行是抢占式执行,所以会发生很多我们不想要的结果和情况。再比如,如果两个线程同时访问一个数组,一个用于存数据,一个用于取数据,那么取数据的线程如果执行在存数据的线程之前,就会发生错误。这是我们不想看到的情况。
以上问题,都可以通过给线程所使用到的临界资源上锁来解决,以下就是线程的同步问题。
上锁在操作系统上来说是一门“艺术”了,但是在java的实现中,只是依托于synchronized的几行代码(开发者牛!!!!)。

5.1 同步代码块
5.1.1 重写四个窗口卖100张票案例

分析之前的代码,主要原因是:
当第一个窗口卖了第一百张票的时候,i--还没有执行,CPU给予当前线程的时间片已经到了。第二个窗口马上抢占了CPU,第二个窗口这里发现 ,第一百张票还在,于是接着卖第一百张票…
1.找到发生冲突的地方加锁,就这个案例来说是while里面的代码。
代码:Ticket
利用Object加锁

public class Ticket implements Runnable{

    private int ticket = 100;
    private Object lock = new Object();
    public Ticket() {
    }

    @Override
    public void run() {
        while (true){

            synchronized (lock){
                if (ticket <= 0){
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket);
                ticket--;
            }
            
        }
    }
}

2.对当前对象ticket的引用加锁:

public class Ticket implements Runnable{

    private int ticket = 100;
  //private Object lock = new Object();
    public Ticket() {
    }

    @Override
    public void run() {
        while (true){

            synchronized (this){//指当前对象的引用
                if (ticket <= 0){
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket);
                ticket--;
            }

        }
    }
}

主函数:

public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket,"窗口1");
        Thread t2 = new Thread(ticket,"窗口2");
        Thread t3 = new Thread(ticket,"窗口3");
        Thread t4 = new Thread(ticket,"窗口4");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

执行结果,发现没有冲突发生了。

5.2 同步方法

如果一整个方法的代码都需要加锁,则直接对整个方法上锁。

  1. 如果方法是实例方法则,锁是this
  2. 如果是静态方法,锁是类名.class

代码

public class Ticket implements Runnable{

    private int ticket = 100;
  //  private Object lock = new Object();
    public Ticket() {
    }

    @Override
    public void run() {
        while (true){
            if (!sellticket()){
                break;
            }
        }
    }

    public synchronized boolean sellticket(){ //这个时候锁是this

        if (ticket <= 0){
            return false;
            }
        System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket);
        ticket--;
        return true;
    }
}
5.3 补一点死锁的知识

通俗一点解释,就是线程1手里有A资源但是在等B资源,线程2手里有B资源但是在等A资源,导致两个线程谁也无法执行完,谁呀无法释放资源,滞留在原地
例子,假如说,两个人去吃饭,A拿着第一根筷子,B拿着第二根筷子,谁都吃不了饭。
代码示例:
筷子类Chosticks

public class Chopsticks {

    protected static Object chopstick1 = new Object();
    protected static Object chopstick2 = new Object();

}

第一个人:

public class Person1 extends Thread{

    @Override
    public void run(){
        synchronized(Chopsticks.chopstick1){
            System.out.println(Thread.currentThread().getName() + "得到了第一根筷子");

            synchronized(Chopsticks.chopstick2){
                System.out.println(Thread.currentThread().getName() + "得到了第二跟筷子");
                System.out.println(Thread.currentThread().getName() + "可以吃饭了");
            }
        }

    }
}

第二个人:

public class Person2 extends Thread{

    @Override
    public void run(){
        synchronized(Chopsticks.chopstick2){
            System.out.println(Thread.currentThread().getName() + "得到了第二根筷子");

            synchronized(Chopsticks.chopstick1){
                System.out.println(Thread.currentThread().getName() + "得到了第一根筷子");
                System.out.println(Thread.currentThread().getName() + "可以吃饭了");
            }
        }

    }

主函数main:

    public static void main(String[] args) {
        Person1 person1 = new Person1();
        Person2 person2 = new Person2();

        person1.start();
        person2.start();

    }

执行结果:会陷入死锁状态,一直相互等待
在这里插入图片描述

6.总结

以上全都是一些基础的操作,后续的一些复杂操作,会在下一篇帖子写出,写CSDN主要是为了自己记录笔记,这又是一个很方便的平台,出现错误请务必及时指正。内容有的是看视频有的是看书得到的,有时候也会盗几个图。

ps:不知道是不是篇幅太长的原因,这篇文章的后半段打字确实卡的离谱。于是决定后续内容新开一篇。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沙丁鱼w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值