Java多线程基础

得意时要看淡,失意时要看开。不论得意失意,切莫大意;不论成功失败,切莫止步。志得意满时,需要的是淡然,给自己留一条退路;失意落魄时,需要的是泰然,给自己觅一条出路Java多线程基础,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

进程

进程:
是处于运行过程中的程序,具有一定的独立性,进程是系统进行资源分配和调度的一个独立单位。
进程包括三个特征:

  1. 独立性
    进程是系统中独立的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间。(在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间)
  2. 动态性
    进程和程序的区别在于,程序是一个静态的指令集合;而进程是一个正在系统中活动的指令集合。在进程中加入了时间概念。进程具有自己的生命周期和各种不同的状态。
  3. 并发性
    多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。

并行和并发

并行:在同一时刻,由多个指令在多个处理器上同时执行。
并发:在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。

线程

线程:
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源。
线程的执行是抢占式的。

在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。
线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。
注意 如果多次调用start() 方法,则会出现异常Exception inthread”main”java.lang.IllegalThreadStateException。

线程方法说明

注意:

  1. Thread.java类中的start() 方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run() 方法。这个过程其实就是让系统安排一个时间来调用Thread中的run() 方法,也就是使线程得到运行,启动线程,具有异步执行的效果。
  2. 如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。
  3. 执行start()方法的顺序不代表线程启动的顺序。

线程的创建

java创建线程,主要有以下三种方式:

继承Thread类创建线程类

步骤:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

在默认情况下,主线程的名字为 main,用户启动的多个线程的名字依次为:Thread-0,Thread-1,…,Thread-n等

案例如下:

public class FirstThread extends Thread {

	private int i;

	@Override
	public void run() {
		for (; i < 100; i++) {
			// 直接使用this就可以获取当前线程
			System.out.println(getName() + " " + i);
		}
	}

	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);

			if (i == 20) {
				// 创建并启动第一个线程
				new FirstThread().start();
				// 创建并启动第二个线程
				new FirstThread().start();
			}
		}
	}
}

结论:
上面的程序共三个线程,分别是main,Thread-0,Thread-1。Thread-0和Thread-1两个线程输出的i变量不连续。因为i是FirstThread的实例变量,程序每次创建线程对象时都需要创建一个FirstThread对象,所以不能共享该实例变量。

实现Runnable接口创建线程类

具体步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法是该线程的执行体
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法来启动该线程

注意:
Runnable接口中只包含一个抽象方法,从Java8开始,Runable接口是用了@FunctionalInterface修饰,也就是说,Runnable接口是函数式接口,可使用Lambda表达式式创建Runnable对象。
Callable接口也是函数式接口。

案例如下:

public class SecondThread implements Runnable {

	private int i = 0;

	@Override
	public void run() {
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
		}
	}

	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);

			if (i == 40) {
				SecondThread thread = new SecondThread();

				// 通过new Thread(target, name)创建新线程
				new Thread(thread, "Thread 1").start();
				new Thread(thread, "Thread 2").start();
			}
		}
	}

}

结论:
两个子线程的i变量是连续的,也就是说Runnable接口的方式创建的多个线程可以共享线程类的实例变量。
因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享一个线程类的实例变量。

使用Callable和Future创建线程

从Java5开始,Java提供了Callable接口,该接口像是Runnable接口的增强版,Callable接口提供了一个call方法可以作为线程执行体,但call()方法比run()方法功能更强大。
Callable接口是函数式接口,所以可以使用Lambda表达式创建Callable对象。

  • call()方法可以有返回值
  • call()方法可以申明抛出异常

创建并启动有返回值的线程步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

案例如下,
第一种方式,不使用Lambda表达式:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThirdThread implements Callable<Integer> {

	@Override
	public Integer call() throws Exception {
		int i = 0;
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " 的循环变量i值为: " + i);
		}
		return i;
	}

	public static void main(String[] args) {
		// 创建Callable对象
		ThirdThread thread = new ThirdThread();

		// FutureTask类来包装Callable对象
		FutureTask<Integer> task = new FutureTask<Integer>(thread);

		for (int j = 0; j < 100; j++) {
			System.out.println(Thread.currentThread().getName() + " 的循环变量j值为: " + j);
			if (j == 40) {
				// 实质还是以Callable对象来创建并启动线程
				new Thread(task, "有返回值的线程").start();
			}
		}

		try {
			// 获取线程返回值
			System.out.println("子线程的返回值:" + task.get());
		} catch (InterruptedException | ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

第二种方式,使用Lambda表达式:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FourThread {

	public static void main(String[] args) {
		FourThread thread = new FourThread();

		// 先使用Lambda表达式创建Callable<Integer>对象
		// 使用FutureTask类来包装Callable对象
		FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> {
			int i = 0;
			for (; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " 的循环变量i值为: " + i);
			}
			return i;
		});

		for (int j = 0; j < 100; j++) {
			System.out.println(Thread.currentThread().getName() + " 的循环变量j值为: " + j);
			if (j == 40) {
				// 实质还是以Callable对象来创建并启动线程
				new Thread(task, "有返回值的线程").start();
			}
		}

		try {
			// 获取线程返回值
			System.out.println("子线程的返回值:" + task.get());
		} catch (InterruptedException | ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

注意:
程序最后调用了FutureTask对象的get()方法来返回call()方法的返回值—-该方法将导致主线程被阻塞,直到call()方法结束并返回为止。

创建线程的三种方式的对比

实现Runnable接口和实现Callable接口的方式基本相同,只是Callable接口定义的方法有返回值,并且可以声明抛出异常而已。他们可以归为一种。
采用实现Runnable接口、Callable接口的方式创建多线程
优点:

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类
  • 多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想

缺点:

  • 编程稍微复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法

采用继承Thread类的方式创建多线程
优点:

  • 编写简单,如果需要访问当前线程,无须使用Thread.currentThread()方法,直接使用this即可获得当前线程

缺点:

  • 因为线程类已经继承了Thread类,不能再继承其他父类

守护线程

在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。

典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。
用个比较通俗的比喻来解释一下“守护线程”:任何一个守护线程都是整个JVM中所有非守护线程的“保姆”,只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),它就是一个很称职的守护者。

案例如下:

public class DaemonThread {

    public static void main(String[] args) {
        try {
            MyThread myThread = new MyThread();
            myThread.setDaemon(true);
            myThread.start();
            Thread.sleep(5000);

            System.out.println("当我结束,thread对象也不再打印");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyThread extends Thread {
    private int i = 0;

    @Override
    public void run() {
        super.run();

        try {
            while (true) {
                i++;
                System.out.println("i=" + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

i=1
i=2
i=3
i=4
i=5
当我结束,thread对象也不再打印

在代码中,我们使用了setDaemon(true),所以myThread对象的线程为Daemon线程,所以此时只有main线程是唯一一个非守护线程,一旦main线程运行结束,守护线程也退出了,然后整个程序结束工作。
如果我们没有设置守护线程,即把上面这行代码去掉,我们再运行起来,程序就会一直运行下去。因为myThread对象的线程也是非守护线程,它还在一直运行,所以整个程序不会停止。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/155835.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!