java多线程之简单介绍

多线程

1、简单介绍:

为什么需要多线程?在早期的设计当中,是没有多线程的存在的,甚至也没有进程。随着操作系统的发展,慢慢演化出来的进程和线程。
可以说线程就是迷你版的进程,但是对于一个进程来说,操作系统为其分配资源,而线程是在进程之中存在的,也就是说线程是在进程空间中的,
那么线程所能够利用进程的资源。对于java的JVM进程来说,是一个用户级线程。

但是在这个过程中,本来程序中很多不相干的程序代码杂糅在一起,导致了CPU的利用率没有充分发挥。

所以为了充分压榨CPU,也为了程序运行的更加快速,从而出现了多线程。原来一个人需要做各种各样的事情来完成一个目标,现在是一群人一起互相帮助来达到相同的目标。

操作系统中的线程和java中的线程是极其类似的,只不过java在设计的时候,和操作系统中的线程有点区别。之后会来进行详细的说明和介绍

2、多线程

java提供了Thread类的API来操作线程,通过这个类可以获取得到多线程的各种信息。

先写代码,再一个一个慢慢解释

2.1、创建线程

创建线程通常来说是有两种方式:1、实现Thread类;2、继承Runnale接口,实际上Thread类也是实现了Runnable接口。

所以总的来说,实现线程的方式最终也还是只有一种方式来进行实现:

@Slf4j
public class CreateThreadDemoOne {
    public static void main(String[] args) {
        // 这里是主线程的位置
        log.info("线程种执行的代码------------->我的线程名字是---->{}",Thread.currentThread().getName());

        // 创建线程并执行
        MyThread thread = new MyThread();
        new Thread(thread).start();
    }
}

/**
 * 通过继承的方式来实现我们自己的线程类
 */
@Slf4j
class MyThread extends Thread {
    /**
     * 一定要重写这个方法
     */
    @Override
    public void run() {
      log.info("线程种执行的代码------------->我的线程名字是---->{}",Thread.currentThread().getName());
    }
}

为什么说要重写run方法?这个在之后会来进行详细的说明。

通过实现Runnable接口来操作:

@Slf4j
public class ThreadDemoOne {
    public static void main(String[] args) {
        // 这里是主线程的位置
        log.info("线程种执行的代码------------->我的线程名字是---->{}",Thread.currentThread().getName());


        // 创建线程并执行
        MyThread thread = new MyThread();
        new Thread(thread).start();

        // 主线程睡一会儿
        try {
            Thread.sleep(1000);
            log.info("我是{},睡醒了",Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@Slf4j
class MyThread implements Runnable{
    @Override
    public void run() {
        log.info("I can run");
    }
}

通过观察,发现上面的代码是及其相似的,那么我们两种通过都会来使用。

但是在jdk8及其之后,更喜欢使用接口来实现,因为可以使用lambda的方式来快速实现一个线程的调用:

@Slf4j
public class ThreadDemoOne {
    public static void main(String[] args) {
        log.info("当前线程是:{}",Thread.currentThread().getName());
        new Thread(()->{
            log.info("I can run");
        }).start();
    }
}

在上面中创建的两个线程对象,最后都会传给Thread的构造中去,然后调用start方法;但是从上面可以看到,自定义的线程类实现或者是继承了接口,那么都重写了run方法,为什么不调用对象直接调用run方法而是调用start方法呢?看下代码,然后走下源码:

        // 创建线程并执行
        MyThread thread = new MyThread();
        new Thread(thread).start();

从构造方法中开始进入,点击init方法,最终会走到下面的源码中来:

// 一定要看注释!不看注释就不知道干嘛的
    /**
     * Initializes a Thread.功能介绍:创建一个线程
     * 参数介绍
     * @param g the Thread group
     * @param target the object whose run() method gets called  这个才是重点参数目标对象调用run方法,找到这个参数调用的地方即可
     * @param name the name of the new Thread
     * @param stackSize the desired stack size for the new thread, or
     *        zero to indicate that this parameter is to be ignored.
     * @param acc the AccessControlContext to inherit, or
     *            AccessController.getContext() if null
     * @param inheritThreadLocals if {@code true}, inherit initial values for
     *            inheritable thread-locals from the constructing thread
     */
   // 现在其他的参数不跟!直接跟踪target
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        // 跟踪到了这里,发现只是给成员变量去进行赋值而已!但是上面说了,target变量会调用run方法!       
        // 刚刚在进行继承或者是实现的时候,又重写了run方法,根据多态的性质,最终会调用自己实现的run方法
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
// 上面的重点参数的注释!可以看到我们为什么要实现Runnable接口了,这里利用了多态
// 这里也是为何说要么是通过继承Thread(实现了Runnable),或者是实现Runnable接口
/* What will be run. */
private Runnable target;
	// 导致这个线程开始运行,java虚拟机将会调用这个线程的run方法
    // 也就是说我们创建的线程在调用start方法之后,会调用重写的run方法
    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     // 看看上面的介绍:结果就是将会调用两个线程来执行,一个调用start方法,另外一个线程调用run方法
     * <p>
     * It is never legal to start a thread more than once.  通常来说,一个线程启动多次是合法的,在执行完成之后不会再次执行啦
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            // 调用了本地方法
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
private native void start0();

本地方法,看不到源码了。

        // 创建线程并执行
        MyThread thread = new MyThread();
        new Thread(thread).start();

这段代码背后的原理:在自定义了线程类对象之后,重写了run方法,传递给Thread构造之后,调用了start方法后。首先保存当前的线程的引用,然后在调用start方法的之后,JVM将会调用当前线程(我们自定义)引用的run方法。

那么这里就是为何我们自定义的线程不去调用run方法来进行执行了。对象执行自己重写的run方法,最终的效果就是一个普通对象调用普通方法执行,而看不到线程的效果。所以线程的创建是由JVM通过系统调用向操作系统来实现的。

在操作系统章节里介绍了java使用的线程模型是KLT模型。

3、对于任务的理解

重写的run方法,可以理解成是我们程序对应的线程所要去做的事情。那么参考着之前只是在单线程中写的代码,也就是在主线程中写的代码。在main线程(没有写多线程的时候)的执行,在main线程种执行的代码就是main线程中在做一件事情。

对于多线程来说,也就是多个线程之间各自做各自需要来做的事情。多线程之间因为存在于同一个进程之中(在一个进程中开辟了多个线程),因为进程是资源分配的单位,而线程是处于进程之间的,还是CPU调度的最小执行单元。所以多线程之间会存在着协调关系、竞争关系等等问题。

4、线程之间的运行关系图

所以我们可以看到其他线程一定是在主线程中创建出来的。

5、守护式线程和非守护式线程

java中的线程分为了守护式线程和非守护式线程。

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意以下几点:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。

(2) 在Daemon线程中产生的新线程也是Daemon的。

(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

所以我们创建的线程中,在线程启动的时候没有对其进行设置setDaemon(true),说明了我们设置的都是非守护式线程。

只有当所有的非守护式线程执行完成之后,JVM才会退出;没有执行完成,JVM就会继续执行,一直等到非守护式线程执行完成。

posted @ 2021-10-12 00:27  雩娄的木子  阅读(141)  评论(0编辑  收藏  举报