JAVA并发编程-1-线程基础

一 、基础概念

1,cpu核心数与线程数

核心数 : 线程数 = 1 : 1
什么意思呢,就是说如果是一个8核的cpu,那么该cpu会至少支持8个线程同时运行
intel引入了超线程技术后:
核心数 : 线程数 = 1 : 2

而我们在编码过程中可以感觉到同时运行的线程远远不止这些。
这是由于cpu的时间片轮转机制又称RR调度,简单点讲,操作系统会把已就绪的线程排成一个队列,给每个进程一个时间分片,该线程在cpu中执行完这个时间分片后,不论是否执行完都会让出cpu资源给另外的线程,这样在某一时间段内就好像有很多线程在同时运行。

值得注意的一点是,操作系统和cpu在进行时间分片的任务切换时也是需要时间的,而且往往占用的时间比例又很大,(举个例子:时间分片有20ms,切换就要5ms),所以我们在多线程开发时关注上下文切换对于多线程执行时间和性能的影响。

2,进程和线程

进程:程序运行进行资源分配的最小单位,进程中有多个线程,会共享这个线程的资源
线程:cpu调度的最小单位,必须依赖进程而存在

举个例子:启动的一个jar包程序就是一个进程,而我们可以通过启动参数配置它的内存大小,-xmx,-xms等。而每一个请求都是在线程上去进行的,cpu通过执行线程任务完成每个请求任务。

3,并行和并发

并行:同一时刻,可以同时处理事情的能力
并发:与单位时间有关,在单位时间内可以处理问题的能力

举个例子,假设不考虑超线程技术,一个4核cpu在任何一个时刻处理的是4个线程,并行数为4,而由于时间片轮转机制,它在1秒内可以支持处理100个线程,它在1秒内的并发数为100

4,高并发编程的意义与问题

好处 :
(1)充分利用cpu的资源。如果是单线程,只占用一个核,其它的空闲
(2)加快响应时间。合理的设计多线程程序,使请求处理加快。
(3)程序模块化异步化

注意事项:
(1)线程共享资源,会存在冲突
(2)会存在死锁
(3)启动线程太多,滥用线程,压垮服务器

二、实现线程的三种方式

1,继承 Thread 类

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("我是线程MyThread");
    }

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

2,实现 Runnable 接口

为什么还要有继承接口的方式呢?因为java单继承

public class MyRunThread implements Runnable {

    @Override
    public void run() {
        System.out.println("我是线程MyRunThread");
    }

    public static void main(String[] args) {
        MyRunThread myRunThread = new MyRunThread();
        new Thread(myRunThread).start();
    }
}

而有了java8 lamda表达式之后,看到Runnable意识到他是个典型的函数式接口,可以更加优雅的实现

    public static void main(String[] args) {
        new Thread(() -> System.out.println("我是lamda MyRunThread")).start();
    }

3,实现 Callable 接口

Callable 接口支持在主线程中拿到子线程的执行结果:

public class MyCallThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "我是 MyCallThread 的执行结果 callReslut";
    }

    public static void main(String[] args) throws Exception {
        MyCallThread myCallThread = new MyCallThread();
        FutureTask<String> futureTask = new FutureTask<>(myCallThread);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

值得关注的一点是:这里的futureTask.get()方法不需要显示的判断线程是否完成,而是会自己阻塞直到拿到线程的执行结果。有兴趣的同学可以去研究下源码。

三、线程的状态

线程只有5种状态,整个生命周期只会在这5钟状态下切换。

    public enum State {
        /**
         * 初始状态,线程被构建new,但是还没有调用start方法
         */
        NEW,
        /**
         * 运行状态,java线程将操作系统中的就绪和运行两种状态笼统的称为"运行中"
         */
        RUNNABLE,
        /**
         * 阻塞状态,表示线程阻塞于锁🔒
         */
        BLOCKED,
        /**
         * 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
         * 比如通过Object.wait()进入WAITING,就需要Object.notify()或者Object.notifyAll()进行通知
         * 调用Thread.join()进入进入WAITING,就需要等待规定的线程执行完成
         */
        WAITING,
        /**
         * 超时等待状态,表示线程在规定时间内等待
         * Thread.sleep(long),Object.wait(long)等方法
         */
        TIMED_WAITING,
        /**
         * 终止状态,表示当前线程执行完毕
         */
        TERMINATED;
    }

在这里插入图片描述
值得强调的两点:
1,java线程的运行状态不同于操作系统的运行状态,而是对就绪和运行两种状态的统称
2,TIMED_WAITING有的人不理解为何还有这个状态。其实可以想一下,有超时时间的等待和无超时时间的等待执行的指令是不一样的,因为还要判断超时,所以需要两种状态来描述

四、线程方法

首先我们要记住一个概念,java线程是协作式的,而不是抢占式的

1, 线程终止的方法interrupt()

线程之前提供了stop(),resume(),suspend()方法来终止线程,但已不建议使用,stop()会导致线程不会正确的释放资源,suspend()会导致死锁。

我们要通过interrupt(),isInterrupted(),static interrupted()来自己实现中断线程

interrupt() :调用一个线程的interrupt() 方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定。
isInterrupted(): 判定当前线程是否处于中断状态。
static interrupted() :判定当前线程是否处于中断状态,同时中断标志位改为false。

public class MyRunThread implements Runnable {

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("线程MyRunThread is run");
        }
        System.out.println("线程MyRunThread interrput flag is " + Thread.currentThread().isInterrupted());
    }

    public static void main(String[] args) throws Exception {
        MyRunThread myRunThread = new MyRunThread();
        Thread thread = new Thread(myRunThread);
        thread.start();
        Thread.sleep(3);
        thread.interrupt();
    }
}

运行结果:
在这里插入图片描述
方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。

在这里插入图片描述
总结一下,interrupt方法实际上就提供了一个可靠的中断标志位,线程在自己的运行过程中去判断这个标记位,决定自己的中断与否。实际上我们自己定义一个合适的变量也可以达到目的。

2,等待和通知wait(),notify(),notifyAll()

典型的等待通知机制,也可以理解为生产者消费者模式
等待和通知的标准范式
等待方:
1、获取对象的锁;
2、循环里判断条件是否满足,不满足调用wait方法,
3、条件满足执行业务逻辑
通知方来说
1、获取对象的锁;
2、改变条件
3、通知所有等待在对象的线程

public class Express {

    int i = 0;

    public synchronized void check() {
        if (i <= 0) {
            System.out.println("不满足条件");
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        System.out.println("被唤醒---");
    }

    public synchronized void change() {
        i = 1;
        notifyAll();
    }

    public synchronized void changeSingle() {
        i = 1;
        notify();
    }

}
public class Test {
    private static Express express = new Express();

    private static class Checki extends Thread {
        @Override
        public void run() {
            express.check();
        }
    }

    public static void main(String[] args) throws InterruptedException{
        for (int i = 0; i < 3; i++) {
            new Checki().start();
        }
        Thread.sleep(1000);
//        express.change();
//        express.changeSingle();
    }
}

另外,这3个方法是定义在Object类中而不是Thread类中。
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁,那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

3,利用join()保证线程的执行顺序

线程A,执行了线程B的join方法,线程A必须要等待B执行完成了以后,线程A才能继续自己的工作

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(() -> System.out.println("我是第1个线程"));
        Thread thread2 = new Thread(() -> System.out.println("我是第2个线程"));
        Thread thread3 = new Thread(() -> System.out.println("我是第3个线程"));
        thread1.start();
        //主线程调用了 thread1 的join,则一定会等待thead1执行完了才会继续执行
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
    }

join底层也是依靠wait()和notify()来实现的,感兴趣的同学可以自己看下

4,调用yield() ,sleep(),wait(),notify()等方法对锁的影响

yield()方法:当前线程在当前时间分片的任务执行完成,让出cpu执行权。
该方法不会释放持有的锁。
sleep(): 不会释放锁
调动方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放,当wait方法返回的时候,线程会重新持有锁
调动方法之前,必须要持有锁,调用notify()方法本身不会释放锁的,要等待代码块跑完。

五、线程的优先级

thead.setPriority()方法
优先级的范围1~100,缺省为5,但线程的优先级不可靠,不建议作为线程开发时候的手段。

原因是java线程是映射到系统的原生线程来实现的,所以线程的调度最终决定于操作系统。虽然现在很多操作系统提供了线程优先级的概念,但是不见得会与java线程的优先级一一对应,如果优先级比java线程多还好说,要是少的话,就不得不出现几个优先级相同的情况了。

另外还有一些情况让我们不能太依赖优先级:优先级可能会被系统自行改变。在widows系统存在着一个“优先级推进器”,它的大致作用就是当前系统发现一个线程执行的特别“勤奋努力”的话,可能就会越过线程优先级给它分配时间。

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