Loading

零基础入门学习Java之多线程

多线程

话不多说,看代码

1.什么是多线程

众所周知CPU单线程的东西,也就是说在同一时间内程序只能去做一件事情,但很多时候比如说多人买票、龟兔赛跑、游戏开发等都需要在同一时间内完成多个东西,因此就有了多线程的概念。

2.多线程的作用

在有了多线程之后,我们可以让程序在同一时间内做多个事情,比如程序一边读取一边处理输出,程序一边渲染动画一边处理数据等

3.多线程的本质

理论上说,多线程是并发的,需要多核同时工作才行,但早期的电脑是单核的,为了使电脑能处理多样事情,CPU会在第一个线程和第二个线程种反复的执行,由于CPU速度很快因此我们是看不到程序有卡顿的现象

4.代码的实现

在Java种有三种方式实现多线程,这里演示其中的两种

1.创建一个类 继承Thread对象 并且重写run方法

package com.SatrThead.Test02;

public class Demo01 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("我只子线程");
        }
    }

    public static void main(String[] args) {
        
        Demo01 demo01 = new Demo01();
        
        demo01.start(); //必须是start方法
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主线程");
        }
        
    }
}

2.继承Runnable接口 代理线程

package com.SatrThead.Test02;

public class Demo02 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("我只子线程");
        }
    }

    public static void main(String[] args) {
        
        Demo02 demo02 = new Demo02(); //创建Runnable代理
        new Thread(demo02).start();  //执行线程
        
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主线程");
        }

    }
}

在使用多线程之前,我们要明白main方法是程序种的主线程,对此我们可以用以下代码去理解多线程

package com.SatrThead.Test01;

public class Demo02 implements Runnable{

    private int ticket = 10;//初始化票数

    @Override
    public void run() {
        while(ticket > 0){
            //输出判断哪个线程拿到了第几张票 并且让票数递减
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"票");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Demo02(),"小明").start();
        new Thread(new Demo02(),"小李").start();
        new Thread(new Demo02(),"小张").start();
        new Thread(new Demo02(),"老师").start();
    }
}

由此可得,线程创建出来过后并不是马上就执行的,具体的执行时间是要根据CPU的调度

如果你细心肯定会发现,程序并不完善,有时候同一张票会被多个人拿到,这是我们不想要的结果 后续会解决

5.静态代理模式

面对初学者,可能会对Runnable的实现方式不太理解,实际上这是代理模式的设计,对此我们可以随意的去创建一个接口通过创建的接口去学习代理模式

interface Marry{
    void happyMarry();
}

简单明了的一个接口,里面只有一个抽象方法就是庆祝结婚

在创建两个类去继承接口

class People implements Marry{

    @Override
    public void happyMarry() {

    }

}

class Company implements Marry{
    @Override
    public void happyMarry() {

    }
}

在这两个方法种,重写一下庆祝婚礼方法,在People中可以随便写,但在代理类Company里面就不能随意写了

class Company implements Marry{

    private People target;

    public Company(People p){
        this.target = p;
    }

    public Company(){}

    private void before(){
        System.out.println("结婚开始前 婚庆公司在准备中ing....");
    }

    private void after(){
        System.out.println("结婚结束后 婚前公司撤离 糟糕忘记收钱了!!!");
    }

    @Override
    public void happyMarry() {
        before();
        target.happyMarry();
        after();
    }
}

在编写Company的时候,为了能让Company代理People去Marry但不是”取代“我去Marry因此里面有些东西是Company类去做,但核心还是People去做在,因此我们在主方法可以这么写

package com.SatrThead.Test02;

public class Demo03 {

    public static void main(String[] args) {
        People people = new People();
        new Company(people).happyMarry();
    }

}

interface Marry{

    void happyMarry();

}

class People implements Marry{

    @Override
    public void happyMarry() {
        System.out.println("结婚?结婚是不可能的,这辈子都不可能结婚的");
    }

}

class Company implements Marry{

    private People target;

    public Company(People p){
        this.target = p;
    }

    public Company(){}

    private void before(){
        System.out.println("结婚开始前 婚庆公司在准备中ing....");
    }

    private void after(){
        System.out.println("结婚结束后 婚前公司撤离 糟糕忘记收钱了!!!");
    }

    @Override
    public void happyMarry() {
        before();
        target.happyMarry();
        after();
    }
}

image

这里是不是和我们的Runnable实现方法一模一样?其实它也是这么封装的

6.Lamda表达式

首先我编写了一个接口,接口中只有一个抽象方法,那么我们可以实例化接口并且编写它的抽象方法

package com.SatrThead.Test02;

public class Demo04 {
    
    public static void main(String[] main){
        
        LambdaTest lambdaTest = new LambdaTest() {
            @Override
            public void printHello() {
                System.out.println("Hello~");
            }
        };
        
    }
    
}

interface LambdaTest{
    void printHello();
}

从JDK1.8开始Java可以使用Lamda表达式,也就是所谓的箭头函数来实现仅有一个抽象方法的接口实例化

package com.SatrThead.Test02;

public class Demo04 {

    public static void main(String[] main){

        LambdaTest lambdaTest = new LambdaTest() {
            @Override
            public void printHello() {
                System.out.println("Hello~");
            }
        };
        
        LambdaTest lambdaTest2 = ()->{
            System.out.println("hello~~");
        };

    }

}

interface LambdaTest{
    void printHello();
}

这是对抽象方法编写的一种简化,当然对于以上代码完全可以省区花括号(仅限抽象方法只有一条语句)

package com.SatrThead.Test02;

public class Demo04 {

    public static void main(String[] main){

        LambdaTest lambdaTest = new LambdaTest() {
            @Override
            public void printHello() {
                System.out.println("Hello~");
            }
        };

        LambdaTest lambdaTest2 = ()->System.out.println("hello~~");
        

    }

}

interface LambdaTest{
    void printHello();
}

如果要传递参数呢?? 很显然我们可以在括号内传递参数

package com.SatrThead.Test02;

public class Demo04 {

    public static void main(String[] main){


        LambdaTest lambdaTest2 = (String name)->System.out.println(name + " hello~~");
        lambdaTest2.printHello("StarVk");

    }

}

interface LambdaTest{
    void printHello(String name);
}

当然对于只有一个参数的我们还可以更简便 我们甚至可以把参数省略掉

package com.SatrThead.Test02;

public class Demo04 {

    public static void main(String[] main){


        LambdaTest lambdaTest2 = name->System.out.println(name + " hello~~");
        lambdaTest2.printHello("StarVk");

    }

}

interface LambdaTest{
    void printHello(String name);
}

如果有多个参数 那么只能老老实实的写括号了

package com.SatrThead.Test02;

public class Demo04 {

    public static void main(String[] main){


        LambdaTest lambdaTest2 = (name,name2)->System.out.println(name + " kill " + name2);
        lambdaTest2.kill("StarVk","Mike");

    }

}

interface LambdaTest{
    void kill(String name,String name2);
}

7.线程停止

关于线程停止,我认为Java给我们提供了方法,但又没有完全提供

通常来说,Java不建议我们使用官方的方法来手动结束线程,通常是以一个While循环来终止线程,实际上线程并没有停止,只是通过flag把它变为了空的线程

package com.SatrThead.Test03;

public class Demo01 implements Runnable{

    private boolean flag = true;

    @Override
    public void run() {

        while(flag){
            System.out.println("我在这,我在这");
        }
    }

    public void stop(){
        flag = false;
    }

    public static void main(String[] args) {

        Demo01 demo01 = new Demo01();
        new Thread(demo01).start();//启动线程

        for (int i = 0; i < 100; i++) {
            System.out.println("我是cpu,让他停止好嘛"+i);
            System.out.println("我是主线程 我现在不想停止demo01线程");
            System.out.println("那我过0.01秒再问问");
        }
        System.out.println("我是cpu,让他停止好嘛");
        System.out.println("那就让他停止把");
        demo01.stop(); //停止线程

    }


}

8.线程休眠

在以往例子中,我有写过线程休眠的例子,通过Thread的sleep方法去让线程在置顶毫秒内休眠,之后会重新准备被CPU调度

注:不能在循环中对线程休眠,否则休眠结束后CPU调度的还是原来的线程,sleep()抱锁休眠

package com.SatrThead.Test01;
public class Demo02 implements Runnable{

    private int ticket = 10;

    @Override
    public void run() {
        while(ticket > 0){
            buy();
        }
    }

    public void buy(){
            if(ticket < 0)
                return;
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"票");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    }

    public static void main(String[] args) {

        Demo02 demo02 = new Demo02();
        new Thread(demo02,"小李").start();
        new Thread(demo02,"小明").start();
        new Thread(demo02,"小张").start();
        new Thread(demo02,"老师").start();
    }
}


9.线程礼让

当有两个线程A和线程B,CPU先调度线程A,但线程A很想让线程B调度,因此线程A执行yield()方法重新回到开始等待被CPU调度,此时,CPU会重新在线程A和线程B之中选择一个去调度,也就是说要么礼让成功要么礼让不成功

对此可以创建线程A和线程B,先启动A线程在启动B线程,A作为礼让线程

package com.SatrThead.Test03;

public class Demo03 {
    public static void main(String[] args) {
        myThreadA myThreadA = new myThreadA();
        myThreadB myThreadB = new myThreadB();

        myThreadA.start();
        myThreadB.start();
    }
}

class myThreadA extends Thread{
    @Override
    public void run() {
        System.out.println("我是线程A 我想礼让线程B");
        Thread.yield();
        System.out.println("我要结束了不知道礼让成功没 ai");
    }
}

class myThreadB extends Thread{
    @Override
    public void run() {
        System.out.println("我是线程B 我想要先运行 我不管!");
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("哼!");
    }
}

image

10.强制执行线程

有时候CPU会和人一样转不过来,把最不重要线程先执行了,此时我们可以通过join方法去插队

注:join方法插队后会一直执行此线程知道线程结束

package com.SatrThead.Test03;

public class Demo02 implements Runnable {
    @Override
    public void run() {
        for (int i = 0;i<1000;i++){
            System.out.println("至尊VIP驾到 让开"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo02 demo02 = new Demo02();
        Thread thread = new Thread(demo02);

        thread.start();
        for (int i = 0; i < 100; i++) {
            if(i==50){
                thread.join(); //当循环到第50次的时候插队
            }
            System.out.println("我是主线程 我最大"+i);
        }
    }
}

11.线程状态

线程有五大状态

* 刚创建好没启动的的**初始状态**
* 启动了等待CPU调用的**等待状态**
* 被CPU调用的**运行状态**
* 遇到延时等方法的**堵塞状态**
* 执行自闭后的**死亡状态**

在Thread类中有一个类是State类,我们可以获取这个类取查看线程的执行状态

package com.SatrThead.Test03;

public class Demo04 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("我还在跑"+i);
            if(i == 3){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

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

        Thread thread = new Thread(new Demo04());
        Thread.State state = thread.getState();


        System.out.println(state);

        thread.start();
        state = thread.getState();
        System.out.println(state);

        while(state != Thread.State.TERMINATED){
            System.out.println(state);
            state = thread.getState();
            Thread.sleep(100); //主线程跑太快了不方便观察 加个延迟
        }

    }
}

12.线程的优先级

在Java中我们可以提高线程的优先级从而让某个线程更有可能的被CPU调度

注意:要先设置优先级在启动线程

package com.SatrThead.Test03;

public class Demo05 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"在运行:"+Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority());

        Demo05 d1 = new Demo05();
        Demo05 d2 = new Demo05();
        Demo05 d3 = new Demo05();
        Demo05 d4 = new Demo05();
        Demo05 d5 = new Demo05();

        Thread t1 = new Thread(d1,"张三");
        Thread t2 = new Thread(d2,"李四");
        Thread t3 = new Thread(d3,"王五");
        Thread t4 = new Thread(d4,"小明");
        Thread t5 = new Thread(d5,"小刚");

        //先设置优先级后启动
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(3);
        t3.setPriority(9);
        t4.setPriority(6);
        t5.setPriority(3);

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


    }
}

13.守护线程

Java线程中分为守护线程和用户线程,JVM保证在结束前执行完所有的用户线程,但不会管守护线程

可以通过setDaemon()方法设置线程为守护线程,即JVM讲不会取管这个线程是否会执行完毕

package com.SatrThead.Test03;

public class Demo06 {

    public static void main(String[] args) {
        God god = new God();
        People people = new People();

        Thread threadPeople = new Thread(people);
        Thread threadGod = new Thread(god);
        threadGod.setDaemon(true);

        threadGod.start();
        threadPeople.start();
    }

}

class God implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("我会一直守护着你");
        }
    }
}

class People implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("我已经活了"+i+"天");
        }
        System.out.println("Goodbye~");
    }
}

14.线程同步机制

多线程在很大程度上,给程序提供很强大的功能,但多线程也是不安全的,如果多个线程同时操作同一个资源那么就会出现资源抢夺的问题,这个是危险的,就好比张三有100w,他取银行取钱,于此同时张三女朋友在家通过手机银行也在张三账户上取钱,张三与他的女友在同一时间取了60w如果不对两个取钱的线程就行同步就会引发多取出的问题

在了解线程同步之前,得先有锁和队列得概念

每个对象都有一把锁,他藏在对象头之中

Java可以通过synchronized关键词修饰方法来获取对象得锁,通过synchronized修饰得方法获取得锁是this的

当然也可以通过

synchronized(obj){

}

来获取obj对象的锁,前提是要弄清楚锁哪,一般都是锁线程之间的公共资源,即会被操作的代码段

当一个线程获取到锁之后,其他线程执行同代码段的时候就会就行排队,等待当前线程释放了锁之后才能轮到下一个线程获取锁(列队)

在第8小点中买票的例子,会发现一张票可能会被多个人抢到,导致线程不安全,原因就是线程A和线程B同时看到第七张票并且都拿到了第七张票,对此通过加锁的方式可以这样解决

package com.SatrThead.Test01;
public class Demo02 implements Runnable{

    private int ticket = 10;

    @Override
    public void run() {
        while(ticket > 0){
            buy();
        }
    }

    public synchronized void buy(){
            if(ticket < 0)
                return;
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"票");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    }

    public static void main(String[] args) {

        Demo02 demo02 = new Demo02();
        new Thread(demo02,"小李").start();
        new Thread(demo02,"小明").start();
        new Thread(demo02,"小张").start();
        new Thread(demo02,"老师").start();
    }
}

在四个线程同时想要使用buy方法进行买票时,通过锁机制,让他们进行排队买票

posted @ 2021-07-16 14:27  StarVik  阅读(82)  评论(0编辑  收藏  举报