Java熟练掌握多线程(超吐血超详细)

引言:线程是为了处理并发任务的,一个线程完成一个任务,写线程实际上就是写任务,其核心点就是拆分任务交给线程去完成,本文只简单介绍Java多线程的入门,线程过于复杂,所以这里只做简单的入门解析。

一,多线程的概念

1.线程和进程

进程:简单理解,进程就是一个程序,运行在内存中。每次需要运行这个程序,都需要把它加进内存中。(可以打开任务管理器)里面所显示的就是进程。每个进程都有独立的空间。

线程:线程是进程里面的一个单元,也可以看作是一个独立运行的程序。比如main方法也是一个线程,我们管他叫主线程。在一个进程里面,可能有多个线程,他们共享同一数据:存于堆中的数据。

多线程设计的目的:优点:多个线程效率快,并且能实现高并发。缺点:多线程存在安全问题,但是为了效率和速度牺牲一部分安全性是可以的,我们可以通过其他手段来尽可能保障安全。

2.并行和并发

并行:取决于硬件,所谓并行就是多任务系统,多个任务同时进行。CPU单核数不能并行。

并发:看似同时发生的事件;例如两人过独木桥,两人一前一后过桥,这就叫并发(独木桥比喻的是CPU核数,意思是并发实际上是CPU单核就可以完成的多任务);而并行则是:两人并排过自己的桥。

3.认识线程

线程就是一个Thread类型的对象。

举例:main方法也是一个线程,为主线程。

public class Test {
    public static void main(String[] args) {
       
       //获取当前线程
        Thread th = Thread.currentThread();
        //更改th线程的名字
        th.setName("主线程");
       //打印线程th的名字
        System.out.println(th.getName());
       //打印线程th的编号
        System.out.println(th.getId());
       //打印线程th的优先级
        System.out.println(th.getPriority());
    }
}
输出结果为: main(更改名字后为 “主线程”)
           1
           5

二,创建线程

创建线程的四种方式:

🔴继承Thread类

🔴实现Runnable接口

🔴实现Callable接口

🔴线程池

1.继承Thread类

编写类>>new对象>>调方法

1.1.1编写类

public class MyThread extends Thread{
    //在线程里面,完成任务必须要用到run方法
    public void run() {
        //执行的任务代码块
        //执行输出循环0-100的数字,并且带上执行的线程名。
        for(int i = 0;i<=100;i++) {
            System.out.println(Thread.currentThread().getName()+"-"+i);
        }
    }
}

1.1.2 new对象和调方法。

public class Test {
    public static void main(String[] args) {
        MyThread mth1 = new MyThread();
//        mth1.run(); 此处不能直接调用run方法,这里调用run方法是由main线程来执行的,所以输出结果的前面线程名为mian-1,main-2····

        mth1.start();//启动线程,线程调用方法要使用start,这里输出结果为Thread0-1,Thread0-2···
    
    }
}

🔴注意:start调的run方法没有参数,并且不能抛异常。传参时,请注意run方法是否满足重写。

2.实现Runnable接口

2.1编写类

public class MyThread implements Runnable{ //实现Runnable接口
    //在线程里面,完成任务必须要用到run方法
    public void run() {
        //执行的任务代码块
        for(int i = 0;i<=100;i++) {
            System.out.println(Thread.currentThread().getName()+"-"+i);
        }
    }
}

2.2 new对象和调方法

public class Test {
    public static void main(String[] args) {
        MyThread mth1 = new MyThread();
        //Thread类能调到start方法
        Thread th1 = new Thread(mth1);
        th1.start();        
    }
}

🔴注意:实现Runnable接口时,不能直接使用new的对象调用start方法;原因是因为Runnable中没有start方法;我们需要将实现了Runnable接口的对象参数传给Thread的对象,这样才能用Thread的对象调用start方法。

实现接口的方式要好一些,为常用方法;原因:

1,java是单继承,如果我们的类已经继承了一个类,就没法继承线程类了。

2,我们实现接口可以方便共享数据。

🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻

例如:两个线程共同输出100个数字🔻

第一步,编写类

public class MyThread implements Runnable {
    int num = 100;
    public void run() {
        while (true) {
            if (num <= 0) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + ":" + num--);
        }
    }
}

第二步,new对象以及调用方法。

public class Test {
    public static void main(String[] args) {
        //设计一个共享对象
        MyThread mth1 = new MyThread();
        Thread th1 = new Thread(mth1);

        Thread th2 = new Thread(mth1);
        th1.start();
        th2.start();
    }
}

🔴注意:两个线程共享run方法,但是会生成两个单独的方法栈,相当于每个线程都有自己的方法栈去运行。

线程的使用以及生命周期:

编写模板>>创建线程对象(新建)>>就绪,运行>>阻塞>>就绪,运行>>死亡(run方法执行完毕)。

3. 实现Callable接口

如果我计算完成能够把结果返回给你,那我们就可以直接在调用者那获取线程的结果,

🔴如果调用线程需要线程返回结果,就用Callable 。

优点:1,有返回值。  2,可以抛异常。(而Runnable不可抛异常,只能try-catch)

import java.util.concurrent.Callable;

public class Test3 implements Callable {
    int[] nums;
    public Test3(int[] nums){
        this.nums = nums;
    }
    @Override
    //这里的call方法自动阻塞,方法执行完后才能拿到它返回的结果
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 0;i<nums.length;i++){
            sum += nums[i];
        }
        return sum;
    }
}

三,线程的调度

线程调度就是线程的几个方法:

🔻setPriority()设置线程的优先级 1-10(默认是5)。

设置了线程的优先级,仅仅是这个线程有概率影响这个线程的运行,不是绝对的。

🔻setName()更改线程的名字。

🔻Thread.sleep()静态方法 线程休眠。

休眠要释放CPU,但是不释放锁。

🔻Thread.yield() 礼让方法,让该线程暂时释放CPU,但该线程依旧可以抢CPU资源。

🔻join 线程加入方法。 

应用场景:两个线程并行,在某个时间点,一个线程绝对优先执行(谁调用谁优先),等该线程执行完毕后,另外一个线程才能执行。

会阻塞当前运行线程,不影响其余线程。

1.线程中断

中断其实就是一个标记,中断这个方法是线程类里面的,我们在run里面去监测这个中断,意思是在run里面去调用这个中断方法,所以我们使用继承来测试。

package demo;

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        InterThread th = new InterThread();
        th.start();
        //中断
        System.out.println("是否中断线程?y/n");
        Scanner input = new Scanner(System.in);
        char choice = input.next().charAt(0);
        if(choice == 'y') {
            th.interrupt();//中断
        }
    }

}
class InterThread extends Thread{
    public void run() {
        while(true) {
            //检测是否收到中断信号
            if(this.isInterrupted()) {
                System.out.println("线程被中断");
                break;
                
            }
        }
        
    }
}
运行结果:

是否中断线程?y/n
y
线程被中断

🔴注意:线程点出来的interrupt方法只是将原线程中的一个boolean值从false变为true,只是一个信号,并不会真正中断线程。如果要实现线程的中断,则需要去检测有没有收到这个信号(这个boolean值是否变为true),之后自己编写中断代码。(也可以在接受到信号时,做一些其他的事情,不一定是中断,要灵活运用)sleep时中断用catch异常捕获,直接中断sleep。

2.守护线程

守护线程就是在后台运行的线程,当所有的用户线程执行完后,它才会结束。

自己设置守护线程的方法: ThreadName.setDaemon(true);

四,线程安全 

1.synchonized简单使用

隐式实现锁,正因为是隐式实现锁,所以才很难看清楚原理是怎么实现的。

其原理下文会讲到。

同步代码块:

package site;
//举例,卖票网站的实现原理
//举例为网站只有一个,大家买票都在这里
//所以这里我们实现Runnable接口来达成数据共享
public class Site implements Runnable{
    int num = 10;
    int count = 0;

    //卖票的方法
    @Override
    public void run() {
        System.out.println(this.hashCode());
        while (true){
            //以下的this指的是Site对象,因为例子只有一个对象,所以可以用this来锁
            synchronized (this){//同步代码块
                if (num<=0){
                    break;
                }
                count++;
                num--;
            }
            System.out.println(Thread.currentThread().getName()+"买到了第几"+count+"张票,还剩下"+num+"张票");
        }
    }
}

Test:

package site;

public class Test {
    public static void main(String[] args) {
        Site site = new Site();
        System.out.println(site.hashCode());
        Thread th1 = new Thread(site,"淘票票");
        Thread th2 = new Thread(site,"去哪儿");
        Thread th3 = new Thread(site,"携程");
        th1.start();
        th2.start();
        th3.start();
    }
}

🔴此处举例是因为new出的三个线程需要共用一个对象,所以我们只需要锁住三个线程的访问点,也就是this,此处的this就是new出的site对象。通过hashcode值也可以证明此点。可以理解为,现在this就是锁,三个线程当中每一个线程要想执行run方法,都需要持有这把锁,而这把锁也只能同一时间被一个线程持有。这样就避免了三个线程同时访问数据,更改数据的时候产生的线程安全。

同步方法块:

package site;

public class SiteMethod implements Runnable {
    int num = 10;
    int count = 0;
    boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            sale();
        }
    }
    //同步方法,当一个线程执行该方法时,其余线程不能执行
    public synchronized void sale() {
        if (num <= 0) {
            flag = false;
            return;//结束
        }
        count++;
        num--;
        System.out.println(Thread.currentThread().getName() + "买到了第几" + count + "张票,还剩下" + num + "张票");
    }
}

🔴同步方法后的锁为当前对象this,同步方法不能写对象,只能用当前对象做锁。如果方法为静态方法,则不能用synchronized(对象锁)加锁。

synchronized底层原理:

synchronized在实现时,并不是在编译时加几行代码,而是在编译后的字节码(class)文件中添加了几行指令(monitorenter)(monitorexit)>>监视器,这就是synchronized底层的实现。jdk1.6之前synchronized是通过互斥来实现的,这会导致效率很低。1.6之后有了 🔻锁的升级膨胀🔻 。

😛😛😛😛偷个懒 待修改更新和完善~

  • 15
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一直在奋斗的鲨鱼辣椒!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值