多线程基本使用

多线程

引入:多线程在我们网络生活中及其常见,例如边打游戏边听歌,在同一时间断执行多个代码,在程序中就要使用多线程。

首先我们要先了解几个概念:什么是并发,什么是并行,什么是进程,什么是线程。

并行: 指两个或多个事件在同一时刻发生(同时执行)。

并发: 指两个或多个事件在同一个时间段内发生(交替执行)。

在这里插入图片描述

进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程: 是进程中的一个执行单元或者叫控制单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

例如:我们的main方法就是一个线程,它有它特定的名称叫做(main线程或者主线程)。

那么JVM虚拟机启动时有几个线程呢?
我的回答是两个:一个是main线程,一个是垃圾回收线程(GC)。

在这里插入图片描述

了解这几个概念后,那么久让我们看看如何创建一个线程,线程的创建方式有两种:
1.继承Thread类,重写run方法。
2.实现Runnable接口,重写run方法。

Thread方式:
首先我们要看Thread中有哪些构造方法和那些成员方法供我们使用

构造方法:
public Thread() :空参构造
public Thread(String name) :创建对象时指定线程名称
public Thread(Runnable target) :创建Thread对象并传入一个实现Runnable接口的类
public Thread(Runnable target,String name) :创建Thread对象并传入一个实现Runnable接口的类并指定线程名称。

成员方法:这些方法在下面的例子中我们会用到
public String getName() :获取当前线程名称。
public void start() :开启当前线程,JVM默认调用线程中的run方法。
public void run() :线程要执行的代码。
public static void sleep(long millis) :让线程睡多久,传入毫秒值。
public static Thread currentThread() :返回当前线程的引用。

继承Thread方式创建一个线程类:

//自定义线程类
public class MyThread extends Thread {
	//继承Thread类重写run方法
    @Override
    public void run() {
    	//run方法里面是我们自己编写线程要执行的代码。
        System.out.print("我是通过继承Thread创建的线程!");
    }
}

//测试类
public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); //start()方法是开启线程,后面会说到
    }
}

测试结果:
在这里插入图片描述

实现Runnable方式创建一个线程类:

//自定义一个线程类 实现Runnable的方式
public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.print("我是通过实现Runnable接口创建的线程!");
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //创建线程类对象
        MyThread mt = new MyThread();

        //创建Thread类,传入继承了Runnable接口的线程类
        Thread t = new Thread(mt);

        t.start(); //start()方法是开启线程,后面会说到
    }
}

测试结果:
在这里插入图片描述

到这里我相信大家一定想问使用多线程的好处是啥?
(单核情况下)多线程的好处就是可以让多个线程同时执行,多个线程抢用一个CPU资源,达到多个程序同时运行的效果。
现在我们看一下多线程的运行效果:

//自定义一个线程类 实现Runnable的方式
public class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread().getName()获取当前线程的名称
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}

//测试类
public class ThreadTest {
    public static void main(String[] args) {
        //创建线程类对象
        MyThread mt = new MyThread();

        //创建Thread类,传入继承了Runnable接口的线程类
        Thread t = new Thread(mt);

        //启动线程
        t.start();

        //注意:这个循环是在main方法中执行的,它属于main线程
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread().getName()获取当前线程的名称
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

测试结果:
在这里插入图片描述

上述两种方式我们创建线程还需要另外创建一个线程类,比较麻烦。
下面介绍一下匿名内部类的方式:

public class ThreadTest {
    public static void main(String[] args) {

        //第一种,直接new Thread (常用)
        //往构造方法中传入一个字符串就是给Thread创建的时候设置一个名称
        new Thread("***"){ 
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //Thread.currentThread().getName()获取当前线程的名称
                    System.out.println(Thread.currentThread().getName() + "---" + i);
                }
            }
        }.start();
        
        //第二种,new Thread 并往里面传入一个Runnable接口 (基本不用传入Runnable不是多此一举吗?)
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //Thread.currentThread().getName()获取当前线程的名称
                    System.out.println(Thread.currentThread().getName() + "---" + i);
                }
            }
        }).start();
    }
}

测试结果:
在这里插入图片描述

sleep方法:

public class ThreadTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            try {
                //每循环一次暂停一秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }
}

测试结果:
在这里插入图片描述

(面试题)(可能面试会问到哦!)
继承Thread和实现Runnable接口有什么好处与坏处呢?
如果你已经继承Thread类 就不能再继承其他类了 (因为一个类只能继承一个类)
而实现接口可以实现多个 (而一个类可以实现多个接口)
避免了单继承的局限性,减少了类域类之间的依赖,降低了耦合度。

所以说在使用多线程的时候还是建议使用实现Runnable接口的方式。

多线程的高并发问题及线程安全

高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节,12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。

线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。

多线程的运行机制:
当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
看一下简单的线程的代码:

在这里插入图片描述
多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题

多线程的安全性问题-可见性

例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。

public class MyThread extends Thread {
    public static int num = 0;

    @Override
    public void run() {
        System.out.println("线程开启!");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
        System.out.println("线程完结!");
    }
}

public class Work1 {
    public static void main(String[] args) {
        new MyThread().start();

        while (true){
            if (MyThread.num == 1){
                break;
            }
        }
        System.out.println("main结束!");
    }
}

运行结果:因为while循环里面用的副本num无法得到更新,所以他还是0,会一直进入死循环。main方法无法正常结束。
在这里插入图片描述

多线程的安全性问题-有序性

有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10; //1行
int b = 20; //2行
int c = a + b; //3行
第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译
完毕。1和2先编译谁,不影响第三行的结果。
但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
在这里插入图片描述
多线程的情况下 ,我们是不希望对代码进行排重的。

多线程的安全性问题-原子性

public class MyThreadWork2 extends Thread {
    public static int num = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class Work2 {
    public static void main(String[] args) {
        new MyThreadWork2().start();

        for (int i = 0; i < 10000; i++) {
            MyThreadWork2.num++;
        }
        System.out.println(MyThreadWork2.num);
    }
}

运行结果:

在这里插入图片描述
结论:按照常理来讲最终结果应该是20000,但是这里的结果却不是,我们连续运行几次发现也不是20000,那么到底是为什么呢,因为num作为public static 所修饰的共享变量,在进行++时,如果说0线程目前+到了4002,当CPU把执行权给了main线程以后那么它会从4002开始+,当main中的线程执行完毕后,就会输出num信息 ,然后main线程结束,此时0线程还没有+完,所以输出的值就不是20000,虽然后续0线程还会继续给num++;但是main线程已经结束,不会再输出了,所以控制台打印的就是不到20000的数字。
所以说两个线程访问同一个变量num的代码不具有"原子性

解决方法

下面就会有volatile原子类出场。

先说volatile,它可以用于修饰变量,当共享变量被修饰后就不会出现,可见性有序性问题了。
这个比较简单就不在演示了。

原子类:他们的基本实现是基于CAS(乐观锁)的机制实现的,下一篇会说到。
1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
它们可以保证对“变量”操作的:原子性、有序性、可见性。

成员方法:
get() 获取当前值。
getAndIncrement() 相当于i++;
incrementAndGet() 相当于++i;
addAndGet(int参数) 相当于当前值与传入的值相加;
getAndSet(int参数) 返回的参数是旧值,参数是新值。

getAndSet演示:

AtomicInteger ai = new AtomicInteger(12);
int num = ai.getAndSet(88);
//最终的值是
num = 12;
ai = 88;

基本使用:

public class MyThread extends Thread {
    public static AtomicInteger ai = new AtomicInteger();//空参就是0,传入的是几就是几

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            ai.getAndIncrement();//先获取再自增;类似于a++;
        }
    }
}


public class Work1 {
    public static void main(String[] args) {

        new MyThread().start();

        for (int i = 0; i < 10000; i++) {
            MyThread.ai.getAndIncrement();//先获取再自增;类似于a++;
        }

        try {
            Thread.sleep(1000); //睡一秒 防止主线程一下执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(MyThread.ai.get());
    }
}

运行结果:可以看到,问题得到了解决。

在这里插入图片描述

数组类型:
1).java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类。
2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。
3).java.utio.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。

演示:
在这里插入图片描述

多线程.start之后会立即启动线程吗?
回答:不是,是要看CPU和系统的调度,源码中真正启动的不是start而是start0

好了,这里就介绍那么多多线程的基本使用,下面还会持续发布多线程解决高并发问题和锁,线程状态等高级部分的文章,请注意查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章